Docs
Errors
Every error has the same shape. Branch on type, show message to developers, never to end users.
The shape
HTTP/1.1 422
X-Request-Id: req_01JA0Q8X2M4N6P8R0T2V4W6Y8Z
{
"error": {
"type": "insufficient_balance",
"message": "Insufficient balance.",
"request_id": "req_01JA0Q8X2M4N6P8R0T2V4W6Y8Z",
"retryable": false,
"details": { "balance": 40, "required": 100 }
}
}
type: a stable code. Match on this. New types may be added over time, so keep a default branch.message: for developers. Its wording can change.request_id: also in theX-Request-Idheader. Send it to support.-
retryable:truefor429and every5xx,falsefor all other statuses. A retryable error should be retried with the sameIdempotency-Key(Idempotency); a non-retryable one will fail the same way until you change something. details: optional, extra fields that depend on the type (listed below).
All error types
| Status | type | Meaning and what to do |
|---|---|---|
| 400 | invalid_request | The body, query or a header failed validation, the JSON is malformed, or a starting_after cursor is unknown. On live, price or currency next to a receipt_id is refused here (the store's price is recorded), and so is bonus: true next to a receipt_id or a price. A validation failure carries details.issues, a list of path and message for each field (a missing Idempotency-Key shows up here too); a rule the ledger enforces (metadata limits) carries details.field; an unknown starting_after cursor may come without it. Fix the request. |
| 400 | sale_kind_required | A credit did not say what it is. Send exactly one kind: receipt_id for an App Store / Google Play sale, price and currency for a web sale, or bonus: true for coins with no sale behind them. Applies on test and live. See How your fee is measured. |
| 400 | currency_not_supported | Live only: a web sale (a credit with price and currency) must be declared in USD, EUR or a currency of the European Central Bank's reference rates. Send the price in one of those. See How your fee is measured. |
| 400 | period_out_of_range | A leaderboard at date is in the future or outside the retained history. Ask for a recent period. |
| 401 | invalid_api_key | See Authentication. |
| 401 | invalid_client_token | See Authentication. Mint a new token and retry once. |
| 403 | project_suspended | The live environment is suspended (details.reason for a secret key; a client token gets the generic message "This app is temporarily unavailable." and no reason). While suspended, a secret key can still read (GET, HEAD) and delete users (DELETE /v1/users/{id}); every other write and all client-token traffic is refused. Test keys are never suspended. Contact support. |
| 403 | test_limit_reached | A test environment hit its cap. details.limit is users or monthly_operations, details.max the cap. See Rate limits. Use a live key, or reset test data for the user cap. |
| 403 | room_not_allowed | A client token sent a gift with a room_id its rooms claim does not cover. Mint the token with that room or a matching prefix. |
| 403 | rooms_disabled | Gift rooms are switched off for the project (battle endpoints). |
| 403 | plan_required | The live environment needs a paid plan (details.plan). On the free Test plan a live key can only read (GET, HEAD), delete users, hide them from leaderboards and revoke client tokens; every other live write is refused, and client tokens get the generic project_suspended. Live gift rooms and battles need a paid plan too. Test keys and test rooms work on every plan. Choose a plan in the dashboard (Plan & billing). |
| 404 | not_found | The route, user, gift, gift transaction or client token does not exist in this project and environment. Check the id and that you are using the right key (test or live). |
| 409 | duplicate_reference | A credit or debit with this reference already exists. details.existing_entry_id points at it. If it is the same purchase, the work is done; otherwise use a new reference. |
| 409 | gift_exists | A gift with this id already exists. |
| 409 | already_reversed | This gift transaction was already reversed. A gift can be reversed once. |
| 409 | receipt_already_credited | This verified receipt already backed a credit (details.credited_at, details.ledger_entry_id). See Receipt validation. |
| 409 | project_deletion_pending | The project is scheduled for deletion, so the ledger takes no more writes (deleting a user is the one exception and still works until the purge). Cancel the deletion in the dashboard to continue. The project's keys stop working at once, so most requests see 401 instead; 409 is what a key still in the 30-second key cache gets. See Users and privacy. |
| 409 | receipts_not_configured | Receipt verification: the project has not set up that store in the dashboard yet (details.reason). See Receipt validation. |
| 409 | round_superseded | A battle start named an earlier round of this room (one of its last 16), for example a late retry after a newer round started. Nothing was changed. Use a new round id to start a round. |
| 410 | project_deleted | The project was deleted and its data erased. Only a key still in the 30-second key cache sees this; after that the key answers 401. |
| 413 | payload_too_large | The request body is over 16 KB. |
| 422 | insufficient_balance | The user does not have enough coins. details.balance and details.required. Offer a top-up. Only a debit with allow_negative: true may take a balance below zero. |
| 422 | balance_limit_exceeded | The new balance would be outside the allowed range (more than 1,000,000,000,000,000 in either direction). |
| 422 | user_deleted | The user was deleted and cannot be changed. |
| 422 | idempotency_key_reused | The Idempotency-Key was already used with a different endpoint or body within 24 hours. Use a new key for a new operation, or send the original request again. |
| 422 | catalog_full | The catalog already holds the maximum of 500 gifts (details.limit). Archive nothing: archived gifts still count, so reuse an existing gift or edit it instead. |
| 422 | gift_inactive | The gift is archived and cannot be sent. Pick an active gift. |
| 422 | self_gift_not_allowed | Sender and receiver are the same user. |
| 422 | receipt_revoked, receipt_price_mismatch, price_missing | A receipt-backed credit was refused: the store reported a refund, the price you sent differs from the receipt's, or (live) the Google Play price of the receipt could not be read: give the service account the View financial data permission and verify the receipt again. See Receipt validation. |
| 422 | receipt_invalid, app_mismatch, environment_mismatch, product_mismatch, receipt_not_found, store_credentials_rejected | Receipt verification refused the purchase; details.reason says exactly why (not authentic, another app, sandbox versus live, another product, unknown to the store, the store rejected the project's credentials). Do not grant the coins. See Receipt validation. |
| 422 | token_not_revocable | That client token has no id (it was minted before revocation existed). Revoke by user instead. |
| 429 | rate_limited | Too many requests: over the plan's limit for this project environment, over an end user's limit, or too many failed authentications from one IP address (a valid key is never limited by its IP). Wait Retry-After seconds: 10 for the request limits, up to an hour for the limit on new receivers (details.limit: "new_receivers_per_hour"). Retryable. See Rate limits. |
| 500 | internal_error | Something broke on our side. Retryable. Quote the request_id if it persists. |
| 503 | temporarily_unavailable | Client tokens or receipt verification are not available right now, or the project's ledger is momentarily overloaded (nothing was applied). Wait Retry-After seconds when present and retry with the same Idempotency-Key. Retryable. |
| 503 | store_unavailable | Receipt verification: the store (Google Play) did not answer. Retry-After says when to try again. Retryable. See Receipt validation. |
| 503 | leaderboard_building | Leaderboards are being built for the first time. details.retry_after and the Retry-After header give the seconds. Retryable. |
| 503 | rooms_unavailable | The gift room service could not be reached (battle endpoints). Retry. |
Receipt verification (POST /v1/receipts/verify) has its own error types, each with
a details.reason; the reasons are listed in
Receipt validation. Note that receipt_not_found is 404 when a credit names an unknown
receipt_id and 422 when the store does not know the purchase during
verification.
Handling errors in practice
- Retry only when
retryableis true, with backoff, and always with the sameIdempotency-Key. See Rate limits for a loop. - A timeout or a dropped connection tells you nothing about whether the request worked. Retry it with the same key: you get the first answer back, never a second change.
- Show end users your own wording.
insufficient_balanceusually means "open your coin store". - Log
typeandrequest_id. Do not log giftmessagetext ormetadata.
Questions or something unclear? support@giftingstack.com.