Action Errors

The error codes platform actions return, what each one means, and how a client should react.


Shape

Failures return a structured error, not just an HTTP status:

{
  "error": {
    "code": "limit_reached",
    "message": "daily limit reached for linkedin/comment",
    "deferUntil": "2026-07-28T00:00:00.000Z",
    "reason": "limit_hit"
  }
}

Branch on code. It is stable; message is for humans and may change.

Two optional fields carry the detail a client needs to act without guessing:

  • deferUntil - an ISO timestamp for quota denials (throttled, limit_reached, limit_exhausted). It is when the quota resets, so a client can re-arm at that time instead of polling.
  • reason - the granular cause under the coarse code, such as weekly_limit_hit, signal_cooldown, twin_paused, or relay_offline. Branch on code first; read reason when you want the precise cause.

HTTP status

The status tells you the shape before you parse:

StatusMeaning
200Action completed; body has result
202Control session is warming; body has status and retryAfterMs, no error
400Unknown action or invalid payload
402Insufficient credits
403Cloud twin requires the Premium plan
404Twin not found
423Twin is paused, or blocked by a critical platform signal
429Action throttled

Only 200 and 202 carry a success body. Everything else carries error. Treat 202 as a retry with retryAfterMs, not a failure - see Running Platform Actions.

Codes

CodeMeaningWhat a client should do
throttledPacing kicked in - the action was asked for too soon after the last oneRetry later; respect the cooldown rather than tightening the loop
deferredDeferred by the gate, OR the response was lost after dispatchSafe to retry for reads; for writes, check state first (see below)
limit_reachedA daily cap for this action was hitStop for the day; retrying will not help
limit_exhaustedThe account's write allowance is spentRaise the allowance or wait for the next period
needs_authThe persona's session lapsedHave a person sign in - see Controlling the Browser
sender_blockedThe platform blocked this account from the actionDo not retry; the account needs attention
command_not_allowedThe action is not permitted for this persona or roleCheck the persona's role and account policy
unsupported_channelNo such platform/action pairFix the path; see the Action Reference
not_foundThe persona does not exist, or your key cannot reach itCheck the slug and the key's access
validation_errorA path parameter is missing (persona, platform, or action)Fix the URL

Access denial does not come back as not_found: a persona your key has no operator grant on returns 403 with code forbidden, and so does a persona that does not exist, so the response cannot be used to discover which personas exist on other accounts.

A malformed payload or an invalid via does NOT return validation_error: it returns command_not_allowed with reason: "invalid_payload". Branching on reason is what separates "you sent the wrong fields" from "this persona may not run this action", since both share the code.

Every code above except validation_error belongs to the twin taxonomy (TwinErrorCode). validation_error is the general request-level code, raised before the action is dispatched, so it never carries deferUntil or reason.

Retrying

Retry throttled, limit_reached, and limit_exhausted at deferUntil, not before.

deferred needs more care. It covers two situations: the gate declined to run the action now, and the action was dispatched but its terminal response was lost (reason is action_timeout or control_unavailable). Cancellation on timeout is best effort, so a write may already have completed. Retrying blindly can produce a duplicate post, comment, or message. For reads, retry freely; for writes, read the current state on the platform and confirm the action did not land before sending it again. That timestamp is why the field exists: the gate tells you when the quota resets so a client re-arms once rather than probing.

Do not retry sender_blocked, command_not_allowed, unsupported_channel, not_found, or validation_error. The outcome will not change, and repeatedly hitting a blocked account makes its standing on the platform worse.

If deferUntil is absent, back off rather than retrying immediately. Wonda already paces actions server-side; a client that retries tightly fights that pacing instead of benefiting from it.

Run history

GET /twin/runs lists control and scheduled twin_run records. It is not an index of individual action executions: local relay actions create no twin_run at all, and cloud action correlation IDs are not represented, so it cannot confirm whether a specific timed-out write landed. Use it as an audit trail, not as confirmation:

curl "https://api.wondercat.ai/api/v1/twin/runs?persona=natty&limit=20" \
  -H "Authorization: Bearer $WONDA_API_KEY"

Both query parameters are optional. When a run failed, its diagnostic artifacts are available as presigned download URLs:

curl https://api.wondercat.ai/api/v1/twin/runs/{runId}/artifact \
  -H "Authorization: Bearer $WONDA_API_KEY"

Dry runs

Some writes accept "dryRun": true, intended to run every check - access, allowance, caps, account health - and report what would have happened without touching the platform.

Do not rely on it as a safety net on the hosted path. Several actions accept the field but do not forward it to the underlying command, so the write is performed for real. linkedin/send-message and linkedin/comment both behave this way today; reddit/chat-send honours it. Until that is fixed, treat dryRun as best effort and test against an account you are willing to post from.

The Action Reference lists dryRun? in the payload of the actions that accept the field, which is not the same as the actions that honour it.

Related