Skip to content
grabs.gg

Errors & limits

Every error string the API can return, aggregated from the endpoint reference so the two cannot disagree. The rule that matters most: a 200 response with verified: false is a normal answer, not an error — the task genuinely was not done.

Rate limits

EndpointLimit
GET /api/partner/connectBucket `partner-connect-ip:<ip>` — 60 requests per minute per client IP, fixed window, SHARED with the POST on the same path. Nobody bypasses it: there is no key to present on this route.
POST /api/partner/connectSame `partner-connect-ip:<ip>` bucket as the GET — 60/minute per IP across both methods, no bypass.
GET /api/partner/connect/infoBucket `partner-info-ip:<ip>` — 60 requests per minute per IP, fixed window, no bypass. It exists so client ids cannot be enumerated at speed, not because the data is secret.
POST /api/partner/v1/tokenTwo fixed 60-second windows: `partner-token-ip:<ip>` at a flat 60 requests/minute, and `partner:<clientId>:token` at your provisioned rate_limit_per_min. Nothing bypasses the IP bucket on this route.
GET /api/partner/v1/contestsThree buckets, all 60-second fixed windows. `partner-read-ip:<ip>` at 60/min is SKIPPED entirely when the Authorization header carries a key of the shape `gp_<digits>_<20+ chars>`, so a provisioned partner is never cut to 60. `partner-badkey-ip:<ip>` at 60/min counts only requests that presented a well-formed key and failed to authenticate. `partner:<clientId>:read` runs at your provisioned rate_limit_per_min and is SHARED with /v1/contests/{id} and /v1/tasks.
GET /api/partner/v1/contests/{id}Identical to /v1/contests: pre-auth `partner-read-ip:<ip>` at 60/min (skipped for a well-formed key), `partner-badkey-ip:<ip>` at 60/min on auth failures only, and the SHARED `partner:<clientId>:read` bucket at your provisioned rate_limit_per_min.
GET /api/partner/v1/tasksSame three buckets as the other read routes: pre-auth `partner-read-ip:<ip>` at 60/min (skipped for a well-formed key), `partner-badkey-ip:<ip>` at 60/min on failures, and the SHARED `partner:<clientId>:read` bucket at your provisioned rate_limit_per_min.
POST /api/partner/v1/verifyThree 60-second fixed windows: `partner-verify-ip:<ip>` at 60/min, SKIPPED when the Authorization header carries a well-formed key; `partner-badkey-ip:<ip>` at 60/min counted only on authentication failures; and `partner:<clientId>:verify` at your provisioned rate_limit_per_min. The verify bucket is separate from the shared read bucket, so catalog polling does not eat your verify budget.
POST /api/partner/v1/tasks/customBucket `partner:<client_id>:tasks-custom`, your provisioned per-minute limit. A pre-auth per-IP wall applies to malformed keys.
POST /api/partner/v1/awardBucket `partner:<client_id>:award`, your provisioned per-minute limit.

429s carry a Retry-After header where the limiter can compute one. Honour it. There is no burst allowance to discover by hammering.

Status codes

400Malformed request

Never retry unchanged — fix the request. Retrying wastes your rate limit.

  • missing client_id or return_to·client_id is absent, non-numeric, or <= 0, or return_to is empty.(GET /api/partner/connect)
  • invalid return_to·return_to does not parse as a URL.(GET /api/partner/connect)
  • invalid request·The body was not form data (`multipart/form-data` or `application/x-www-form-urlencoded`).(POST /api/partner/connect)
  • missing client_id or return_to·Same validation as the GET.(POST /api/partner/connect)
  • invalid return_to·return_to does not parse as a URL.(POST /api/partner/connect)
  • bad_params·client_id missing/non-positive, or return_to missing or unparseable.(GET /api/partner/connect/info)
  • bad_json·The request body was not parseable JSON.(POST /api/partner/v1/token)
  • code_required·`code` was absent, empty, or not a string.(POST /api/partner/v1/token)
  • bad_json·The body was not parseable JSON.(POST /api/partner/v1/verify)
  • task_id_and_identity_token_required·Either field is missing, empty, or not a string.(POST /api/partner/v1/verify)
  • invalid_slug·The slug is missing, too short or long, or contains anything outside a-z0-9, dash and underscore.(POST /api/partner/v1/tasks/custom)
  • invalid_label·The label is empty or longer than 120 characters.(POST /api/partner/v1/tasks/custom)
  • invalid_points·Points are missing, non-numeric, zero or negative.(POST /api/partner/v1/tasks/custom)
  • points_too_high·Points exceed the per-task ceiling of 1000.(POST /api/partner/v1/tasks/custom)
  • bad_json·The body was not valid JSON.(POST /api/partner/v1/tasks/custom)
  • slug_contest_id_and_identity_token_required·Any of the three is missing, or contest_id is not a positive integer.(POST /api/partner/v1/award)
  • bad_json·The body was not valid JSON.(POST /api/partner/v1/award)
  • task_disabled·The task exists but you have disabled it.(POST /api/partner/v1/award)
  • award_failed·The write failed and was rolled back. Nothing was recorded; retrying is safe.(POST /api/partner/v1/award)
