Skip to content

Errors

Every error the Caramel API can return, with the cause and the action to take. The tool reference pages in Tools list per-tool errors — this page is the complete union.

The REST gateway returns a standard JSON body on all errors:

{
"error": "rate_limited",
"message": "Per-token rate cap exceeded.",
"status": 429
}

The MCP endpoint wraps tool-level errors in the MCP result envelope:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": "Per-token rate cap exceeded." }],
"isError": true
}
}

Protocol-level failures (expired token, malformed JSON-RPC) return standard JSON-RPC 2.0 errors:

{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32001, "message": "Unauthorized — access token expired or invalid" }
}

-32001 is a Caramel extension meaning “access token expired; refresh and retry.” Standard JSON-RPC codes: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error.

Important Some responses occasionally return messsage (three s’s) instead of message. This is a known quirk in one upstream dependency. Normalize both keys in every error-handling path — failing to do so causes silent “200-shaped UI but no content” failures.

function asError(body) {
return {
code: body.error ?? body.code,
message: body.message ?? body.messsage ?? 'Unknown error',
status: body.status,
};
}

Two lines, one helper, applied everywhere. The TypeScript SDK already does this internally.

The call succeeded. Response body shape depends on the tool — see Tools.

The request was malformed or a required field is missing.

CodeCauseFix
email_requiredemail argument missing or empty on caramel.v1.contact.upsertSend a valid email address. Email is the unique contact key — there is no phone-only path.
invalid_requestOAuth token endpoint received a malformed bodyCheck grant_type, code, client_id.
invalid_grantOAuth code is wrong, expired, used, or refresh token was rotatedRestart the authorize flow.
invalid_clientOAuth client_id doesn’t exist or is disabledVerify the client ID; contact support if it should exist.
malformed_requestGeneric validation failure — missing JSON body, wrong Content-TypeSend Content-Type: application/json and a valid body.

Authentication failed. The bearer token is missing, expired, or invalid.

CodeCauseFix
unauthorizedAccess token expired or invalidRefresh the token once and retry. If still 401, restart the OAuth flow.
token_missingNo Authorization headerAdd Authorization: Bearer <access_token>.
token_revokedUser revoked the connectionRestart the OAuth flow — the user must re-grant access.

A 401 during a refresh exchange returns invalid_grant instead. See Authentication.

The call requires AI credits that the business doesn’t have.

CodeCauseFix
insufficient_creditsNo AI credits remaining for this billing cycleAdd credits or upgrade tier. Check the current balance with caramel.v1.meta.usage.

Affects generate_campaign and refine_campaign only.

The token is valid but the caller isn’t authorized for this operation.

CodeCauseFix
forbiddenCaller is not a member of the target businessVerify business_id is one you have access to via list_businesses.
tier_requiredCaller is below the tool’s minimum tierUpgrade the business to the required tier. See Tiers and scopes.
scope_requiredToken doesn’t have the OAuth scope the tool needsRestart the authorize flow with the additional scope.

The resource doesn’t exist, or you don’t have access to it.

CodeCauseFix
campaign_not_foundcampaign_id doesn’t exist or you don’t have accessConfirm the ID via list_campaigns.
form_not_foundform_id doesn’t exist or the form is inactiveConfirm the ID via caramel.v1.form.list. Inactive forms can’t receive submissions.
business_not_foundbusiness_id doesn’t exist or you’re not a memberConfirm via list_businesses.

Note 404 deliberately doesn’t distinguish “doesn’t exist” from “exists but you can’t see it.” This prevents resource ID enumeration.

The resource is in a state that doesn’t allow the requested operation.

CodeCauseFix
deployed_campaignTried to delete_campaign on a deployed campaignPause it first with pause_campaign, then delete.
not_approvedTried to deploy_campaign on a non-approved campaignRefine until the quality score clears, then deploy.
not_runningTried to pause_campaign on a non-running campaignOnly deployed campaigns can be paused.
not_pausedTried to resume_campaign on a non-paused campaignOnly paused campaigns can be resumed.

Campaign state transitions are summarized in Tools — campaign state machine.

