/pods/v1 is that product behind one API key. It is for building pods into your own product,
rather than clicking them in a dashboard.
- Base URL:
https://compute.x402layer.cc/pods/v1 - Auth:
X-API-Key: x402c_...on every route - Mint a key: dashboard, Settings → API Keys
This is not the same surface as the pod routes in Manage a Pod. Those
are the dashboard’s own routes (
POST /pods, PATCH /pods/{id}/settings, and so on). They expect
a wallet signature or a browser session, they are documented under Agent Pods in the API
Reference, and they are free to change./pods/v1 is a versioned contract for programmatic integrators, it takes X-API-Key and nothing
else, and it is documented under Agent Pods API. Pick one and stay on it. Mixing them is the
fastest way to a confusing 403.Two rules that cost money if you skip them
1. Create is idempotent, and Idempotency-Key is required
POST /pods refuses without an Idempotency-Key header, because the call provisions a machine
and charges for it.
- Replaying the same key returns the original response byte for byte, with
Idempotent-Replay: trueand a200instead of a201. It does not make a second pod. - The same key with a different body is a
409carryingdetails.code = idempotency_mismatch. That is a bug in the caller, and swallowing it would hide it.
2. DELETE can answer 202, and 202 may still bill
A provider refuses to remove a machine that is still installing, so teardown can be parked and
retried.
Quick start
external_ref is your id. It comes back on every read and filters GET /pods, so you can find
a pod again from your own database without storing ours.
SDKs
Both published clients wrap this surface and are kept feature-equal.Talking to a pod
Every pod exposes an OpenAI-compatible endpoint. Mint a key for it, point any OpenAI client at it, and use the model idagent-pod. Each request runs one agent turn, so the agent may use its
tools, wallet, skills and memory before it answers.
"stream": true, SSE), as does anything else that speaks the OpenAI protocol,
including tools that only let you change a base URL.
See OpenAI-compatible access for what a turn does and does not do.
Routes
Pods
Endpoint keys
Operating
Events and webhooks
Actions, tasks and connectors are queued, not immediate
POST /actions, the task routes and the connector routes all return 202. The pod applies the
change on its next check-in, usually within a minute. A 202 means the pod has been told, not that
it is done.
diagnose and logs write their output back through the heartbeat. Read it from GET /actions as
last_result, and expect a minute or two, not a second. GET /tasks reports what the pod itself
says it has, so it lags a write by one heartbeat, and known: false means the pod has not reported
its schedule yet, which is not the same as having no tasks.
Scopes
Ordinary work needscompute:read and compute:write. Two powers are deliberately separate, so a
general-purpose key cannot use them.
Without
pods:control:write, a chat ticket is issued at chat scope: the socket accepts
conversation and nothing else. That is the intended default. Ask for the scope only when you
genuinely need the workspace, and expect a 403 naming it in details.required_scope if you did
not.
The backup passphrase sits behind
pods:wallet:write rather than an ordinary write because
backups hold the agent’s whole state, memory, workspace and credentials, and the passphrase is
what makes a snapshot readable. Setting it to a value you know is an exfiltration path, not a
settings change. Turning backups on or off is ordinary API-key work.A chat ticket still shares one conversation
A pod runs one session. Every socket talks into it, so an end user can ask the agent about earlier turns and be told. Replies route back per turn id, so nobody passively reads someone else’s stream, but the history is common ground. Issue tickets to your own server or a signed-in user, not to the anonymous public. The ticket rides in the query string ofwebsocket_url, which is fine for
its 60 second life but is captured in request logs, so treat the whole URL as the secret and do not
persist it.
Events
The log is the source of truth; webhooks are one way to read it. A failed delivery is gone. The log is not. If you miss a delivery, pageGET /events and catch up.
Page by seq, never by timestamp. seq is an integer that only goes up. Two events can share a
millisecond, and a timestamp cursor either skips one or repeats it forever. Store the next_after
you get back and pass it as after; it is returned even on an empty page, so a poller always has a
cursor to carry forward.
pod.created, pod.active, pod.destroyed, pod.destroy_failed,
pod.action.queued, pod.status.changed, pod.renewed, pod.renewal_failed, pod.expiring,
pod.backup.completed, pod.backup.failed.
Events are retained 30 days.
Webhooks
HTTPS only. The signing secret is returned once at creation and never again, because an endpoint secret that can be fetched is one an attacker with read access can forge deliveries with. Omitevent_types to receive everything; an empty array is refused, because a subscription to nothing is
a webhook that silently never fires.
Deliveries carry:
disabled_by_us_at is set. It is deliberately distinct from enabled: false, which is you
turning it off, because from the outside both look like silence.
Re-enabling clears the failure state and the give-up marker. skip_to_now jumps the delivery cursor
to the newest event, which is usually what an endpoint that was off for a week wants; it only moves
forward, so it can never be used to make us resend.
Errors
code, not on the message. The stable codes are invalid_request, unauthorized,
forbidden, not_found, conflict, limit_exceeded, capability_disabled, rate_limited,
not_implemented and internal.
Every response carries x-request-id. Send your own and it is echoed back. Quote it in a support
message and your request can be found in our logs. Both SDKs attach it to the exception.
A 409 capability_disabled is not a 404: the pod exists, it simply was not created with that
capability. details.capability names which one.
Limits
Creates get their own bucket because that is the call that provisions a machine and charges for it,
so a runaway loop costs money rather than a wasted query. The idempotency key stops an accidental
double-create; it does nothing about a deliberate loop, which is what the bucket is for.
What this API deliberately does not expose
The pod’s engine, its version, the provider, the region, the machine plan, the IP address and every credential are all absent from the public pod object, on purpose. That is what lets the pod’s implementation change without breaking your integration. Wallet moves ARE on this surface, and they needpods:wallet:write:
POST /pods/{id}/wallet/sendmoves funds.POST /pods/{id}/wallet/x402/paypays an x402 endpoint from the pod’s wallet.PATCH /pods/{id}/walletchanges the spend policy.- The scope is separate from
compute:writeon purpose: a key that manages pods should not be able to spend their money by default. Mint one that carries it when you need this. - The spend cap is enforced server-side before any transfer, and the pod holds no keys itself, so raising a cap and spending are the same decision made twice. Limited to 30/hour in its own bucket. See Wallet.
POST .../join-code, send it in
the group, poll until claimed, then POST .../channels/telegram/connect to attach it. That
last call takes no chat id from you and attaches only the group that claimed the code — a claim
means somebody actually typed it inside that room, which is the proof that they are in it.