401Authentication failed

Do not retry. Either the key is missing, malformed, revoked, or the identity token is stale — mint a new one.

  • sign in to grabs.gg first·No valid grabs session cookie on the request.(POST /api/partner/connect)
  • invalid_or_revoked_key·No Bearer header at all, a malformed one, an unknown client id, a wrong secret, or a revoked client — this route cannot tell you which.(POST /api/partner/v1/token)
  • invalid_or_expired_code·The code was tampered with, has expired (~90s), was issued to a different client, or has ALREADY been exchanged. All four collapse to this one answer.(POST /api/partner/v1/token)
  • missing_bearer_key·No `Authorization` header, or one that is not `Bearer <key>`.(GET /api/partner/v1/contests)
  • invalid_or_revoked_key·The key parsed but did not resolve to an active client: unknown client id, wrong secret half, a revoked client — or the partner tables are missing entirely. All four are indistinguishable by design.(GET /api/partner/v1/contests)
  • missing_bearer_key·No `Authorization` header, or one that is not `Bearer <key>`.(GET /api/partner/v1/contests/{id})
  • invalid_or_revoked_key·The key parsed but did not resolve to an active client: unknown client id, wrong secret half, a revoked client — or the partner tables are missing entirely. All four are indistinguishable by design.(GET /api/partner/v1/contests/{id})
  • missing_bearer_key·No `Authorization` header, or one that is not `Bearer <key>`.(GET /api/partner/v1/tasks)
  • invalid_or_revoked_key·The key parsed but did not resolve to an active client: unknown client id, wrong secret half, a revoked client — or the partner tables are missing entirely. All four are indistinguishable by design.(GET /api/partner/v1/tasks)
  • missing_bearer_key·No `Authorization` header, or one that is not `Bearer <key>`.(POST /api/partner/v1/verify)
  • invalid_or_revoked_key·The key parsed but did not resolve to an active client: unknown client id, wrong secret half, a revoked client — or the partner tables are missing entirely. All four are indistinguishable by design.(POST /api/partner/v1/verify)
  • invalid_identity_token·The token expired (>15 min), was tampered with, or was issued to a DIFFERENT client. Indistinguishable on purpose.(POST /api/partner/v1/verify)
  • invalid_or_revoked_key·The key is unknown or revoked.(POST /api/partner/v1/tasks/custom)
  • invalid_identity_token·The token is malformed, expired, or was issued to a different partner.(POST /api/partner/v1/award)
403Not permitted

Do not retry. The credential is valid but not for this.

  • return_to is not allow-listed for this client·The URL parsed, but its origin is not in your allowed_origins. Ask a grabs admin to add it — you cannot.(GET /api/partner/connect)
  • Cross-site request blocked·The POST did not come from grabs.gg itself.(POST /api/partner/connect)
  • return_to is not allow-listed for this client·Origin not in allowed_origins.(POST /api/partner/connect)
  • return_to_not_allowed·The link names a real partner but points at an origin that partner never registered. Deliberately distinct from unknown_client — this is the tampered-link case.(GET /api/partner/connect/info)
  • insufficient_scope·The key is valid but was not provisioned with `read`.(GET /api/partner/v1/contests)
  • insufficient_scope·The key is valid but lacks `read`.(GET /api/partner/v1/contests/{id})
  • insufficient_scope·The key is valid but lacks `read`.(GET /api/partner/v1/tasks)
  • insufficient_scope·The key is valid but lacks `verify`.(POST /api/partner/v1/verify)
  • insufficient_scope·The key lacks the verify scope.(POST /api/partner/v1/tasks/custom)
  • contest_not_yours·That contest is not bound to your account. This is the authorisation boundary — no key can award into a contest it does not own.(POST /api/partner/v1/award)
  • member_not_eligible·The member is banned from contests platform-wide.(POST /api/partner/v1/award)