The request was syntactically valid but failed business-rule validation.

CodeCauseFix
validationRequired form fields missing or invalid on caramel.v1.form.submitInspect the details array — each entry has field and reason.

Example 422 body:

{
"error": "validation",
"message": "One or more required fields missing or invalid.",
"status": 422,
"details": [
{ "field": "email", "reason": "format" },
{ "field": "first_name", "reason": "required" }
]
}

A rate limit was exceeded. Always honor the Retry-After header.

CodeCauseFix
rate_limitedPer-token, per-IP, or per-host gateway limit hitWait Retry-After seconds, then retry with jitter.
submission_capPer-tier monthly form-submission cap reachedUpgrade tier, or wait until next billing cycle.
credit_throttleAI generation calls arriving faster than the credit ledger can settleSerialize generation requests; add 250 ms between calls.

Example 429 body:

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{ "error": "rate_limited", "message": "Per-token rate cap exceeded.", "status": 429 }

See Rate limits for the full cap matrix.

Something went wrong on Caramel’s side. Safe to retry with exponential backoff.

CodeCauseFix
internal_errorUnexpected server failureRetry with exponential backoff (1 s, 2 s, 4 s). Alert and stop after 3 failures.
dependency_unavailableAn internal dependency is degradedSame as internal_error. Check https://status.caramelme.com.

Include the x-caramel-request-id from the response header in any support request — it correlates directly with server logs.

The gateway couldn’t reach the backing service. Treat as transient.

  • Retry with exponential backoff.
  • Retry-After is set when the gateway knows when it will recover.
  • Persistent failures in a region: check https://status.caramelme.com.
StatusRetry?Strategy
400, 401, 402, 403, 404, 409, 422NoFix the request, token, or business state first.
429YesWait Retry-After seconds, then retry with jitter.
500YesExponential backoff: 1 s, 2 s, 4 s, 8 s. Cap at 3 retries.
502, 503, 504YesSame as 500. Check the status page after 2 consecutive failures.

Reference implementation:

async function callWithRetry(fn, opts = {}) {
const maxRetries = opts.maxRetries ?? 3;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fn();
if (res.ok) return res;
if (res.status === 429) {
const wait = (Number(res.headers.get('retry-after')) || 1) * 1000;
await sleep(wait + Math.random() * 500);
continue;
}
if (res.status >= 500 && res.status < 600 && attempt < maxRetries) {
await sleep(2 ** attempt * 1000 + Math.random() * 500);
continue;
}
throw await res.json();
}
}
import time, random
def call_with_retry(fn, max_retries=3):
for attempt in range(max_retries + 1):
res = fn()
if res.is_success:
return res
if res.status_code == 429:
wait = int(res.headers.get('retry-after', '1'))
time.sleep(wait + random.random() * 0.5)
continue
if 500 <= res.status_code < 600 and attempt < max_retries:
time.sleep(2 ** attempt + random.random() * 0.5)
continue
res.raise_for_status()
raise RuntimeError('exceeded retries')

The API does not currently accept an Idempotency-Key header. Tools that mutate state have different retry semantics:

ToolSafe to retry?Why
caramel.v1.contact.upsertYesEmail is the unique key. Repeated calls with the same payload are idempotent by design.
deploy_campaign, pause_campaign, resume_campaignEffectively yesThe state machine returns 409 if the operation is already complete — treat as a no-op.
delete_campaignEffectively yesReturns 404 campaign_not_found if already deleted.
caramel.v1.form.submitNoEvery call creates a new submission row.
generate_campaign, refine_campaignNoEvery call consumes AI credits. Use the campaign ID from the first response; don’t re-call on UI retries.
deploy_templateNoRepeated calls create duplicate workspace entities.

An Idempotency-Key header is planned. Until it ships, use the table above as the contract.

  1. Capture the x-caramel-request-id response header and the request_id in the response body.
  2. Inspect the details array if present — validation errors put per-field reasons there.
  3. Email aymen@reactmotion.com with the request ID and timestamp. Logs are retained for 30 days.