Getting Started

API Error Codes & Troubleshooting

Full error code reference for the TokPortal API. Error response format, HTTP status codes, and troubleshooting guide.

Errors

The TokPortal API uses a consistent error format across all endpoints. Errors are returned as JSON with an error object.

Error Response Format

{
  "error": {
    "code": "ERROR_CODE",
    "message": "A human-readable description of the problem.",
    "details": {}
  }
}
FieldTypeDescription
codestringMachine-readable error code. Use this for programmatic handling.
messagestringHuman-readable description. May change — do not match against this.
detailsobjectOptional. 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.

Error Code Reference

Authentication Errors

CodeHTTPDescription
AUTH_MISSING_KEY401No X-API-Key header was provided in the request.
AUTH_INVALID_KEY401The API key does not match any account.
AUTH_REVOKED_KEY401The API key has been revoked. Generate a new key from the developer dashboard.
AUTH_EXPIRED_KEY401The API key expired. Rotate it in the developer dashboard.
AUTH_FORBIDDEN_SCOPE403The key does not have the scope required by this endpoint.

Rate Limiting

CodeHTTPDescription
rate_limited429Current per-key limiter code. Wait for Retry-After, then retry. See Rate Limits.
RATE_LIMIT_EXCEEDED429Legacy rate-limit code. Handle it like rate_limited.
capacity_cooldown429The 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.

Idempotency

CodeHTTPDescription and safe recovery
IDEMPOTENCY_KEY_TOO_LONG400Use a stable key of at most 255 characters.
IDEMPOTENCY_KEY_NOT_ALLOWED_FOR_SENSITIVE_RESPONSE400This 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_REUSED409The 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_PROGRESS409The 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_UNAVAILABLE503TokPortal 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.

Billing

CodeHTTPDescription
INSUFFICIENT_CREDITS402Not enough credits. Response includes required, available, missing, and breakdown in details.

Bundle Errors

CodeHTTPDescription
BUNDLE_NOT_FOUND404The specified bundle does not exist.
BUNDLE_NOT_OWNED403The bundle belongs to another account.
BUNDLE_INVALID_STATUS409The bundle is not in a valid status for this operation (e.g., trying to configure a published bundle).
BUNDLE_ALREADY_PUBLISHED409The bundle has already been published and cannot be modified.
DUPLICATE_ACCOUNT_BUNDLE409A 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.

Account Errors

CodeHTTPDescription
ACCOUNT_NOT_FOUND404The specified account does not exist.
ACCOUNT_INVALID_STATUS409The account is not in a valid status for this operation.
ACCOUNT_NOT_CONFIGURED409The account must be configured before this operation can proceed.

Saved Account Errors

CodeHTTPDescription
SAVED_ACCOUNT_NOT_FOUND404The specified saved account does not exist.
SAVED_ACCOUNT_NOT_OWNED403The saved account belongs to another user.
ACCOUNT_CREDENTIALS_NOT_AVAILABLE409The account has no complete credential payload. Nothing was charged or detached. Do not accept a partial response.
MANAGED_ACCOUNT_TASK_BLOCKED409TokPortal Coverage does not currently allow work on this account. Read details.reason, fetch the Coverage state, and reactivate if the state is recoverable.

TokPortal Coverage

CodeHTTPDescription and next action
MANAGED_SUBSCRIPTION_NOT_FOUND404No 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_PAUSED409Coverage is not in a recoverable paused state. Fetch the current state and do not submit payment again.
MANAGED_SUBSCRIPTION_NOT_CANCELLABLE409Coverage cannot be paused in its current state, usually because it is already terminal. Fetch the current state.
MANAGED_ACCOUNT_UNAVAILABLE409The saved account is no longer available, so Coverage cannot be reactivated. Do not retry payment.
QUOTE_CHANGED409No debit or resume occurred. Fetch a new Coverage snapshot, show the new amount, and confirm it with a new idempotency key.
INSUFFICIENT_CREDITS402The wallet cannot cover the current quote. Show details.required, details.available, and details.missing; after top-up, fetch a fresh quote before retrying.

Credential Reveal and Account Owning Fee

CodeHTTPDescription and next action
CREDENTIALS_ACKNOWLEDGMENT_REQUIRED428No 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_CHANGED409No 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_AVAILABLE409No usable credentials are stored. No credits were charged and the account was not detached.
OWNING_FEE_REQUIRED402The 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_SUSPENDED402Account Owning Fee billing is suspended. Resolve billing before retrying an owning-fee-gated action.

Video Errors

CodeHTTPDescription
VIDEO_NOT_FOUND404The specified video does not exist.
VIDEO_INVALID_STATUS409The video is not in a valid status for this operation.
VIDEO_POSITION_OUT_OF_RANGE400The video position exceeds the available slots.
VIDEO_NO_DOWNLOAD_ISSUE409The video does not have a download issue to fix.

Account Edit Request Errors

