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 asweekly_limit_hit,signal_cooldown,twin_paused, orrelay_offline. Branch oncodefirst; readreasonwhen you want the precise cause.
HTTP status
The status tells you the shape before you parse:
| Status | Meaning |
|---|---|
| 200 | Action completed; body has result |
| 202 | Control session is warming; body has status and retryAfterMs, no error |
| 400 | Unknown action or invalid payload |
| 402 | Insufficient credits |
| 403 | Cloud twin requires the Premium plan |
| 404 | Twin not found |
| 423 | Twin is paused, or blocked by a critical platform signal |
| 429 | Action 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
| Code | Meaning | What a client should do |
|---|---|---|
throttled | Pacing kicked in - the action was asked for too soon after the last one | Retry later; respect the cooldown rather than tightening the loop |
deferred | Deferred by the gate, OR the response was lost after dispatch | Safe to retry for reads; for writes, check state first (see below) |
limit_reached | A daily cap for this action was hit | Stop for the day; retrying will not help |
limit_exhausted | The account's write allowance is spent | Raise the allowance or wait for the next period |
needs_auth | The persona's session lapsed | Have a person sign in - see Controlling the Browser |
sender_blocked | The platform blocked this account from the action | Do not retry; the account needs attention |
command_not_allowed | The action is not permitted for this persona or role | Check the persona's role and account policy |
unsupported_channel | No such platform/action pair | Fix the path; see the Action Reference |
not_found | The persona does not exist, or your key cannot reach it | Check the slug and the key's access |
validation_error | A 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
- Running Platform Actions
- Action Reference
- Error Codes - the general API error taxonomy