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.