{"service":"402cron","audience":"autonomous agents","guarantee":"We guarantee that we tried to deliver your task. We never pretend we can guarantee what someone else's server does with it.","mcp":{"url":"https://402cron.com/mcp","transport":"streamable-http","auth":"Send `Authorization: Bearer <management token>` as a header on the MCP connection. The token is never a tool argument. No OAuth, no account.","unauthenticatedTools":["get_pricing","get_docs","get_service_status"],"claudeCode":"claude mcp add --transport http 402cron https://402cron.com/mcp --header \"Authorization: Bearer <token>\"","note":"Every management tool is one of the REST routes below, called with your token. There is no purchase tool: pay over plain HTTP, because a signed payment must not travel through a model context."},"quickstart":["GET  https://402cron.com/buy/trial → 402 with payment requirements; pay with any x402 client. The response carries your management token, shown ONCE — send it as \"Authorization: Bearer <token>\" from here on. Lost it? Buy again from the same wallet with ?rotate=1 and the old one dies.","POST https://402cron.com/api/destinations  {\"url\":\"https://your.host/hook\"} → returns a signing secret, shown once. The URL must be https — an http:// destination is refused right here.","Answer our challenge, then verify: Verification is one request from us to the exact URL you registered: POST, headers X-402cron-Challenge: <value> and X-402cron-Event: verify, JSON body {\"challenge\":\"<value>\"}. Reply within 5 seconds, without a redirect, with a body that is the bare value or {\"challenge\":\"<value>\"}; we read the first 512 bytes and do not check the HTTP status (2xx is fine). Then call POST /api/destinations/{id}/verify within 15 minutes of registering. A fixed-response webhook cannot pass: you need an endpoint you can program. Registering a URL that is still pending issues a new challenge and a new signing secret every time — the old secret immediately stops verifying deliveries; a verified URL is left untouched unless you add ?rotate=1. The daily verification allowance (10 per destination, 40 per client, UTC day) does not reset on re-registration.","POST https://402cron.com/api/destinations/{id}/verify → the permission covers that path and everything under it, not the whole domain","POST https://402cron.com/api/tasks  {\"name\",\"url\",\"cron\",\"body\"} → your cron is live; the response shows the next 3 runs","GET  https://402cron.com/api/tasks/{id} → state, failures, deliveries left"],"verification":{"how":"Verification is one request from us to the exact URL you registered: POST, headers X-402cron-Challenge: <value> and X-402cron-Event: verify, JSON body {\"challenge\":\"<value>\"}. Reply within 5 seconds, without a redirect, with a body that is the bare value or {\"challenge\":\"<value>\"}; we read the first 512 bytes and do not check the HTTP status (2xx is fine). Then call POST /api/destinations/{id}/verify within 15 minutes of registering. A fixed-response webhook cannot pass: you need an endpoint you can program. Registering a URL that is still pending issues a new challenge and a new signing secret every time — the old secret immediately stops verifying deliveries; a verified URL is left untouched unless you add ?rotate=1. The daily verification allowance (10 per destination, 40 per client, UTC day) does not reset on re-registration.","example":"const http = require('http');\nhttp.createServer((req, res) =>\n  res.end(req.headers['x-402cron-challenge'] ?? 'ok')\n).listen(3000);"},"headers":{"note":"Every request from us carries X-402cron-Event, and its value says which kind it is: \"verify\" (the one-time challenge), \"delivery\" (a scheduled, retried or manually run delivery) or \"task_paused\" (the courtesy pause notice). Branch on it first. The label itself is not signed: before trusting a delivery or a notice, verify the signature, which covers the timestamp, the delivery id and the attempt. onDelivery, onVerification and onNotice list the headers each kind carries.","signature":"X-402cron-Signature","timestamp":"X-402cron-Timestamp","deliveryId":"X-402cron-Delivery-Id","attempt":"X-402cron-Attempt","taskId":"X-402cron-Task-Id","deliveriesLeft":"X-402cron-Deliveries-Left","challenge":"X-402cron-Challenge","event":"X-402cron-Event","eventValues":{"verification":"verify","delivery":"delivery","notice":"task_paused"},"onDelivery":["X-402cron-Event","X-402cron-Signature","X-402cron-Timestamp","X-402cron-Delivery-Id","X-402cron-Attempt","X-402cron-Task-Id","X-402cron-Deliveries-Left"],"onVerification":["X-402cron-Event","X-402cron-Challenge"],"onNotice":["X-402cron-Event","X-402cron-Task-Id","X-402cron-Timestamp","X-402cron-Delivery-Id","X-402cron-Attempt","X-402cron-Signature"],"storage":"Custom headers you attach to a task are encrypted at rest with a key that never leaves the worker. No endpoint returns them."},"signature":{"algorithm":"HMAC-SHA256, hex, prefixed with \"sha256=\"","signedValue":"timestamp \\n deliveryId \\n attempt \\n METHOD \\n path+query \\n rawBody","note":"Bytes, not characters. Method and path are included: without them a valid delivery could be replayed against another of your endpoints."},"notice":{"note":"When we pause a task we send ONE courtesy POST to the same URL. It is not retried, not charged, and it is not a delivery — but it is signed with your destination secret and the same canonical string as a delivery.","event":"task_paused","deliveryId":"notice-<taskId> — the literal prefix \"notice-\" followed by the task id","attempt":0,"headers":["X-402cron-Event","X-402cron-Task-Id","X-402cron-Timestamp","X-402cron-Delivery-Id","X-402cron-Attempt","X-402cron-Signature"],"verify":"Rebuild the canonical string from the headers exactly as for a delivery. The notice carries every field the string needs.","reasonCodes":{"no_credits":"The balance ran out. Buy deliveries, then resume.","destination_refused":"The destination stopped qualifying — not verified, out of scope, or no longer an allowed address. Fix or re-verify it, then resume.","paused_failures":"Too many failed deliveries in a row. Fix your endpoint, then resume.","paused_abandoned":"Stopped after a long silence. Credits are intact; resume to continue."},"reason":"A human-readable sentence. For a failure pause it carries the diagnostic (e.g. \"HTTP 500\").","authoritative":"The notice is a courtesy. The channel you should rely on is GET /api/tasks/{id}."},"delivery":{"guarantee":"at least once","idempotency":"Delivery-Id is stable across all attempts of one execution. Treat a repeated Delivery-Id as already handled and return 200.","maxAttempts":3,"retryAfterSeconds":[60,120],"timeoutMs":2000,"advice":"Return 202 immediately and do the work in your own background. The 2s cap is for ACCEPTANCE, not completion.","statuses":{"delivered":"2xx — your endpoint accepted it","refused":"4xx — your endpoint refused it. Not retried.","delivered_failed":"5xx — it reached your server and your app errored. Not retried.","delivery_timeout":"no answer within the timeout. Retried.","unreachable":"we could not reach the host (DNS, TLS, 520-527, 530). Retried.","skipped":"an occurrence was skipped because the previous one was still running. Not charged."},"pausing":"After 3 failed attempts the task is paused. So do 20 refusals or 10 server errors in a row. We also pause a task whose URL stopped being allowed — no longer https, outside the verified scope, or a destination that is no longer verified: the state carries the reason and nothing is charged. Resume is ONE call for any live task with credits. A DELETED task must be restored first, and an empty balance needs topping up — both refuse with 409 and say which."},"billing":{"unit":"delivery attempt, not successful execution","charged":["delivered","refused","delivered_failed","delivery_timeout","unreachable"],"notCharged":["skipped","our own internal failure","requests we never sent"],"maxExposurePerFailureCycle":"0.003 USD","balance":"counted in DELIVERIES, not money. Packs never expire.","pricing":"https://402cron.com/api/pricing"},"payment":{"how":"GET /buy/{pack} answers 402; the requirements are in the PAYMENT-REQUIRED header (x402 v2) and in the body as `accepts[0]`. Sign an EIP-3009 TransferWithAuthorization for exactly those values and repeat the same GET with the PAYMENT-SIGNATURE header (X-PAYMENT for v1). A 402 never charges anything; only a 200 means the settlement went through.","beforeYouSign":["1. The EIP-712 signature recovers to `from` under this network's USDC domain: `name` and `version` are in `accepts[0].extra`, the chain is the CAIP-2 id in `accepts[0].network`, `verifyingContract` is `accepts[0].asset`.","2. The wallet at `from` holds at least `accepts[0].amount` of that USDC on that network.","3. `value`, `validAfter` and `validBefore` satisfy `accepts[0]` (value is at least `amount`; validBefore is in the future, validAfter is not — we tolerate 120 s of clock skew), and all six authorization fields (from, to, value, validAfter, validBefore, nonce) are DECIMAL STRINGS — nonce is 32 bytes as 0x-hex.","4. For v2, `accepted` echoes `accepts[0]` exactly, and `payload.authorization.to` equals `accepts[0].payTo`."],"ifRefused":{"localCheck":"A 402 with `rejectedBy: \"us\"` failed one of OUR format checks before the facilitator was called: `error` is a sentence naming the field, `message` says what to fix. Nothing was charged or counted. `retryable` is false except for \"not valid yet\" (validAfter in the future), where the identical header passes once the clock catches up. An EXPIRED header is checked against its nonce first: if we already settled that nonce you get the same 200 receipt as any repeat (500 payment_accepted_not_recorded if the settlement is unreconciled, 409 payment_in_progress if it is still settling) — never this 402, so a 402 for an expired header means that nonce was never settled here.","facilitatorVerdict":"A 402 with `rejectedBy: \"facilitator\"` and `retryable: false` is a verdict on the signed bytes: the same header will be refused again. `message` walks the checklist above in order. Nothing was charged and the nonce is not spent — fix the cause and sign again; re-signing costs nothing.","temporary":"409 payment_in_progress, 429 limit_reached and 503 facilitator_unavailable / service_stopped / not_selling all carry `retryable: true`: nothing was charged and the nonce is not spent. Do NOT sign a new authorization — wait (`retryAfterSeconds` when given) and resend the IDENTICAL header. For not_selling there is nothing to resend: no requirements were issued, so nothing was signed.","neverRetry":"500 payment_accepted_not_recorded, 500 internal_error_creating_client and 403 wallet_blocked carry `retryable: false` and a `nonce`: money may have moved. Do NOT pay again — quote the nonce to `support.email`."}},"authentication":{"note":"A management token is issued once at first purchase and shown once only.","format":"Bearer token — paste it as \"Authorization: Bearer <token>\"","lifetime":"Until revoked by rotation.","rotation":"GET /buy/<pack>?rotate=1 from the same wallet — rotation is not a separate endpoint: it is an ordinary purchase, paid like any other, that also issues a new token and kills the old one immediately. GET is what this documentation, /openapi.json and the catalogue manifest all declare; the endpoint itself does not branch on the method, so a POST to the same URL is answered identically — build on GET. ONE EXCEPTION: HEAD carrying a payment header is refused with `head_cannot_settle` and settles nothing, because a HEAD response has no body and the receipt plus the one-time token live there. HEAD without a payment header is still the ordinary 402 storefront. OPTIONS is a CORS preflight (204, settles nothing): the till, /api/* and the public reads answer any origin without credentials and expose PAYMENT-REQUIRED, PAYMENT-RESPONSE and Retry-After, so an agent running in a browser can pay and then manage its tasks.","lost":"If you lose your token: buy the smallest pack (/buy/trial) again with ?rotate=1 from the same wallet to get a new one.","shared":"If two agents share a wallet, rotating the token will break the other agent until it gets the new one."},"limits":{"minIntervalSeconds":60,"cron":"5 fields, UTC. Day-of-month and day-of-week follow the standard: when both are restricted, the match is OR.","cronSyntax":{"fields":5,"timezone":"UTC only","operators":["*","a-b","a,b","*/n","a-b/n"],"names":{"dayOfWeek":["SUN","MON","TUE","WED","THU","FRI","SAT"],"month":["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]},"macros":["@yearly","@annually","@monthly","@weekly","@daily","@midnight","@hourly"],"dayOfWeek":{"numbering":"0-6 with 0 = Sunday; 7 also means Sunday","advice":"Write days as names (MON-FRI). Numbers differ between schedulers: Quartz, AWS EventBridge and Cloudflare number Sunday as 1, we and croniter as 0."},"translated":{"quartz":["?"],"note":"\"?\" as a whole day-of-month or day-of-week field becomes \"*\". A NUMERIC day-of-week next to \"?\" is refused, because \"?\" means the expression was written where Sunday is 1."},"notSupported":{"quartz":["L","W","#"],"fieldCounts":["six fields (seconds)","seven fields (year)"],"macros":["@reboot","@every"],"other":["CRON_TZ= prefixes","time zones of any kind"]}},"maxBodyBytes":65536,"maxHeaderBytes":8192,"maxNameBytes":200,"maxUrlBytes":2048,"maxCronBytes":256,"urlScope":"The path is canonicalised before we match it against your verified destination: \"..\", \".\" and their encoded forms are folded first. An encoded separator is NOT refused on its own: %2F inside a segment is kept and matched as it stands, so a GitLab-style project path keeps working. What we refuse is a path that HIDES A DOT SEGMENT — one where \"..\" or \".\" shows up once you decode it (we try up to twice), drop a \";\" parameter from a segment, or strip control characters. So \"/hook/..%2Fadmin\" and \"/hook/..%5Cadmin\" are refused; \"/api/v4/projects/group%2Fproject/trigger\" is not. The refusal carries reason \"encoded-separator\" — the name is older than the rule; branch on it, not on the word.","maxRequestBytes":131072,"maxLiveTasks":50,"maxDestinations":25,"verifyAttemptsPerDay":{"perDestination":10,"perClient":40,"needsBalance":"Verification does not consume a delivery, but it does need a live balance: it sends a real request to your server. With an empty balance it refuses with \"no_credits\"."},"whenFull":"Hitting a limit returns error \"limit_reached\" with the field `limit` naming it, plus `max`, `used` and one sentence saying how to free space. 409 when YOU free it (delete something), 429 when TIME does. Time comes in two shapes and the FIELD tells them apart, not the status: a DAILY ration carries `retryTomorrow: true` and no seconds; an HOURLY one carries `retryable: true`, `retryAfterSeconds` and a retry-after header — at least 1 second, never more than 3600 (the UTC hour boundary).","freeingTasks":"Limits count what is LIVE. DELETE /api/tasks/{id} frees its slot immediately, and a soft-deleted task does not count against you.","freeingDestinations":"GET /api/destinations lists them; DELETE /api/destinations/{id} removes one that no task points at.","raising":"Most of these limits are service-wide today and are raised by a deploy, not per account. The exception is `buy_hourly`: it is counted per caller — see `buyAttemptsPerCaller` — so waiting the retry-after does help. If you need more, ask before you build around the limit.","settlementAttempts":"60 payment settlements per hour across all buyers, split into two lanes: 48/hour for authorizations whose EIP-712 signature we verify locally (a plain ECDSA signature from the paying address — every standard x402 client) and 12/hour for signatures only the chain can verify (smart wallets, ERC-1271 / ERC-6492). Over a lane /buy answers 429 with a retry-after header and nothing is charged. When a lane is full, an attempt whose authorization passes an on-chain simulation of USDC transferWithAuthorization (signature, balance, validity window, unused nonce) is counted in a separate corridor of 120/hour instead — so a funded buyer is not locked out by unfunded attempts. The simulation only picks the counter; it never refuses a payment on its own. A header without a well-formed EIP-712 signature (65–4096 bytes) is refused with 402 and a reason; nothing is charged or counted. That lane refusal is one of the OLDER ones: it carries an English-or-not sentence in `error`, not a machine code, so branch on the 429 and on the retry-after header. Only the per-caller limit below answers with `limit_reached`.","buyAttemptsPerCaller":"16 payment attempts per hour per CALLER — one number, both lanes. A caller is your network address (IPv6 counted per /64) TOGETHER WITH your payer address when your signature verifies locally, and the address alone when only the chain can verify it. So two agents behind one shared egress — Cloudflare Workers, Lambda behind NAT, Vercel — each get their own allowance as long as they pay from different addresses. Replaying an already-settled payment does not count; an authorization the facilitator REFUSED does count, because it reached the key — so retrying a refused header burns the allowance and re-signing does not reset it. Over it /buy answers 429 `limit_reached` with `limit: \"buy_hourly\"`, `max`, `used`, `retryAfterSeconds`, `retryable: true` and a retry-after header; nothing is charged, your nonce is not burnt and the service-wide budget is not touched. One narrower case, said out loud: if our edge hands us no readable client address at all, every such request shares ONE allowance — we alarm on it because it means the route is broken, not that you are.","targets":"https only — an http:// URL is refused when you register a destination and when you change a task URL, and a task that still points at one stops at delivery with a notice instead of sending your headers in the clear. Hostnames only: no IP literals, no single-label or internal names (localhost, and anything ending .local, .internal, .localhost, .home, .lan, .intranet, .corp, .test, .example or .invalid), and none of our own hosts. No user:password in the URL. Ports 80/443 only, no redirects followed, and no path that hides a dot segment — see `urlScope`, where a plain %2F inside a segment is explicitly fine.","overlap":"skip (only supported mode)"},"errors":{"note":"A refusal is JSON. `error` carries the machine code below and `message` one English sentence telling you what to do. Codes are never renamed; the sentences may change, so branch on the code and the HTTP status, never on the text. Every refusal from /api/ carries a code. ONE PLACE STILL ANSWERS WITH A SENTENCE: the till, when the payment header itself will not parse — there `error` is a human sentence describing what is malformed, and the machine signal is the 402 status plus `rejectedBy: \"us\"`; that body still carries `message`, `retryable` and the full `accepts` to fix against (see `payment.ifRefused`). Branch on the status there. A resource that does not exist and one that belongs to another client both answer 404, never 403: we do not confirm what we will not show you.","codes":[{"code":"unauthorized","status":401,"meaning":"Missing or unknown bearer token. A token is issued at purchase, once."},{"code":"head_cannot_settle","status":402,"meaning":"You sent a payment header with HEAD. HEAD carries no body, and the receipt plus the one-time management token live in the body — so settling would take your money and show you nothing. Nothing was settled or charged. Repeat the identical request as GET. The reason also travels in the `x-402cron-refusal` header, because the refusal body is stripped too."},{"code":"unknown_section","status":400,"meaning":"GET /docs?section= was given a name that is not a section. `allowed` lists them; omit the parameter to get the whole document."},{"code":"not_found","status":404,"meaning":"No such path. This is about the URL, not an id — `no_such_task` and `no_such_destination` answer when the path is right and the id is not yours. Paths are listed in /openapi.json."},{"code":"invalid_cron","status":400,"meaning":"The cron expression was refused. The body carries `code` with one of the sub-codes below, `got` with your field, `field` with its index (0-based, so 4 is day-of-week), `supported` with the whole dialect, and an `example`. Branch on `code`, not on the message.","subCodes":["CRON_FIELD_COUNT","CRON_MACRO_UNSUPPORTED","CRON_QUARTZ_CHAR","CRON_DOW_NUMBERING_AMBIGUOUS","CRON_TZ_IN_EXPRESSION","CRON_NAME_UNKNOWN","CRON_OUT_OF_RANGE","CRON_STEP_INVALID","CRON_NOT_A_NUMBER","CRON_NEVER_FIRES"]},{"code":"schedule_impossible","status":400,"meaning":"The expression is valid but never fires (e.g. 30 February)."},{"code":"no_editable_fields","status":400,"meaning":"The PATCH body carried nothing editable, so nothing was changed. `allowed` lists what is; `state` is never among them — use /pause and /resume."},{"code":"unknown_field","status":400,"meaning":"The PATCH body carried a field that is not editable, next to ones that are. The WHOLE request is refused and nothing was written — a partly applied change looks like success. `rejected` names the offending fields, `allowed` lists what you may send. `state` is not among them: pause and resume are separate actions, so that an edit can never quietly bring a task back to life."},{"code":"wrong_http_method","status":405,"meaning":"The address exists, but this verb is not one of its own. `allowed` and the Allow header list the verbs it takes. On a task this is NOT `no_such_task`: the id is good. On /mcp it means the endpoint is there and speaks POST with a JSON-RPC body."},{"code":"destination_not_verified","status":409,"meaning":"No verified destination covers this URL. Register it and answer the challenge."},{"code":"too_long","status":413,"meaning":"A field or the whole request is over its byte limit. `field`, `maxBytes` and `got` say which and by how much — in UTF-8 bytes, not characters. Shorten it and resend; nothing was written."},{"code":"limit_reached","status":409,"meaning":"A structural limit is full. `limit`, `max` and `used` say which; free space and retry — waiting will not help."},{"code":"limit_reached","status":429,"meaning":"A TIME-based allowance is spent: wait, do not delete anything. Which time is in the BODY, not the status — `retryTomorrow: true` for a daily ration, `retryAfterSeconds` plus a retry-after header for an hourly one (`buy_hourly`, the payment allowance per caller)."},{"code":"unknown_state","status":400,"meaning":"The `state` filter on GET /api/tasks names a value no task ever carries. Refused on purpose: answering 200 with an empty page reads as \"you have none\", which is a lie an agent cannot tell apart from the truth. `allowed` lists what works; `got` echoes what you sent."},{"code":"unknown_filter","status":400,"meaning":"GET /api/destinations was given a `state` query it does not support — destinations are not filtered by state. Refused rather than silently ignored, so you do not read the full list as a filtered one. Read each destination’s `status` field (pending | verified); the whole list is always returned. To filter tasks by state, use /api/tasks?state=…"},{"code":"task_deleted","status":409,"meaning":"The task is deleted. Restore it first; a deleted task cannot be resumed."},{"code":"no_credits","status":409,"meaning":"The balance is empty. Resuming now would pause the task again on the next tick, and verification would send a request you cannot pay for. Buy deliveries first."},{"code":"destination_in_use","status":409,"meaning":"A task still points at this destination. The body names it; delete the task first."},{"code":"service_stopped","status":503,"meaning":"The service is deliberately stopped. Nothing was charged and nothing was sent."},{"code":"not_configured","status":503,"meaning":"We cannot store your secret safely right now, so we refused instead of storing it in the clear."},{"code":"no_such_task","status":404,"meaning":"No task with that id belongs to you. A task owned by someone else answers 404 too — never 403."},{"code":"no_such_destination","status":404,"meaning":"No destination with that id belongs to you."},{"code":"no_such_pack","status":404,"meaning":"No pack with that id is on sale. GET /api/pricing lists the live ones."},{"code":"invalid_json","status":400,"meaning":"The request body is not valid JSON."},{"code":"name_required","status":400,"meaning":"The task needs a name. It is the key we upsert on, so it cannot be empty."},{"code":"name_taken_by_live_task","status":409,"meaning":"A live task of yours already carries that name. Names are unique per client among non-deleted tasks. Returned by PATCH (rename) and by restore."},{"code":"delivery_method_not_allowed","status":400,"meaning":"The `method` you want US to send to YOUR server is not on the allow list. `allowed` lists what we deliver; CONNECT and TRACE never are. This is NOT `wrong_http_method`: that one is about the verb YOU used on us, and answers 405. This one is 400, and the request that carried it was fine."},{"code":"body_not_allowed","status":400,"meaning":"GET and HEAD carry no body. Remove it, or switch the method to POST."},{"code":"body_too_large","status":413,"meaning":"The request body is over 64 KB."},{"code":"timeout_out_of_range","status":400,"meaning":"timeoutMs must be between 250 and 2000. Two seconds is a ceiling, not a target."},{"code":"headers_not_an_object","status":400,"meaning":"The `headers` field must be a JSON object of string values."},{"code":"invalid_header_name","status":400,"meaning":"A header name is not an RFC 7230 token (or is `__proto__`, which no fetch can deliver). The `header` field echoes YOUR name, trimmed to 40 characters — it is your input, not our text."},{"code":"invalid_header_value","status":400,"meaning":"A header value is not a string, or carries CR/LF — the HTTP header separator. `header` names which one. Rejecting it here costs you nothing; letting it through would fail mid-delivery, billed."},{"code":"reserved_header","status":400,"meaning":"That header is ours to set (user-agent, host, content-length, anything prefixed x-402cron-) or belongs to the connection, not the request (connection, keep-alive, transfer-encoding, te, trailer, upgrade, expect, proxy-connection). `header` says which one you sent. Drop it and the rest go through."},{"code":"field_cannot_be_null","status":400,"meaning":"A PATCH field was sent as null. Omit it to leave it unchanged."},{"code":"overlap_mode_unsupported","status":400,"meaning":"That overlap mode is not implemented; nothing was written. Send \"overlap\": \"skip\" or omit the field — while a run is in flight the task skips the next firing."},{"code":"already_run","status":409,"meaning":"A delivery for this task is already in flight, so POST /run did not start a second one. This is `overlap: skip` doing what it promises. Nothing was sent and nothing was charged — your balance is untouched. The same code also answers a duplicate firing for a scheduled moment that has already been attempted. Wait for the run in flight to finish, then try again."},{"code":"task_not_active","status":409,"meaning":"POST /run was called on a task that is not running. Nothing was sent and nothing was charged. GET /api/tasks/{id} and read `state`: `paused_by_client` (you paused it — nobody else), `paused_no_credits` (the balance ran out), `paused_failures` (too many failed deliveries in a row — `failsInARow` counts them), `paused_destination` (your destination stopped qualifying: it is no longer verified, the task URL fell outside its verified scope, or the address itself is no longer one we deliver to), `paused_abandoned` (we stopped it after a long silence). Clear the cause, then POST /api/tasks/{id}/resume — resume re-checks the destination, so it will not put the task back into the same wall."},{"code":"destination_refused","status":400,"meaning":"The destination URL fails a guard. `reason` names which and `message` says what to send instead. This refusal always reaches you AFTER payment — credits are bought first, destinations registered second — so it says so explicitly: nothing was charged for it and your balance is untouched. Credits count deliveries, never calendar time. Point at an allowed https:// address and the same credits apply."},{"code":"destination_no_longer_allowed","status":400,"meaning":"The destination passed once but no longer does — the guard list changed under it. `reason` names the guard and `message` the way out: on a task, PATCH its URL and resume; on a destination (whose URL cannot be edited), register an allowed URL and verify that one. Nothing was sent or charged."},{"code":"destination_unreachable","status":502,"meaning":"We could not reach the destination to deliver the verification challenge; `detail` is what our request saw. The attempt counted against the daily verification allowance. Verification is one request from us to the exact URL you registered: POST, headers X-402cron-Challenge: <value> and X-402cron-Event: verify, JSON body {\"challenge\":\"<value>\"}. Reply within 5 seconds, without a redirect, with a body that is the bare value or {\"challenge\":\"<value>\"}; we read the first 512 bytes and do not check the HTTP status (2xx is fine). Then call POST /api/destinations/{id}/verify within 15 minutes of registering. A fixed-response webhook cannot pass: you need an endpoint you can program. Registering a URL that is still pending issues a new challenge and a new signing secret every time — the old secret immediately stops verifying deliveries; a verified URL is left untouched unless you add ?rotate=1. The daily verification allowance (10 per destination, 40 per client, UTC day) does not reset on re-registration."},{"code":"challenge_mismatch","status":400,"meaning":"The destination answered, but not with the challenge value we issued; `httpStatus` and `got` show what we saw. The attempt counted against the daily verification allowance. Verification is one request from us to the exact URL you registered: POST, headers X-402cron-Challenge: <value> and X-402cron-Event: verify, JSON body {\"challenge\":\"<value>\"}. Reply within 5 seconds, without a redirect, with a body that is the bare value or {\"challenge\":\"<value>\"}; we read the first 512 bytes and do not check the HTTP status (2xx is fine). Then call POST /api/destinations/{id}/verify within 15 minutes of registering. A fixed-response webhook cannot pass: you need an endpoint you can program. Registering a URL that is still pending issues a new challenge and a new signing secret every time — the old secret immediately stops verifying deliveries; a verified URL is left untouched unless you add ?rotate=1. The daily verification allowance (10 per destination, 40 per client, UTC day) does not reset on re-registration."},{"code":"challenge_expired","status":409,"meaning":"The verification challenge timed out. Register the destination again. Verification is one request from us to the exact URL you registered: POST, headers X-402cron-Challenge: <value> and X-402cron-Event: verify, JSON body {\"challenge\":\"<value>\"}. Reply within 5 seconds, without a redirect, with a body that is the bare value or {\"challenge\":\"<value>\"}; we read the first 512 bytes and do not check the HTTP status (2xx is fine). Then call POST /api/destinations/{id}/verify within 15 minutes of registering. A fixed-response webhook cannot pass: you need an endpoint you can program. Registering a URL that is still pending issues a new challenge and a new signing secret every time — the old secret immediately stops verifying deliveries; a verified URL is left untouched unless you add ?rotate=1. The daily verification allowance (10 per destination, 40 per client, UTC day) does not reset on re-registration."},{"code":"task_not_deleted","status":409,"meaning":"Restore was called on a task that is not in the recycle bin."},{"code":"cannot_delete","status":409,"meaning":"The task could not be deleted in its current state."},{"code":"cannot_restore","status":409,"meaning":"The task could not be restored in its current state."},{"code":"restore_window_expired","status":409,"meaning":"The 30-day restore window has passed. A deleted task is unrecoverable after it."},{"code":"not_selling","status":503,"meaning":"The till is deliberately closed. The body says which consent value is missing; nothing was charged and no requirements were issued, so there is nothing to sign — do not sign an authorization. Retry later, or poll GET /health for `selling`."},{"code":"misconfigured","status":503,"meaning":"The payment configuration on our side is invalid, so no payment requirements can be issued (no `PAYMENT-REQUIRED` header, no `accepts`, no address in the body). `retryable` is true and `Retry-After` is 3600. Nothing was charged and there is nothing to sign — do not sign an authorization; retry later, or poll GET /health. Repeating the header of a payment we already settled still returns the receipt."},{"code":"payment_rejected","status":402,"meaning":"The payment facilitator judged your authorization and refused it. The body is a full storefront (`accepts`, `resource`) plus `rejectedBy: \"facilitator\"`, `retryable: false` and a `message` walking the checklist in `payment.beforeYouSign`, in that order. It is `false` because the verdict is on the bytes you sent: the same header will be refused again. Nothing was charged and the nonce is not spent — fix the cause and sign again, it costs nothing."},{"code":"facilitator_unavailable","status":503,"meaning":"We could not reach the payment facilitator, so nobody judged your authorization. Nothing was charged, the nonce is not spent, and `retryable` is true — resend the identical header shortly. There is deliberately no `retryAfterSeconds`: we do not know when someone else's service returns, and an invented number is a promise we cannot keep."},{"code":"payment_in_progress","status":409,"meaning":"The same nonce is settling right now, in another request. Wait a few seconds and resend the identical header: you will get the receipt, not a second charge."},{"code":"wallet_blocked","status":403,"meaning":"This payer wallet is blocked. The payment that just settled is recorded against the `nonce` in the body and refunded by hand. Do NOT pay again from that wallet — `retryable` is false — and quote the nonce to the support address in `support.email` below."},{"code":"payment_accepted_not_recorded","status":500,"meaning":"We took your money and failed to record it. This is the one 500 you must NOT retry — the body carries `retryable: false` and the nonce. Quote the nonce to the support address in `support.email` below; paying again spends a second authorization for nothing."},{"code":"internal_error","status":500,"meaning":"An unhandled error. Safe to retry."},{"code":"internal_error_processing_payment","status":500,"meaning":"An unhandled error on the payment path. It does NOT say whether the money moved — read the body before retrying."},{"code":"internal_error_creating_client","status":500,"meaning":"The payment settled but the client row could not be created. Contact the support address in `support.email` below, with the nonce."}]},"abuse":{"policy":"We only deliver to destinations that answered our challenge. If you are receiving unwanted requests from us, the owner of that endpoint consented — and can stop it.","contact":"https://402cron.com/abuse"},"support":{"email":"abuse@402cron.com","whenToWrite":"You paid and something does not work; a refusal you cannot act on; a delivery that arrived wrong. Include the payment reference from your purchase receipt and the task id if you have one.","whenNotTo":"Do not write for an endpoint that refuses you — a 4xx from your own server is yours to fix, and the refusal body says what we saw. Unwanted requests FROM us: see `abuse` above."}}