POST /v1/client-tokens

Issue a short-lived client token

Issues a client token: a short-lived credential that can call POST /v1/scans and nothing else. Call it from your server with your API key and hand the token to a browser or an app, which then sends the scan straight to this API without ever holding the key. A token lives 300 seconds and allows 1 scan request unless the body asks otherwise; send {} to take the defaults. Its scans are billed and stored exactly as the issuing key's would be, a sandbox key issues sandbox tokens, and revoking or deleting the key ends every token it issued. The token is returned once and only its hash is kept.

Call it from your own server with your API key, then hand the token to the app or the browser. The app sends POST /v1/scans with Authorization: Bearer <token>, and the secret key never leaves your server.

A token lives 300 seconds unless expires_in asks for 10 to 900, and allows one scan request unless max_uses asks for up to 10. Every request presented with the token spends a use, whatever its outcome.

A token can call POST /v1/scans and nothing else. Its scans are billed, stored and rate-limited exactly as the issuing key's, a sandbox key issues sandbox tokens, and revoking or deleting the key ends every token it issued.

The token is returned once. Only its hash is kept, so it cannot be read back.

Request

Method POST
Path /v1/client-tokens
Authentication Authorization: Bearer <api key>

Body fields

Field Type Required Default Description
expires_in integer no – Seconds until the token stops working: 10 to 900, 300 when omitted.
max_uses integer no – How many scan requests the token may make: 1 to 10, 1 when omitted. Every request presented with the token uses one, whatever its outcome.

Example request body

{
  "expires_in": 300,
  "max_uses": 1
}

Responses

201

The new client token.

Body: ClientToken.

Field Type Description
token string The client token. Send it as Authorization: Bearer <token> to POST /v1/scans. It is shown once and cannot be read back.
expires_at string When the token stops working (UTC).
max_uses integer How many scan requests the token may make.
sandbox boolean True when the token was issued with a sandbox key: its scans run in the sandbox, exactly as the key's would.
{
  "token": "ct_live_4f1d0c9a7b2e8f36a5c4d1e0b9a8f7e6d5c4b3a2918f7e6d5c4b3a2918f7e6d5",
  "expires_at": "2026-09-17T09:46:12Z",
  "max_uses": 1,
  "sandbox": false
}

400

The request is malformed: the body is not JSON or a required part is missing.

Body: ErrorResponse.

{
  "error": {
    "code": "invalid_request",
    "message": "Body is not valid JSON.",
    "docs_url": "https://doc.cheap/docs/errors/invalid_request",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

401

Missing or malformed Authorization: Bearer <api key> header, or an unknown key.

Body: ErrorResponse.

{
  "error": {
    "code": "unauthorized",
    "message": "Send your API key as `Authorization: Bearer <key>`.",
    "docs_url": "https://doc.cheap/docs/errors/unauthorized",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

403

The credential cannot call this operation: a client token can only call POST /v1/scans.

Body: ErrorResponse.

{
  "error": {
    "code": "client_token_not_allowed",
    "message": "A client token can only call POST /v1/scans.",
    "docs_url": "https://doc.cheap/docs/errors/client_token_not_allowed",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

415

The request body must be sent as Content-Type: application/json, and a scan's image must be a JPEG or PNG.

Body: ErrorResponse.

{
  "error": {
    "code": "unsupported_media_type",
    "message": "Send the request body as `Content-Type: application/json`.",
    "docs_url": "https://doc.cheap/docs/errors/unsupported_media_type",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

422

The body is well-formed JSON but does not satisfy the request schema.

Body: ErrorResponse.

{
  "error": {
    "code": "validation_failed",
    "message": "/options/mode: Invalid option: expected one of \"full\"",
    "docs_url": "https://doc.cheap/docs/errors/validation_failed",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

429

Rate limit exceeded for this key or IP.

Body: ErrorResponse.

{
  "error": {
    "code": "rate_limited",
    "message": "Sandbox limit of 10 requests per hour per IP reached.",
    "docs_url": "https://doc.cheap/docs/errors/rate_limited",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

500

Unexpected failure on the server; the scan was not charged.

Body: ErrorResponse.

{
  "error": {
    "code": "internal_error",
    "message": "Unexpected error.",
    "docs_url": "https://doc.cheap/docs/errors/internal_error",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

503

Temporarily unable to serve the request: a store this service depends on is unreachable. Nothing was charged and it may be retried, after the Retry-After seconds where that header is present.

Body: ErrorResponse.

{
  "error": {
    "code": "service_unavailable",
    "message": "The service is temporarily unable to handle this request; nothing was charged. Retry shortly.",
    "docs_url": "https://doc.cheap/docs/errors/service_unavailable",
    "request_id": "req_9e6b1f7c-2d4a-4b83-9c51-7f0ad3e8b642",
    "event_id": null
  }
}

Error codes reachable here

Code When Page
client_token_not_allowed The request was authenticated with something other than an API key – a client token, or a dashboard login. client_token_not_allowed
internal_error The service failed in a way it does not model. No token was issued. internal_error
invalid_request The body is missing or not readable as JSON. Send {} to take the defaults. invalid_request
rate_limited The caller is over the rate limit for its key kind. rate_limited
service_unavailable The store tokens are kept in is unreachable, so no token was issued; retry after the Retry-After seconds. service_unavailable
unauthorized The Authorization header is missing, malformed or names an unknown key. unauthorized
unsupported_media_type The body was sent as something other than application/json. unsupported_media_type
validation_failed expires_in is not a whole number from 10 to 900, max_uses is not a whole number from 1 to 10, or the body has another field. validation_failed

Every error body carries the same shape; the full list is on Errors.