CodeHTTPDescription
EDIT_REQUEST_NO_CM409The active order has no eligible active manager.
EDIT_REQUEST_ALREADY_EXISTS409Another edit request is already in progress.
EDIT_REQUEST_NO_ACTIVE_ORDER409The latest delivery is older than 14 days, or no active historical manager can be resolved.

Validation Errors

CodeHTTPDescription
INVALID_COUNTRY400The country code is not supported.
INVALID_PLATFORM400The 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_DELAYED400Bundle 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_TYPE400The video type is not valid for the target platform.
INVALID_DATE400The 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_EXCEEDED400The bundle already has 3 videos scheduled on that day (max 3 per day per bundle).
INVALID_FIELD400A field value is invalid (details specify which field and why).
INVALID_BODY400The request body is malformed or not valid JSON.
MISSING_FIELD400A required field is missing from the request body.
UNKNOWN_FIELD400The body carries a field TokPortal does not recognise, on an endpoint where that cannot be tolerated. See Unknown fields below.

Configuration Errors

CodeHTTPDescription
EDIT_SLOTS_EXCEEDED400The number of edit slots exceeds the allowed limit.
WARMING_CONFLICT400Conflicting warming options: wants_advanced_warming and wants_niche_warming are mutually exclusive.
DEEP_WARMING_PLATFORM400Legacy code (deep warming was Instagram-only). New deep-warming orders now fail earlier with DEEP_WARMING_DEPRECATED.
DEEP_WARMING_DEPRECATED400Deep warming has been discontinued. Use wants_advanced_warming with advanced_warming_terms instead.
ADVANCED_WARMING_PLATFORM400Advanced warming is only available for TikTok and Instagram accounts.
ADVANCED_WARMING_TERMS400advanced_warming_terms must contain 3-30 terms (multiple of 3), each 2-50 characters.
ADVANCED_WARMING_FLAG_REQUIRED400advanced_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_REJECTED400At 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_MISMATCH400advanced_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_ORDER409No routable active manager or non-cancelled support order is available. Completed bundles remain eligible; cancelled orders do not.
REWARM_ALREADY_ACTIVE409An advanced warming session is already active for this account.
WARMING_SESSION_NOT_FOUND404Warming session not found or not owned by your account.
TERM_GENERATION_FAILED502Search-term generation failed — retry with a more detailed description or provide terms manually.
AI_UNAVAILABLE503AI term generation is temporarily unavailable — provide the search terms manually.
VIDEOS_ONLY_REQUIRES_ACCOUNT400A videos-only bundle requires an existing saved account.
ACCOUNT_ID_NOT_ALLOWED400The 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).

Analytics Errors

CodeHTTPDescription
ANALYTICS_COOLDOWN429Analytics were recently refreshed. Wait before requesting again.
ANALYTICS_QUOTA_EXCEEDED429Analytics refresh quota has been exceeded for this period.
ANALYTICS_NOT_FOUND404No analytics data is available for this account.

Verification Errors

CodeHTTPDescription
VERIFICATION_CODE_NOT_FOUND404No pending verification code was found for this account.

CSV Errors

CodeHTTPDescription
CSV_PARSE_ERROR400The CSV file could not be parsed. Check formatting and encoding.
CSV_VALIDATION_ERROR400The CSV content failed validation. details includes row-level errors.

Server Errors

CodeHTTPDescription
UPLOAD_FAILED500A file upload failed. Retry the request.
INTERNAL_ERROR500An unexpected server error occurred. If this persists, contact support.

Unknown fields

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:

EndpointBehaviour
POST /bundlesRefused — 400 UNKNOWN_FIELD
POST /bundles/bulkRefused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/accountRefused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/videos/{position}Refused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/videos/batchRefused — 400 UNKNOWN_FIELD
PUT /bundles/{id}/warming-termsRefused — 400 UNKNOWN_FIELD
Every other endpointRuns 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.

When the field is real but sent to the wrong place

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 message is 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 sentWhereWhat you get
target_publish_end_dateany 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_dateany video configuration bodyRefused: on those endpoints the field is called target_publish_date, and it already is the window's start.
auto_publishinside videos[] on PUT .../videos/batchRefused: the flag is top-level on that endpoint and covers every video in the call.
target_publish_end_datea CSV import column headerRefused 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.

Handling Errors in Code

const response = await fetch("https://app.tokportal.com/api/ext/bundles", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.TOKPORTAL_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(bundleConfig),
});

const result = await response.json();

if (result.error) {
  switch (result.error.code) {
    case "INSUFFICIENT_CREDITS":
      console.error(
        `Need ${result.error.details.missing} more credits.`
      );
      break;
    case "AUTH_INVALID_KEY":
      console.error("Invalid API key. Check your configuration.");
      break;
    case "rate_limited":
    case "RATE_LIMIT_EXCEEDED": // Legacy code
      // Wait and retry
      await new Promise((r) => setTimeout(r, 5000));
      break;
    default:
      console.error(`API error: ${result.error.message}`);
  }
}