404Not found

Do not retry. Re-read the catalog; the id may have ended.

  • unknown or revoked client·No active partner_clients row for that client_id.(GET /api/partner/connect)
  • unknown or revoked client·No active client for that id.(POST /api/partner/connect)
  • unknown_client·No active client with that id.(GET /api/partner/connect/info)
  • not_found·No contest with that id — OR the underlying contest query failed. A DB blip degrades to 404 here rather than 500.(GET /api/partner/v1/contests/{id})
  • unknown_task·A status toggle named a slug you have not declared.(POST /api/partner/v1/tasks/custom)
  • unknown_task·You have not declared a task with that slug.(POST /api/partner/v1/award)
409Not available in this context

Do not retry. Typically a task that is not portable inside a contest — read the task's portable flag first.

  • not_portable·The task is not server-verifiable in the derived context. The body also carries `message: "This task type can only be completed in the Telegram bot."`(POST /api/partner/v1/verify)
413Payload too large

Do not retry. Trim the request body.

  • image_too_large·`extra.image_base64` is longer than 8,000,000 characters.(POST /api/partner/v1/verify)
429Rate limited

Retry AFTER the Retry-After header. Polling faster gains you nothing.

  • rate limited — try again shortly·The per-IP connect bucket filled.(GET /api/partner/connect)
  • rate limited — try again shortly·The per-IP connect bucket filled.(POST /api/partner/connect)
  • rate_limited·Per-IP info bucket filled. This response also carries a `Retry-After` header.(GET /api/partner/connect/info)
  • rate_limited·Any of the buckets described under `rateLimit` filled. The body carries `retryAfterSeconds`; only the pre-auth buckets also set a `Retry-After` header.(POST /api/partner/v1/token)
  • rate_limited·Any of the buckets described under `rateLimit` filled. The body carries `retryAfterSeconds`; only the pre-auth buckets also set a `Retry-After` header.(GET /api/partner/v1/contests)
  • rate_limited·Any of the buckets described under `rateLimit` filled. The body carries `retryAfterSeconds`; only the pre-auth buckets also set a `Retry-After` header.(GET /api/partner/v1/contests/{id})
  • rate_limited·Any of the buckets described under `rateLimit` filled. The body carries `retryAfterSeconds`; only the pre-auth buckets also set a `Retry-After` header.(GET /api/partner/v1/tasks)
  • rate_limited·Any of the buckets described under `rateLimit` filled. The body carries `retryAfterSeconds`; only the pre-auth buckets also set a `Retry-After` header.(POST /api/partner/v1/verify)
  • rate_limited·Your per-minute limit is exhausted.(POST /api/partner/v1/tasks/custom)
  • rate_limited·Your per-minute limit is exhausted.(POST /api/partner/v1/award)
500Server error on our side

Safe to retry with backoff.

  • internal_error·An unhandled exception inside the handler. Retry; if it persists it is a grabs-side configuration problem, not a bad request.(POST /api/partner/v1/token)
  • internal_error·An unhandled exception inside the handler. Retry; if it persists it is a grabs-side configuration problem, not a bad request.(GET /api/partner/v1/contests)
  • internal_error·An unhandled exception inside the handler. Retry; if it persists it is a grabs-side configuration problem, not a bad request.(GET /api/partner/v1/contests/{id})
  • internal_error·An unhandled exception inside the handler. Retry; if it persists it is a grabs-side configuration problem, not a bad request.(GET /api/partner/v1/tasks)
  • internal_error·An unhandled exception inside the handler. Retry; if it persists it is a grabs-side configuration problem, not a bad request.(POST /api/partner/v1/verify)