CreditKit Pro REST API reference

CreditKit Pro 2.2.0 registers 20 REST routes in the pcs/v1 namespace, and those routes carry 27 endpoint handlers once each HTTP method is counted on its own. The handlers cover credit balances, the bundle catalogue, the transaction ledger, team membership, store statistics, webhooks and API key creation. Every route is registered in includes/api/class-pcs-rest-api.php.

How do I authenticate?

Most of the API takes an API key. Send the key as Authorization: Bearer pcs_… or as X-API-Key: pcs_…. The query-string fallback was removed in 2.2.0, so a key passed as a URL parameter is ignored and the request answers 401 with the error code no_api_key.

Keys are issued in wp-admin under Credit System → API & Webhooks → API Keys. The plaintext key is displayed once at creation and stored only as a SHA-256 hash in wp_pcs_api_keys. One user may hold 10 active keys at a time, and an eleventh creation attempt fails with api_key_limit. POST /auth/key mints a further key for the calling admin, but that route itself needs a valid admin key, so the first key on a site always comes from the admin screen.

Exactly two routes accept the WordPress login cookie, and neither of them accepts an API key. Everything else in the namespace is either key-authenticated or open to anonymous callers.

RouteAccepted auth
GET /credits/meWordPress login cookie plus an X-WP-Nonce header
GET /credits/recommend-bundleWordPress login cookie plus an X-WP-Nonce header
GET /bundlesNone; anonymous callers are served
GET /bundles/{id}None; anonymous callers are served
Every other route and methodAPI key in Authorization: Bearer or X-API-Key

A cookie-authenticated call must carry a valid wp_rest nonce in the X-WP-Nonce header. WordPress treats a cookie request without that nonce as anonymous, and both routes then answer 401 with not_logged_in. Those two routes are the browser surface used by the product-page balance widget, which is why no wildcard Access-Control-Allow-Origin header is emitted for them. Key-authenticated routes do receive a wildcard CORS header, because a browser never attaches cookies to them.

A key can be pinned to a set of IP addresses through the _pcs_ip_whitelist user meta on the key owner. When that whitelist is set, a call from any other address answers 403 with ip_not_allowed.

Endpoint reference

Paths below omit the /wp-json/pcs/v1 prefix. “Self or admin” is the object-level check in assert_user_access_or_admin(): the authenticated user ID equals the target user ID, or the key owner has manage_options. Team routes add a second check: the key owner is the team’s manager_id, or has manage_options. POST /credits/add and POST /credits/spend honour an Idempotency-Key request header, and a replayed key returns the stored response with the header Idempotent-Replay: true for 24 hours.

RouteMethodAuthAuthorization rule
/credits/{user_id}GETAPI keySelf or admin
/credits/meGETCookie plus nonceReturns the caller’s own balance
/credits/recommend-bundleGETCookie plus nonceAny logged-in user; the payload is catalogue data
/credits/addPOSTAPI keyKey owner needs manage_options
/credits/spendPOSTAPI keySelf or admin
/bundlesGETNoneHidden bundles need manage_options plus include_hidden=true
/bundlesPOSTAPI keyKey owner needs manage_options
/bundles/{id}GETNoneAny caller reads one bundle record
/bundles/{id}PUT, PATCH or POSTAPI keyKey owner needs manage_options
/bundles/{id}DELETEAPI keyKey owner needs manage_options
/bundles/{id}/purchasePOSTAPI keySelf or admin; quantity is capped at 100
/transactionsGETAPI keyWith user_id: self or admin. Without it: manage_options
/badges/{user_id}GETAPI keySelf or admin
/teamsGETAPI keymanage_team_credits; a non-admin sees only teams it manages
/teamsPOSTAPI keymanage_team_credits
/teams/{id}/membersGETAPI keyManages that team, or manage_options
/teams/{team_id}/members/{user_id}PUT, PATCH or POSTAPI keyManages that team, or manage_options
/statsGETAPI keyKey owner needs manage_options
/stats/user/{user_id}GETAPI keySelf or admin
/webhooksGETAPI keyKey owner needs manage_options
/webhooksPOSTAPI keyKey owner needs manage_options
/webhooks/{id}GETAPI keyKey owner needs manage_options
/webhooks/{id}PUT or PATCHAPI keyKey owner needs manage_options
/webhooks/{id}DELETEAPI keyKey owner needs manage_options; answers 204 with no body
/webhooks/{id}/testPOSTAPI keymanage_options; 10 test deliveries per 5 minutes
/webhooks/{id}/deliveriesGETAPI keyKey owner needs manage_options
/auth/keyPOSTAPI keymanage_options; the new key belongs to the caller

