{
"error": {
"code": "ERROR_CODE",
"message": "A human-readable description of the problem.",
"details": {}
}
}
Field
Type
Description
code
string
Machine-readable error code. Use this for programmatic handling.
message
string
Human-readable description. May change — do not match against this.
details
object
Optional. Additional context (e.g., which field failed validation, credit breakdown).
Every response also includes a correlation header:
X-TokPortal-Request-ID: req_...
Generated SDKs expose this value on structured API errors as requestId, request_id, or RequestID. Include it when contacting support about a failed request.
Current per-key limiter code. Wait for Retry-After, then retry. See Rate Limits.
RATE_LIMIT_EXCEEDED
429
Legacy rate-limit code. Handle it like rate_limited.
capacity_cooldown
429
The rolling bundle capacity for the client workspace is temporarily exhausted. Team activity shares the same window; retry later.
API-key throttling responses include Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Bundle-capacity cooldowns do not expose capacity counters.
This endpoint can return a credential, verification code, signing secret, signed upload URL, or private report artifact. Remove Idempotency-Key and retry once without it. details.retry_without_idempotency_key is true. TokPortal rejects the request before the operation runs.
IDEMPOTENCY_KEY_REUSED
409
The key already belongs to a different request fingerprint. Do not retry with that key. Generate a new key only for a genuinely new logical action.
IDEMPOTENCY_KEY_IN_PROGRESS
409
The claim is still processing. Retry the exact same method, path, body, and key after a short delay. If it persists beyond normal request time, stop automated retries and do not use a new key: the mutation may have committed before its response was stored. Contact support for log and ledger reconciliation.
IDEMPOTENCY_UNAVAILABLE
503
TokPortal did not execute the mutation because the ledger was unavailable. Retry the exact same request with the same key.
For a network timeout where no response was received, also retry the exact same request with the same key. Reusing a key with a changed body is always an integration error. Completed responses replay for 24 hours after completion. Uncertain processing claims never expire automatically into a second execution; they remain fail-closed until TokPortal support/operator reconciles the request, resource, and financial logs. Include the X-TokPortal-Request-ID, request timestamp, method, path, idempotency key, and a non-secret correlation reference when asking for reconciliation.
The bundle is not in a valid status for this operation (e.g., trying to configure a published bundle).
BUNDLE_ALREADY_PUBLISHED
409
The bundle has already been published and cannot be modified.
DUPLICATE_ACCOUNT_BUNDLE
409
A new-account bundle matched an existing external_ref or the short no-reference duplicate guard. Inspect details.existing_bundle_id. This detects a likely duplicate but does not replay the prior response; only Idempotency-Key provides retry/replay.
The account has no complete credential payload. Nothing was charged or detached. Do not accept a partial response.
MANAGED_ACCOUNT_TASK_BLOCKED
409
TokPortal Coverage does not currently allow work on this account. Read details.reason, fetch the Coverage state, and reactivate if the state is recoverable.
No Coverage record is available for this account. This alone does not prove grandfathering; also check platform, account creation time, and delivery state.
MANAGED_SUBSCRIPTION_NOT_PAUSED
409
Coverage is not in a recoverable paused state. Fetch the current state and do not submit payment again.
MANAGED_SUBSCRIPTION_NOT_CANCELLABLE
409
Coverage cannot be paused in its current state, usually because it is already terminal. Fetch the current state.
MANAGED_ACCOUNT_UNAVAILABLE
409
The saved account is no longer available, so Coverage cannot be reactivated. Do not retry payment.
QUOTE_CHANGED
409
No debit or resume occurred. Fetch a new Coverage snapshot, show the new amount, and confirm it with a new idempotency key.
INSUFFICIENT_CREDITS
402
The wallet cannot cover the current quote. Show details.required, details.available, and details.missing; after top-up, fetch a fresh quote before retrying.
No reveal, debit, or detach occurred. Display the complete details.disclosure and details.acknowledgment, then retry only after explicit confirmation with acknowledge_support_forfeit: true and the exact details.policy_version.
CREDENTIAL_REVEAL_QUOTE_CHANGED
409
No reveal, debit, or detach occurred. The caller supplied a nonempty stale policy version, or the price or policy changed during the atomic operation. Fetch the current preview, display it again, and request fresh confirmation before retrying.
ACCOUNT_CREDENTIALS_NOT_AVAILABLE
409
No usable credentials are stored. No credits were charged and the account was not detached.
OWNING_FEE_REQUIRED
402
The admin-approved Account Owning Fee agreement is not ready or its billing setup is incomplete. Follow details.activation_url when present. Pending approval does not remove the 150-credit reveal option.
BILLING_SUSPENDED
402
Account Owning Fee billing is suspended. Resolve billing before retrying an owning-fee-gated action.
The platform string is not one the API recognises. It accepts tiktok, instagram and youtube, but recognising youtube is not the same as being able to order it — see YOUTUBE_DELAYED below.
YOUTUBE_DELAYED
400
Bundle creation supports TikTok and Instagram only. GET /credit-costs lists a YouTube price and GET /platforms lists YouTube, but a YouTube bundle is rejected here; YouTube accounts are delivered through a separate flow arranged with the team.
INVALID_VIDEO_TYPE
400
The video type is not valid for the target platform.
INVALID_DATE
400
The date is malformed, or earlier than the minimum publish lead time. details.min_days_ahead carries the lead time applied (1 or 3) and details.earliest_allowed the first acceptable day (YYYY-MM-DD). Raised by video configuration, batch, CSV import and — since 2026-08-26 — PATCH /bundles/:id/videos/:position reschedules.
VIDEOS_PER_DAY_EXCEEDED
400
The bundle already has 3 videos scheduled on that day (max 3 per day per bundle).
INVALID_FIELD
400
A field value is invalid (details specify which field and why).
INVALID_BODY
400
The request body is malformed or not valid JSON.
MISSING_FIELD
400
A required field is missing from the request body.
UNKNOWN_FIELD
400
The body carries a field TokPortal does not recognise, on an endpoint where that cannot be tolerated. See Unknown fields below.
The number of edit slots exceeds the allowed limit.
WARMING_CONFLICT
400
Conflicting warming options: wants_advanced_warming and wants_niche_warming are mutually exclusive.
DEEP_WARMING_PLATFORM
400
Legacy code (deep warming was Instagram-only). New deep-warming orders now fail earlier with DEEP_WARMING_DEPRECATED.
DEEP_WARMING_DEPRECATED
400
Deep warming has been discontinued. Use wants_advanced_warming with advanced_warming_terms instead.
ADVANCED_WARMING_PLATFORM
400
Advanced warming is only available for TikTok and Instagram accounts.
ADVANCED_WARMING_TERMS
400
advanced_warming_terms must contain 3-30 terms (multiple of 3), each 2-50 characters.
ADVANCED_WARMING_FLAG_REQUIRED
400
advanced_warming_terms or advanced_warming_terms_count was sent without wants_advanced_warming: true — absent, or explicitly false. TokPortal never infers the flag, so the call is refused instead of charging for the bundle and dropping the targets. Nothing is debited. On POST /bundles/bulk it rejects the whole batch.
ADVANCED_WARMING_TERMS_REJECTED
400
At least one target would have been removed by cleanup — trimmed then compared case-insensitively, so a duplicate, or a term outside 2-50 characters. details.dropped_terms lists them. TokPortal will not deliver, or bill, fewer targets than you asked for, so the call fails rather than silently shrinking the order. Send distinct terms and retry.
ADVANCED_WARMING_COUNT_MISMATCH
400
advanced_warming_terms and advanced_warming_terms_count state different quantities. Send the term list or the count; neither quantity silently wins any more.
REWARM_NO_ACTIVE_ORDER
409
No routable active manager or non-cancelled support order is available. Completed bundles remain eligible; cancelled orders do not.
REWARM_ALREADY_ACTIVE
409
An advanced warming session is already active for this account.
WARMING_SESSION_NOT_FOUND
404
Warming session not found or not owned by your account.
TERM_GENERATION_FAILED
502
Search-term generation failed — retry with a more detailed description or provide terms manually.
AI_UNAVAILABLE
503
AI term generation is temporarily unavailable — provide the search terms manually.
VIDEOS_ONLY_REQUIRES_ACCOUNT
400
A videos-only bundle requires an existing saved account.
ACCOUNT_ID_NOT_ALLOWED
400
The account_id field was passed with a bundle_type other than videos_only. To add videos to an existing saved account, set bundle_type to videos_only (this also skips the account-creation credit charge).
A field TokPortal does not recognise used to be dropped during validation and the request succeeded anyway. On a full-replace write that was quietly destructive: a body sending biograhpy instead of biography was accepted with a 200, and because the endpoint replaces the whole record, the stored biography was wiped to null.
Unrecognised fields are now reported, and on the endpoints where dropping them destroys data they are refused:
Endpoint
Behaviour
POST /bundles
Refused — 400 UNKNOWN_FIELD
POST /bundles/bulk
Refused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/account
Refused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/videos/{position}
Refused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/videos/batch
Refused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/warming-terms
Refused — 400 UNKNOWN_FIELD
Every other endpoint
Runs normally; the names come back in meta.ignored_fields
The error names the fields, suggests the closest valid one, and lists what the endpoint does accept:
{
"error": {
"code": "UNKNOWN_FIELD",
"message": "The request body contains fields TokPortal does not recognise. On this endpoint a write is a FULL REPLACE, so a misspelled field is not merely ignored — it erases the value it was meant to set. Check \"details.unknown_fields\" for the exact names and \"details.did_you_mean\" for the closest valid field. Endpoints that only ignore unknown fields report them in \"meta.ignored_fields\" instead of failing.",
"details": {
"unknown_fields": ["biograhpy"],
"did_you_mean": { "biograhpy": "biography" },
"accepted_fields": ["username", "display_name", "biography", "profile_picture_url", "link_in_bio"]
}
}
}
The check descends into arrays, so a typo inside one item of a batch body is caught too.
A spelling guess is the wrong answer for a field that exists, is spelled correctly, and is simply not settable on that endpoint. Those cases answer 400 UNKNOWN_FIELD with details.hints, and the messageis the fix:
{
"error": {
"code": "UNKNOWN_FIELD",
"message": "auto_publish is a top-level field on this endpoint and applies to every video in the call — move it out of videos[]. It cannot be set per item: there is one publish attempt per call, not one per video.",
"details": {
"unknown_fields": ["videos[0].auto_publish"],
"hints": {
"videos[0].auto_publish": "auto_publish is a top-level field on this endpoint and applies to every video in the call — move it out of videos[]. It cannot be set per item: there is one publish attempt per call, not one per video."
},
"accepted_fields": ["videos", "auto_publish"]
}
}
}
Field sent
Where
What you get
target_publish_end_date
any video configuration body (PUT .../videos/:position, PUT .../videos/batch)
Refused: the window's end is derived as target_publish_date + 1 day and cannot be chosen. Use PATCH /bundles/:id/videos/:position for an explicit window.
target_publish_start_date
any video configuration body
Refused: on those endpoints the field is called target_publish_date, and it already is the window's start.
auto_publish
inside videos[] on PUT .../videos/batch
Refused: the flag is top-level on that endpoint and covers every video in the call.
target_publish_end_date
a CSV import column header
Refused with CSV_PARSE_ERROR for the same reason.
WARNING: These fire for every caller, not only MCP
Unlike the did-you-mean refusal below, the hints refusals ignore API_STRICT_UNKNOWN_FIELDS. Both fields used to be dropped in silence, so the call was answered 200 and the thing it asked for never happened — a publishing window nobody chose, a batch that was never published. A 400 naming the fix is strictly better than that. Two production REST integrations were sending target_publish_end_date on configure when this shipped.
INFO: Who gets the 400 today
The refusal is gated by the API_STRICT_UNKNOWN_FIELDS server setting, currently mcp:
MCP callers get the 400 UNKNOWN_FIELD.
REST callers run as before and get the names back in meta.ignored_fields on the response.
The gate exists because request bodies are not stored, so there is no way to know in advance which live integrations would start failing. meta.ignored_fields is the telemetry that will decide when the refusal is extended to every caller — treat anything appearing there as a bug in your integration, not as a supported way to send extra data.
Two related cases are reported rather than refused:
A link_in_bio on a non-Instagram bundle is accepted, stored as null, and named in the response's _warnings array.
Platform-inapplicable video fields (TikTok-only fields on an Instagram slot, and vice versa) come back as ignored_fields.
Read meta.ignored_fields and _warnings, not just the status code.