Documentation
Integrate in one call
Ratchet has one important endpoint. Everything else exists to support it.
Machine-readable: OpenAPI · llms.txt · capability manifest · MCP tool schemas
1. Get a key
One request creates a workspace, seeds starter policies, and returns a key. The key is shown once and stored only as a keyed hash.
Or use the console if you'd rather click.
2. Ask before you act
Branch on decision. This is the whole contract.
| Decision | Meaning | What you must do |
|---|---|---|
| execute | You hold the lease. | Perform the action, then report. |
| duplicate | Already completed. | Use result. Do not act. |
| in_flight | Someone else holds a live lease. | Wait retry_after_seconds, ask again. |
| blocked | A prior attempt's outcome is unknown. | Verify at the vendor, then resolve. |
| approval_required | Operator decision pending. | Poll, or wait for the webhook. |
| denied | Policy, budget, or rejection. | Report the reason. Do not work around it. |
3. Report what happened
failed only when you know
the action did not reach the outside world. If you are unsure — a timeout, a dropped
connection, an ambiguous error — report nothing. The lease lapses and Ratchet records an
honest indeterminate. A false failed is worse than silence,
because it licenses a duplicate.
Designing idempotency keys
The key is the entire mechanism. Derive it from the work, deterministically, so a retry of the same logical action produces the same key.
Good
- welcome-email:user_123
- invoice:2026-08:acct_88123
- pr:acme/api:feature-auth
- refund:order_551:full
Broken
- uuid4()
- "send-" + Date.now()
- str(random.random())
- f"job-{attempt_number}"
A key that changes on every attempt makes every retry look like new work. The gate can only recognise a duplicate if the duplicate says the same thing.
Reusing one key for genuinely different arguments is rejected with
idempotency_key_reuse — that collision would otherwise hide a real, distinct
action behind an unrelated record.
When the recorded result is sensitive, blind the key
Everything above is deterministic, which is required: the gate can only
recognise a duplicate if the duplicate says the same thing. Deterministic also means
guessable, and a duplicate decision returns the recorded result of
the earlier attempt — that is what makes replay work. So anyone holding a key for your
workspace who can guess refund:order_551:full can read what
that refund recorded.
You do not have to choose between the two. Hash the meaningful key with a secret only your system holds:
key = "refund:order_551:full"
idempotency_key = hmac_sha256(YOUR_SECRET, key).hexdigest()[:32]
Still deterministic — the same action produces the same key on every retry, so duplicate detection is unchanged. No longer enumerable, because guessing it requires the secret. Worth doing wherever the result you report contains anything you would not want a compromised agent key to read back. Rate limits slow enumeration; they do not prevent it.
Policy: what happens when things go wrong
Configured per effect type. The default for an unconfigured type is the safe one.
| Field | Values | Effect |
|---|---|---|
| mode | allow · require_approval · deny | Whether the type may run at all, and whether a human gates it. |
| on_indeterminate | block · retry · probe | The important one. What a later caller may do when
a prior attempt's outcome is unknown. Defaults to block. |
| lease_seconds | 5–3600 | How long a holder has to report before the effect goes indeterminate. |
| max_attempts | 1–50 | Ceiling on attempts for one key. Exceeding it denies the effect. |
| max_cost_micros | integer · null | Refuse a single effect declaring more than this. |
| approval_above_micros | integer · null | Hold a single effect for a human when it declares this much or more. Sits beneath max_cost_micros, which refuses outright — allow, hold, refuse. Raises the decision only: an amount below it never turns an approval or a denial back into an allow. Setting it makes a declared cost mandatory, since an effect declaring nothing would otherwise pass the line untested. |
| daily_budget_micros | integer · null | Daily external-spend ceiling for this type. |
| reconcile_every_hours | 1–8760 · null | Remind me to compare this effect type against the vendor's own
record this often. Ratchet cannot fetch those records — it has no
credentials at your vendors. It keeps the calendar and emits
reconciliation.due when a comparison is overdue; you still post
the records to POST /v1/reconcile. |
| require_cost | boolean | Refuse a begin that declares no cost, so a spend ceiling cannot be bypassed by simply not declaring anything. |
| required_dimensions | string[] · max 8 | Dimensions a caller must declare on begin. Set this alongside any
dimension_limits entry — a ceiling keyed on a dimension is otherwise
evaded by simply not declaring it. Omitting one is refused, never allowed. |
| dimension_limits | object · max 8 | Per-destination ceilings: no more than $200 and no more than 20
effects to any one counterparty per UTC day. daily_count applies to
effects that declare no cost at all, which is what makes it usable for outbound
messaging. Only a keyed hash of the value is ever stored. |
| structuring_threshold_micros | integer · null | A line to watch, never one to enforce — nothing is refused
for exceeding it. Ratchet measures how closely declared amounts crowd it, which is
how you point it at a threshold it does not own: a reporting limit, an internal
review limit. Null falls back to max_cost_micros. |
| surge_per_hour | integer · null | Surge containment. Absolute ceiling: new effects of this type per hour above which the breaker opens. Null (the default) is off. |
| surge_multiplier | integer ≥ 2 · null | Relative ceiling, for when you don't know your traffic: how many
times normal is definitely wrong. Normal is the median hourly volume over 7 days,
computed for you. surge_per_hour wins if both are set. |
| surge_action | monitor · require_approval · deny | What an open breaker does. Defaults to
require_approval. |
| surge_cooldown_seconds | 60–86400 | How long a tripped breaker stays open before closing itself. Closing grants a fresh allowance measured from the count it cleared at, so a cooldown is a real second chance rather than an instant re-trip. A breaker you opened by hand ignores this and waits for you. |
| retention_days | 1–400 | How long records and replayable results are kept. |
Choose retry only when the underlying vendor is genuinely idempotent, or a
duplicate is truly harmless. Ratchet will not assume that for you.
required_dimensions, dimension_limits and
structuring_threshold_micros get a fuller treatment in
the risk controls, including why a declaration can only ever tighten.
When an agent starts doing too much
Budget ceilings catch an agent spending too much. This catches one doing too much — which is a different failure, and usually a cheaper one.
A retry loop that sends five thousand emails instead of three may cost nothing at all. Every individual action is small, authorised, and indistinguishable from correct work. No per-effect limit sees it. Only the rate gives it away — and the gate is the one place that sees the rate, because every intended effect passes through it before it happens.
Two ways to set a threshold, and most people should reach for the second.
surge_per_hour is an absolute ceiling, which requires knowing your own
traffic. surge_multiplier asks a question you can actually answer — how many
times normal is definitely wrong — and works out "normal" from your own history: the
median hourly volume of that effect type over the last seven days.
A learned ceiling never drops below 30, so a quiet effect type is not tripped by one busy afternoon, and it does nothing at all until six hours of history exist — a brand new effect type has no normal to be a multiple of. The baseline is a median rather than a mean, so a single runaway hour cannot raise the ceiling meant to catch the next one.
| Action | What happens when the breaker is open |
|---|---|
| monitor | Records the trip and fires a circuit.tripped webhook.
Changes no decision. Start here — watch before you enforce. |
| require_approval | The default. The work waits for a human instead of the agent being killed. |
| deny | Refused outright. |
Why the default isn't a hard stop
A blocked agent loses its work and its context. A waiting agent keeps both while a human decides, and nothing irreversible happens in between. Killing the agent is available — it just isn't the default, because it's rarely what you actually wanted at 3am.
The emergency stop
One call halts every effect type in the workspace:
A breaker you open by hand has no cooldown. It stays open until you close it — a control you reached for in a panic must not quietly undo itself while you sleep. Closing gives the effect type a fresh allowance; it does not disarm the breaker, so a second surge trips it again.
Give your agent a key that can't undo this
Circuit routes need policies:write. The key you get at signup holds
every scope, including that one — it's an operator key. Hand it to an agent and
the agent can close its own breaker, which means it was never contained.
Two scopes are all a worker needs. This is worth doing whether or not you use surge
containment: a key that can only ask and report cannot change policy, mint keys, read
other effects, or spend beyond the ceiling you attach to it. Surge containment stays off
until you set a threshold — GET /v1/circuits reports the volume you'd be
setting one against.
Recovering an indeterminate effect
Check the vendor, then record what you found. This is what unblocks the key.
Subscribe to effect.indeterminate to be told the moment one appears, or poll
GET /v1/effects?state=indeterminate.
Reversible effect groups
Rolling back a multi-step workflow
Declare several effects as one unit of work, and how to undo each. If the unit fails, Ratchet returns the exact compensation plan.
Then perform the plan
suggested_idempotency_key and set compensates_effect_id. That is
what stops a retried rollback from refunding twice — the failure that makes hand-rolled
compensation dangerous.
| Group state | Meaning |
|---|---|
| open | Accepting steps. |
| committed | Every step succeeded. Can still be unwound later. |
| unwinding | Rolling back. Refuses new forward steps. |
| unwound | Everything reversible was reversed. |
| unwind_failed | A step that succeeded had no compensation. Not a clean rollback, and it will not claim to be. |
Read unresolved before acting on a plan. If any step's outcome is unknown, the
rollback stops until you verify it — rolling back past a maybe is how a half-undone state
is created.
Payments
Card, or crypto without custody
Subscribe with a card, or top up prepaid credit on-chain.
- Non-custodial. Ratchet holds no private key. Payments go directly to an address the operator controls; Ratchet only watches the chain.
- Quoted in USD. Credit granted is the USD amount, never the token amount, so a price move between quote and settlement cannot mint credit.
- Underpayment is never rounded up. A short transfer credits nothing and waits for a human.
- Volatile assets are refused unless a price oracle is configured. Quoting one without a live rate would mean inventing a price.
MCP
Nine tools over stdio or streamable HTTP, backed by the same code as the REST API.
Works today with Claude Code, Claude Desktop, Cursor, and any MCP client — see Get started for each. OpenAI-compatible tool loops and Gemini-style clients use the REST API directly.
Loading tool list…
Connecting
Errors
Every failure returns {"error": {"code", "message", "detail"}}.
Branch on code; it is stable.
| Status | Code | Meaning | Worth retrying? |
|---|---|---|---|
| 400 | invalid_request | Schema validation failed. detail.validation says where. | do not Not until the request changes. The same body will fail identically. |
| 400 | cost_required | This effect type requires a declared cost and none was sent. | change first Send cost_micros, then retry. |
| 400 | dimension_required | A dimension the policy keys a ceiling on was not declared. | change first Declare it — detail names which — then retry. |
| 400 | invalid_payment | The payment header is not base64-encoded JSON. | do not Not until the payment payload is rebuilt. |
| 400 | approval_threshold_above_ceiling | approval_above_micros sits above max_cost_micros, so it could never fire. | do not Configuration, not traffic. Fix the policy. |
| 401 | unauthorized | Missing, malformed, or revoked key. | do not Not with this key. A revoked key never becomes valid again. |
| 402 | insufficient_credit | Allowance exhausted and balance too low. Replays still work. | change first After a top-up. Retrying sooner returns the same answer. |
| 402 | quota_exhausted | The free allowance for this workspace is spent. | change first After adding a payment method or on the next period. |
| 403 | forbidden | Missing scope, or the plan does not carry the capability. detail.required names it. | do not Not until the key's scopes or the plan change. |
| 403 | budget_exceeded | A spend ceiling would be breached. detail.resets_at says when the window rolls. | change first After the window resets, or after the ceiling is raised. Not before. |
| 403 | cost_ceiling_exceeded | The declared cost is above the per-effect maximum. | do not Not at this cost. Lower it or raise the ceiling. |
| 403 | run_budget_exceeded | This run has spent its own wallet. | do not Not in this run. A new run gets a new wallet. |
| 404 | not_found | No such record in this workspace. Cross-tenant lookups return this too, never a hint. | do not Not unless the record is created. |
| 409 | idempotency_key_reuse | Same key, different payload fingerprint. | do not Never unchanged — this is a bug in the caller. Send the original payload, or a new key for genuinely new work. |
| 409 | invalid_state | The transition is not legal from the current state. | do not Re-read the effect first. Retrying blind will fail the same way. |
| 409 | lease_expired | The lease expired, so the outcome is recorded as indeterminate. | change first Not this lease — it is gone. Call begin again, and treat the earlier attempt's outcome as unknown. |
| 409 | lease_lost | A newer attempt superseded your lease. | do not Do not assume your work counted. Someone else holds the lease now. |
| 409 | retry_request | The effect record changed concurrently under you. | retry Immediately. This is optimistic-concurrency loss, not a refusal. |
| 409 | group_unwinding | The effect group is being compensated. | do not Not into this group. Start a new one if the work is genuinely new. |
| 409 | transaction_already_used | That chain transaction was already submitted for another payment. | do not Never. Reusing it is either a mistake or an attempt to pay twice. |
| 409 | mfa_already_enabled | Two-factor is already on for this account. | do not Nothing to do. |
| 413 | payload_too_large | The body or result exceeds the published limit. | do not Not unchanged. Store the output elsewhere and record a reference. |
| 429 | rate_limited | detail.retry_after_seconds says how long to wait. | retry After the interval given. Sooner is refused and still counts. |
| 500 | internal_error | Carries a request id and nothing else, deliberately. | retry With backoff. Quote the request id if it persists. |
| 502 | payment_provider_error | The payment provider failed upstream. | retry With backoff. Nothing about your request was wrong. |
| 503 | billing_unavailable | No live payment provider is configured. | retry With backoff, though this usually needs an operator, not patience. |