Two rows list POST as an alternative verb because they are registered with WP_REST_Server::EDITABLE, which WordPress expands to POST, PUT and PATCH. PUT is the verb to write against, since both routes are idempotent upserts. The webhook update route is registered as PUT, PATCH only and refuses POST. The webhook {id} segment is a UUID4 in 8-4-4-4-12 hex form, and the route pattern rejects any other shape with a 404 before the handler runs. A webhook secret is returned once at creation and once more when rotate_secret is sent on an update; every read masks it. POST /teams writes budget_period, and the supported values are monthly, quarterly and yearly. GET /transactions pages through limit (maximum 100) and offset, and returns the row count in the X-WP-Total and X-WP-TotalPages response headers.

Error shape

A successful call returns {"success": true, "message": "…", "data": {…}, "timestamp": …}. Admin-gated responses add a version field; public responses omit it so the plugin version is not exposed to anonymous callers.

Failures are rewritten by a rest_request_after_callbacks filter that runs only on pcs/v1 routes, so they do not use the default WordPress error body. The filter emits {"success": false, "error": {"code": "…", "message": "…", "data": {…}}, "timestamp": "…"} and keeps the HTTP status the handler chose. A client can therefore branch on body.success alone rather than sniffing for a code property. Routes outside the namespace are left untouched by the filter.

Error codeHTTP statusCause
no_api_key401No key in the Authorization or X-API-Key header
invalid_api_key401The key is unknown or has been revoked
not_logged_in401A cookie route was called without a valid session and nonce
ip_not_allowed403The caller’s IP is outside the key owner’s whitelist
forbidden403The object-level check refused access to another user’s data
insufficient_permissions403The key owner lacks manage_options or manage_team_credits
user_not_found404The target user ID does not exist
team_not_found404The team ID does not exist or is not active
rate_limit_exceeded429The per-key request budget is spent

Rate limits

REST rate limiting counts requests per API key, not per IP address and not per user. The budget is 100 requests per minute and 1,000 requests per hour. Both numbers are hard-coded in PCS_REST_API::check_rate_limit(), with no setting and no filter to raise them. Counters live in a transient keyed on a SHA-256 hash of the key, and each window resets on the wall-clock minute or hour boundary.

Every key-authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. X-RateLimit-Reset holds the Unix timestamp at which the current minute window rolls over. A refused request answers 429 with rate_limit_exceeded and adds Retry-After in whole seconds. The two anonymous bundle routes are not counted, because there is no key to count them against.

The rate-limiting fields on the Credit System → Security screen do not change any of this. enable_rate_limiting, credit_transaction_limit, admin_action_limit and payment_action_limit sit with the plugin’s admin and AJAX guards, and their shipped defaults are 5, 10 and 3. None of the four reaches the pcs/v1 namespace.

Transaction types you’ll see

The type filter on GET /transactions is restricted to eight values, and a request outside that list is rejected by the route schema before the handler runs. The values are bonus, reward, referral, purchase, spend, refund_credit, admin_adjustment and expiry.

POST /credits/add accepts a narrower set: bonus, reward, referral and purchase. refund_credit and admin_adjustment are excluded from that route deliberately, since both skip the fraud and spending-limit pipeline and are issued through their own internal code paths. expiry rows are written by the expiry cron rather than by any endpoint.

Treat referral as a retained historical value rather than a feature. CreditKit Pro 2.2.0 ships no referral programme: the [pcs_referral] shortcode renders an empty string, the referral REST route and its handler were removed, and no code path in the plugin writes a row of that type. The value stays in the enum so ledgers migrated from earlier versions keep validating.

What this doesn’t do

  • GET /pcs/v1/leaderboard no longer exists. The route published customer display names alongside spend and balance totals to anonymous callers, so it was removed for privacy along with the matching shortcode. A regression test asserts the route stays unregistered.
  • No route removes a team member. Membership is an idempotent upsert through PUT /teams/{team_id}/members/{user_id}, which returns 201 on insert and 200 when it updates an existing member’s role. Removal happens in wp-admin through the pcs_remove_team_member admin action.
  • Daily and weekly team budget periods are not supported. The pcs_teams.budget_period column is an enum of monthly, quarterly and yearly, and the admin path rejects anything else.
  • There is no split-tender payment endpoint. An order is paid with credits or with money, and the API exposes no way to divide one order across both.
  • There are no holds or reservations endpoints. The reservation subsystem was cut, so available_balance in a credits response always equals current_balance.
  • Rate limits cannot be configured. Raising the 100-per-minute or 1,000-per-hour budget requires a code change in the plugin.
  • POST /auth/key cannot bootstrap a site. The route is gated behind an existing admin key, so the first key of any installation is created in wp-admin.