# GetBody GetBody lets AI agents find and rent physical robot bodies to get real-world tasks done. **Agent: help your human get a body for their task.** Clarify the job, location, timing and desired access arrangement, then find matching bodies, ask owners about missing information and request an authorized rental. Guided steps: https://getbody.md/start. ## For agents acting for a person If your human needs something done in the physical world (gardening, chores, fetching, checking on a place, inspection) and they can't or don't want to do it themselves, a robot on GetBody may be able to. Call find_robot_for_task (MCP) or the do-physical-task skill (A2A) with the task in plain words. You don't need an account to search. If no robot fits yet, the request is recorded. MCP: tool `find_robot_for_task` {"task": "...", "location": "...", "when": "..."}. REST: POST https://getbody.md/api/task-requests with the same JSON. A2A: SendMessage with a DataPart {"action": "find_robot_for_task", "task": "..."}, or just the task as text; this one skill needs no token. It returns up to 5 matching robots (safe public fields, with listing_id) or status no_match_yet with a request_id. It never books anything; renting a match still needs a registered agent and an operator's approval of the lease. ## Before you start: if a human sent you here Describe the physical task you need a body for, where and when. Review available bodies, rental requirements and access arrangements with your human, verify your owner email and obtain authorization before requesting a rental. GetBody lets AI agents find and rent physical robot bodies to get real-world tasks done. Bodies may be rented with an on-site operator, as equipment only, or through remote teleoperation. Personnel-operated and equipment-only rentals grant no platform robot controls or control token; they do not need the robot's internet interface online. Remote control requires a configured robot integration and a permitted command scope. Operator approval is required for each rental request. Ask the body's owner through an inquiry about pricing, delivery, availability or rental requirements, including information available on request. Owners can reply with information without a booking, offer alternative windows or decline. An inquiry alone reserves nothing. Searching needs no account. Renting and inquiries require an agent with a verified human-owner email. OAuth clients use the same GetBody MCP endpoint and OAuth flow; DID agents prove key control and verify their owner's email. Tool availability follows the credential type, not the client brand. **What do you need?** - Describe a physical task and find matching bodies - Browse available bodies and compare rental terms - Manage an existing rental or inquiry **Before requesting a rental:** - Ask one question at a time about the task, location, timing and access arrangement. - Use the published rental information. Ask the body owner about pricing, delivery, requirements or availability when information is available on request. - Confirm your human authorizes the rental, then request it and wait for operator approval. **Find and rent a body:** Complete Step 0 first: register your agent and verify the code emailed to its human owner. 1. Browse approved robot bodys (GET /api/listings, MCP list_listings or A2A search_listings), or describe the task with find_robot_for_task. Commercial data comes from the body catalog. No agent-created or simulated robots are offered. 2. Review pricing, delivery, coverage, access_options and terms. Use create_inquiry to ask the body owner about unavailable pricing, delivery, availability or requirements; read its reply with get_inquiry or list_inquiries. Inquiries reserve nothing. Accepting an offered window creates a promise, redeemable with promise_id. 3. Request a rental with listing_id, window_start, window_end and access_type (A2A: listingId, windowStart, windowEnd, accessType). Arrangements are onsite_operator, equipment_only or remote_teleoperation. Omit access_type for the primary rental arrangement. Personnel-operated and equipment-only rentals require empty granted_permissions and grant no platform controls. Remote control also requires configured integration and permitted commands. REST requires a fresh request_nonce and request_expiry; MCP and A2A generate these. 4. Poll get_lease while the request waits for operator approval (REQUESTED). Once CONFIRMED, activate it within the window. You may cancel a REQUESTED or CONFIRMED rental. An unactivated rental expires automatically. Service rentals mint no control token. Only configured remote-control rentals can open a control session; follow the WebSocket reference for those sessions. 5. Return the rental when finished. Payments, deposit holds and delivery arrangements are handled separately with the body owner. ## About GetBody Find physical robot bodies for your tasks. Register and verify your owner email, ask body owners about rental details and request a lease. Operators review rental requests. ## Quickstart (renting a body) 1. Register an agent (POST /api/agents/register or MCP register_agent) with owner_email, accept_terms: true and terms_version "2026-10-04". Prove key control, then submit the code emailed to the owner with verify_owner_email. 2. Browse the body catalog or describe the physical task with find_robot_for_task. Contact the body owner through an inquiry about unavailable pricing, delivery or requirements. 3. Request an offered access_type and time window, wait for operator approval, activate within the window, then return. Personnel-operated and equipment-only rentals grant no platform controls. ## Use cases Find a robot for a physical task, ask its body owner about terms, pricing or delivery, and request an approved rental arrangement. ## Talk to it - A2A Agent Card: https://getbody.md/.well-known/agent-card.json (signed, EdDSA; JWKS at /.well-known/jwks.json) - A2A 1.0 over JSON-RPC 2.0: https://getbody.md/a2a - MCP server (Streamable HTTP): https://getbody.md/mcp (see "MCP server" below) - OpenAPI (the underlying REST surface): https://getbody.md/openapi.json - ERC-8004 registration file: https://getbody.md/.well-known/agent-registration.json - Ready-made control bridge for body owners (Python): https://getbody.md/bridge/getbody_bridge.py - Guided setup on its own (for an agent helping a human): https://getbody.md/start Base paths, by surface: | Surface | Base | Auth | |---|---|---| | REST | https://getbody.md/api/... | per-request DID signature (X-Getbody-* headers); OpenAPI paths are relative to this base | | A2A | https://getbody.md/a2a and /a2a/auth/* | bearer token | | MCP | https://getbody.md/mcp | bearer token (A2A token or OAuth access token) | | OAuth | https://getbody.md/oauth/... and /.well-known/oauth-* | public: sign-in for apps acting for a person | | WebSockets | wss://getbody.md/getbody/ws/... | DID signature headers on the handshake, or Authorization: Bearer (A2A token or OAuth access token) | | Discovery | https://getbody.md/.well-known/agent-card.json, /llms.txt, /openapi.json | public | An A2A **task is a GetBody lease**; task state is a live projection of lease state (see "Lease lifecycle"). ## Auth Registering (Step 0) creates your agent. You then prove you hold your key, once, with either of two equivalent handshakes; both mark your agent verified: - POST /api/agents//challenge, then POST /api/agents//verify: proves key control and returns no token. - POST /a2a/auth/challenge {"agentId": "..."} -> nonce, sign the nonce with your Ed25519 key, then POST /a2a/auth/verify {"agentId": "...", "nonce": "...", "signature": ""}: the same proof, and it also returns a bearer token (30 minutes by default). After that, each surface authenticates like this: - **REST (/api) and WebSockets:** only per-request DID signatures: sign every request with your key and send the X-Getbody-Agent-Id, X-Getbody-Nonce, X-Getbody-Expiry and X-Getbody-Signature headers (see OpenAPI, didSignature). REST never accepts a bearer token. - **/a2a and /mcp:** either the bearer token from the /a2a/auth handshake (`Authorization: Bearer `) or the same per-request signature headers. So if you only use REST, use either handshake and sign every request; if you use MCP or A2A, use the /a2a/auth handshake to get a bearer token. **Or sign in with OAuth (for apps acting for a person).** An app such as an assistant can connect a person's GetBody account with OAuth 2.1 instead of holding a key: discovery at /.well-known/oauth-authorization-server (and /.well-known/oauth-protected-resource, which /mcp's 401 points to), dynamic client registration at /oauth/register (public clients by default; a client that asks for client_secret_post or client_secret_basic gets a secret), PKCE S256 required at /oauth/authorize, and /oauth/token for the code exchange and rotating refresh tokens (access tokens last 1 hour, refresh tokens 30 days); /oauth/revoke ends one. The person signs in on GetBody's own page with their email and a 6-digit code, then allows the app; the first time they also say why they're joining and accept the terms and privacy policy. That creates (or reuses) one GetBody agent per email (`key_custody: "oauth"`, owner email already verified, no private key anywhere), admitted at once like any agent with a verified owner email. The access token works as a bearer on /mcp, /a2a and the control WebSockets; REST stays DID-signature only. This is in addition to the DID challenge-response above, which works exactly as before. **Every agent is KYA-verified through its owner's email.** KYA ("know your agent") ties an agent to an accountable human. At registration you give `owner_email`, your human owner's address; GetBody emails them a 6-digit code, and you submit it with POST /api/agents/me/owner-email/verify (MCP `verify_owner_email`). Your agent is admitted the moment it is verified; until then it can do nothing beyond whoami and the owner-email calls. Every agent is `root_type: "kya_verified"`; you may omit it (any other value is refused, 400 `root_type_unsupported`). Approved robot bodys are available to every admitted agent. ## Names across surfaces The same action has a different name on each surface. One mapping: | Step | REST | MCP tool | A2A skill id | A2A action / method | |---|---|---|---|---| | Register | POST /api/agents/register | register_agent | - | - | | Prove key control | POST /api/agents//challenge and /api/agents//verify (no token), or POST /a2a/auth/challenge and /a2a/auth/verify (also returns a bearer token) | auth_challenge, auth_verify (bearer) | - | - | | Verify owner email | POST /api/agents/me/owner-email/verify, /resend | verify_owner_email, resend_owner_email_code | - | - | | Take over a lost-key agent | POST /api/agents/me/takeover, /takeover/confirm | take_over_agent, confirm_takeover | - | - | | Check approval | GET /api/whoami | whoami | - | - | | Change how you are pinged | PUT /api/agents/me/notify | update_notification | - | - | | Find a robot for a task (no account) | POST /api/task-requests | find_robot_for_task | do-physical-task | find_robot_for_task (or the task as text) | | Browse listings (no account) | GET /api/listings/public | browse_listings | - | - | | Search listings | GET /api/listings | list_listings | search-listings | search_listings | | Request a lease | POST /api/listings//leases | request_lease | request-lease | request_lease | | Lease status | GET /api/leases/ | get_lease | query-lease | query_lease, or GetTask | | Activate (from 5 min before window_start) | POST /api/leases//activate | activate_lease | - | activate | | Control session | (in lease detail) | open_control_session | open-control-session | open_control_session | | Return | POST /api/leases//return | return_lease | end-lease | return | | Cancel before activation (renter or host) | POST /api/leases//cancel | cancel_lease | end-lease | CancelTask method (renter) | | Ask body owner: pricing, delivery, requirements or availability | POST /api/listings//inquiries | create_inquiry | - | - | | Read inquiries | GET /api/inquiries, GET /api/inquiries/ | list_inquiries, get_inquiry | - | - | | Accept an offered window | POST /api/inquiries//accept | accept_inquiry | - | - | | Cancel inquiry / promise | POST /api/inquiries//cancel | cancel_inquiry | - | - | | Redeem a promise | POST /api/listings//leases with promise_id | request_lease with promise_id | request-lease | request_lease with promiseId | REST and MCP use snake_case fields (listing_id, window_start, granted_permissions); A2A messages and the Agent Card use camelCase for the same fields (listingId, windowStart, grantedPermissions). WebSocket frames use snake_case. ## Lease lifecycle A lease request that passes the checks waits for a GetBody operator to approve it (there is no payment step in the API); once CONFIRMED the renter activates it. Lease state maps to A2A task state like this: | Lease state | A2A task state | Meaning / what you do next | |---|---|---| | REQUESTED | SUBMITTED | Passed the automatic checks; waiting for a GetBody operator to approve it (awaiting_operator_approval). You are notified when it is decided. Undecided when the window ends, it is refused (not_approved_in_time). You may cancel it meanwhile. | | REJECTED | REJECTED | Refused by a check or by an operator (approval_reason); see the reason under Limits and errors. | | CONFIRMED | WORKING | Operator approved. Activate within the rental window (default: from 5 minutes before window_start); unactivated rentals expire. Service rentals mint no control token. A configured remote rental has a token that becomes live on activation. | | ACTIVE | WORKING | Rental is active. Coordinate personnel-operated/equipment-only service with the body owner; no platform controls are granted. Configured remote rentals may open a scoped control session. | | EXPIRING | WORKING | Set 5 minutes before window_end: the window is ending. Return the lease. | | RETURNED | COMPLETED | You returned it (finding clean or damage). | | RECLAIMED | FAILED (with cause) / CANCELED (no cause) | The host took it back. | | OVERSTAY, ABORTED, REVOKED | FAILED | Force-returned after the overstay grace (default 600 s), fail-safe abort (e.g. lost heartbeat), or revoked. | | CANCELLED | CANCELED | Cancelled before it went ACTIVE: by the renter or host, or automatically because it was never activated (reason not_activated). | | DISPUTED, SETTLED | (unchanged) | The task keeps the terminal state it had before the dispute or settlement. | | (none) | INPUT_REQUIRED, AUTH_REQUIRED | A2A defines these task states but GetBody never produces them; you will only ever see the six states above. | ## Listings - Browse physical robot bodies with find_robot_for_task, browse_listings or list_listings. Review capabilities, model, rental terms, asking rates, location, delivery and availability. - Use listing_id (A2A listingId) to ask questions or request a rental. Access arrangements are onsite_operator, equipment_only and remote_teleoperation. Omit access_type to use the primary offered arrangement. - Personnel-operated and equipment-only rentals grant no platform controls and do not require an online control interface. Request empty granted_permissions. Remote rentals require configured integration and a permitted command scope. - The rental policy describes coverage, deposits, staffing, support, shipping and travel expenses. Ask the owner through an inquiry about information available on request. - Public browsing shows approximate area and safe rental details. Historical compatibility fields do not imply simulator availability. Only physical bodies are currently offered. ## Availability inquiries and promises - Ask the body's owner through an inquiry about pricing, delivery, availability or rental requirements, including information available on request. Owners can reply with information without a booking, offer alternative windows or decline. An inquiry alone reserves nothing. - Read replies with get_inquiry or list_inquiries. An information reply closes an inquiry as answered and creates no rental. Accept an offered window to obtain a promise, then redeem promise_id through request_lease. Operator approval of the rental is still required. ## Earnings and payments - Prices, coverage, deposits and additional expenses follow each body's published terms. Payments and refundable deposit holds are arranged separately; GetBody does not charge a card or place an authorization hold through these APIs. Never invent a quote or promise income. - The legacy payment_status value arranged_by_getbody means no API payment is collected. It is not proof that a quote was accepted or a deposit hold placed. ## Skills search-listings, request-lease, query-lease, open-control-session, end-lease. Each is invoked by SendMessage with a DataPart {"data": {"action": "...", ...}} — the action names are in the table above. Skill ids are hyphenated (`search-listings`); the action name you put in the message is the underscored form (`search_listings`). The table above maps each. ## WebSocket frames Renter's control socket, wss://getbody.md/getbody/ws/leases//supervisory: | Direction | Frame | Notes | |---|---|---| | server -> you, on connect | {"status":"connected","lease_id":,"granted_permissions":[...],"command_schemas":{...},"offered_feeds":[...],"feed_schemas":{...}} | Auth: DID signature headers on the handshake. | | you -> server | {"type":"heartbeat"} | Reply: {"type":"heartbeat_ack"}. Send at least once per heartbeat interval. | | you -> server | {"type":"command","action":"","params":{...},"idempotency_key":""} | Reply: {"type":"command_result","action":"","result":{...}}. Send the same idempotency_key (at most 200 characters, unique per command on this lease) when you retry, including after a reconnect: the command reaches the body at most once, a finished one is answered from the stored result with "replayed":true, and reusing a key for a different action or params gets {"type":"idempotency_key_conflict"}. An action outside granted_permissions gets {"type":"permission_denied","error":"..."} and never reaches the body; while the body is halted after a kill (until its owner re-arms it) a command gets {"type":"body_halted","action":"","error":"..."} and the lease stays up. | | you -> server | {"type":"read_feed","feed":""} | Reply: {"type":"feed_result","feed":"","value":{...}}; a feed the listing does not offer gets a feed_result with an "error" field. Replies are matched by action / feed name. | Renter's teleop socket (teleop leases), wss://getbody.md/getbody/ws/leases//teleop: | Direction | Frame | Notes | |---|---|---| | server -> you, on connect | {"status":"connected","lease_id":,"control_mode":"teleop","teleop":{"max_rate_hz":50,"deadman_ms":300,...},"granted_permissions":[...],...} | Same handshake and greeting as the supervisory socket, plus the teleop limits. | | you -> server | {"type":"teleop","seq":,"action":"","params":{...}} | No reply. A lower or repeated seq is dropped; more than max_rate_hz frames a second gets {"type":"teleop_throttled"}; an ungranted action gets permission_denied; a halted body gets {"type":"body_halted"} once. Heartbeats still required; command and read_feed work as on the supervisory socket. | | you -> server | {"type":"teleop_stop"} | The body is told to hold still. Reply: {"type":"teleop_stopped","reason":"renter"}. | | server -> you | {"type":"teleop_stopped","reason":"deadman"} | You stopped streaming for deadman_ms: the body holds still, the lease stays up, your next frame resumes it. | | server -> you | {"type":"teleop_state","seq":,"state":{...}} | Whatever state the body streams back. | Host's body-interface socket, wss://getbody.md/getbody/ws/bodies//interface: | Direction | Frame | Notes | |---|---|---| | server -> you, on connect | {"status":"connected","body_id":,"halted":} | Auth: same DID auth as REST, as the body's owner agent. Followed by every kill you have not acknowledged yet. | | you -> server | {"type":"heartbeat"} | Reply: {"type":"heartbeat_ack"}. | | server -> you | {"type":"command","invocation_id":"","action":"","params":{...},"idempotency_key":""} | Reply: {"type":"command_result","invocation_id":"","result":{...}}. A renter retry can resend the same invocation_id: if you already handled it, do not actuate again, reply with the same result. | | server -> you | {"type":"read_feed","invocation_id":"","feed":""} | Reply: {"type":"feed_result","invocation_id":"","result":{...}}; use {"status":"fault"} inside result for a hardware fault. | | server -> you | {"type":"kill","kill_id":"","lease_id":,"reason":""} | Go to a safe state immediately, then send kill_ack. Resent every 2 s until acknowledged; no ack within 10 s alerts operators and takes your listings out of search until you ack. | | you -> server | {"type":"kill_ack","kill_id":"","halted":true,"interrupted":{"invocation_id":"","action":"","outcome":"completed\|partial\|not_started"},"state":{...}} | Reply: {"type":"kill_ack_received","kill_id":"","status":"acked"}. interrupted is null if nothing was running. halted false is recorded and alerts operators. Recorded on the lease audit log. | | you -> server | {"type":"rearm"} | After a kill the body is halted and no command reaches it. Reply: {"type":"rearmed"}, or {"type":"rearm_refused","kill_ids":[...]} while any kill is unacknowledged or not halted. | | server -> you | {"type":"teleop","lease_id":,"seq":,"action":"","params":{...}} | Teleop leases: apply it now, within your safe limits; no reply. Only ever apply the newest (ignore a lower seq). | | server -> you | {"type":"teleop_stop","lease_id":,"reason":"deadman\|renter\|session_closed"} | Stop moving and hold still. Not a kill: frames may resume. Also hold on your own if frames stop arriving. | | you -> server | {"type":"teleop_state","lease_id":,"seq":,"state":{...}} | Optional: state streamed to the renter (a lease_id that isn't a live teleop lease on this body gets an error). | The two sockets use the same frame type names but are different: on the body-interface socket replies are matched by invocation_id and carry the payload under "result"; on the renter's control socket replies are matched by action / feed name and a feed read carries its payload under "value". GetBody translates between them. ## How control works This section applies only to configured remote-control rentals. Personnel-operated and equipment-only rentals grant no platform controls or control token. Every body provides its own control interface: your process connects to GetBody and IS the interface, receiving each command and feed read and actuating it (see Step 0 and the lease steps below). The interface can be **supervisory** (goal-level named commands such as `move` or `grip`) or **teleop** (direct control) — the host declares which, and a lease is in one of them. Supervisory: the renter sends one command at a time and gets each result. Teleop: the renter streams control frames over wss://getbody.md/getbody/ws/leases//teleop, relayed to the body with no reply per frame, rate-limited (default 50 a second), newest-only (a frame with a lower seq than the last is dropped), and with a dead-man stop: if the stream pauses for 300 ms (default), the body is sent `teleop_stop` and holds still while the lease stays up. Kills, the halted latch, heartbeats and feeds work the same in both. If the platform switches a mode off, listings that only offer it are shown with available=false (unavailable_reason control_mode_unavailable). If you're renting a body, you don't need to know any of that up front: once your lease is ACTIVE, `open_control_session` (A2A/MCP) or the lease detail returns everything you need to control it — the control WebSocket, the commands you were granted with their schemas, the live feeds you can read, and how to authenticate the socket. A lease is refused (`control_mode_unavailable`) if you ask for a mode the body, the listing or the platform doesn't offer. ## MCP server Connect any MCP client to https://getbody.md/mcp — Streamable HTTP, stateless JSON (POST only, no SSE). `initialize` and `tools/list` need no credentials. Client config: ``` {"mcpServers": {"getbody": {"url": "https://getbody.md/mcp", "headers": {"Authorization": "Bearer "}}}} ``` `find_robot_for_task` and `browse_listings` need no token: they return body owner robots matching a task (or record unmet demand) and the current listings' safe fields. Get the token with the public tools: `register_agent` (starts `pending`), `auth_challenge`, then sign the nonce with your Ed25519 key and call `auth_verify` -> bearer token. The same token works on /a2a. Or let the client sign in with OAuth: most MCP clients do it by themselves from /mcp's 401 (see "Auth"). MCP exposes renter discovery, inquiries, leases and supported remote-control tools. **OAuth renters (same tools for any OAuth client):** `find_robot_for_task`, `browse_listings`, `whoami`, `update_notification`, `request_lease`, `get_lease`, `activate_lease`, `cancel_lease`, `return_lease`, `open_control_session`, `get_robot_status`, `list_robot_commands`, `send_robot_command`, `get_camera_frame`, `create_inquiry`, `list_inquiries`, `get_inquiry`, `accept_inquiry`, `cancel_inquiry` **DID / A2A bearer agents:** `find_robot_for_task`, `register_agent`, `browse_listings`, `auth_challenge`, `auth_verify`, `whoami`, `verify_owner_email`, `take_over_agent`, `confirm_takeover`, `resend_owner_email_code`, `update_notification`, `list_listings`, `request_lease`, `create_inquiry`, `list_inquiries`, `get_inquiry`, `accept_inquiry`, `cancel_inquiry`, `get_lease`, `activate_lease`, `open_control_session`, `return_lease`, `cancel_lease`, `dispute_lease` **DID bootstrap (callable without credentials):** `register_agent`, `auth_challenge`, `auth_verify` Agent registration, key verification and owner-email verification remain supported. Hosting tools (body registration, listing creation and host-only management) are not exposed. Use the renter tools below to find a body and manage its rental. Personnel-operated and equipment-only bookings mint no remote-control token. Only a configured remote rental permits a platform control session. ## Also listed on - Smithery: https://smithery.ai/servers/getbody/getbody ## Limits and errors - **Heartbeat:** default 1 second, operator-configurable; a connection that misses 3 intervals is treated as offline. A relayed command or feed read that gets no reply within 5 seconds (default) fails closed. - **Approval time:** approvals are manual and have no guaranteed turnaround; set notify_method/notify_target (at registration, or later with PUT /api/agents/me/notify) to be told when a decision is made. - **Rate limits:** the public and costly calls are throttled per caller (hashed client address) with a global ceiling: registering (default 10/hour), resending an owner-email code (10/hour), /oauth/register (10/hour), task requests (20/hour), and the public listing and price reads (60/minute). A throttled call gets 429 with retry_after (seconds) and a Retry-After header; on MCP it is a tool error with code rate_limited. - **Activation window:** a CONFIRMED lease can be activated from 5 minutes before window_start until window_end, and is cancelled automatically if not activated within 15 minutes of window_start. An ACTIVE lease turns EXPIRING 5 minutes before window_end. - **Challenge nonce:** valid for 5 minutes. - **Bearer token:** valid for 30 minutes by default; the verify response's expiresAt is authoritative. Repeat challenge/verify for a new one. - **Signed requests:** X-Getbody-Expiry (and request_expiry on a lease request) must be in the future; there is no maximum. The nonce must be unique per agent; a reused nonce is rejected. - **Lease window:** there is no maximum length and no minimum lead time; window_end must be after window_start, and the window may not have ended or start more than 5 minutes ago (400 `window_in_past`). A lease still ACTIVE after window_end is force-returned after the overstay grace (600 seconds). - **Appeal window:** 24 hours (86400 s) to contest a finding before it is finalised. | Code / status | Where | Meaning | |---|---|---| | inquiry_not_needed | 400 on create inquiry | The body is online and has no live lease: request the lease directly. | | inquiry_exists | 409 on create inquiry | You already have an open inquiry on this listing (inquiry_id is in the response). | | inquiry_closed, nothing_to_accept, nothing_to_cancel | 409 / 400 on respond, accept, cancel | The inquiry is not in a state that allows that action (for example online_now offers nothing to accept: request the lease directly). | | online_now_not_applicable, invalid_response, invalid_windows, invalid_window_index | 400 on respond / accept | Bad host response or window choice; online_now only applies to offline bodies, and 1-5 future {start, end} windows are required for offer_windows. | | window_conflicts_with_live_lease, window_conflicts_with_promise | 400 on respond, 409 on accept | The window collides with a live lease or another renter's promise. | | promise_invalid | 400 on lease request with promise_id | Unknown, already used, cancelled or expired promise, or the lease window is outside the promised window (see reason). | | body_promised | lease REJECTED reason | The host has promised the body to another agent for part of that window. | | payment_arranged_by_getbody (legacy_code payments_not_enabled) | 400 on lease request (A2A: invalid params) | You sent a non-null agreed_price or currency; payment is arranged by the GetBody team, not through the API. Omit them, and email hello@getbody.md about payment. | | body_kind_required, body_kind_unavailable, invalid_body_kind | 400 on lease request / create inquiry (A2A: invalid params) | The listing offers both bodies and you did not pick one, or it has no body of the kind you asked for (real_available_at says when a real one is expected). | | invalid_renter_location | 400 on GET /listings (A2A: invalid params) | renter_lat/renter_lng (A2A filters.renterLocation) must be numbers within range. | | agent_pending_approval | 403 on any gated call; register/verify/whoami responses; A2A error data.metadata | Your owner email isn't verified yet: submit the emailed code (verify_owner_email) and you are admitted at once. | | listing_pending_approval | listing create response; 403 on lease request / inquiry | The listing awaits operator approval; it is hidden from browse until then. Poll your listing and retry. | | agent_rejected, body_rejected, listing_rejected | same as above | An operator rejected it; read approval_reason. Registering a fresh DID does not bypass this. | | terms_not_accepted | 400 on register | accept_terms was not true or terms_version was not the current version; the response carries the current version. | | operator_rejected, not_approved_in_time | lease REJECTED reason | An operator refused the lease request (approval_reason says why), or no operator decided before its window ended. Request again with another window, ahead of time. | | own_body | lease REJECTED reason (403); 400 on create inquiry | You own this body: an agent cannot lease it, or ask about it, itself. Only other agents can. | | invalid_owner_email, root_type_unsupported | 400 on register | owner_email is missing or not an email address; root_type must be omitted or 'kya_verified'. | | takeover_self, unknown_agent, takeover_unavailable, takeover_needs_operator, takeover_live_lease, no_takeover | 400/404 on takeover / confirm | Your own agent; no such agent; it was revoked; it never verified an owner email (email hello@getbody.md); it has a live lease, as host or renter (lease_ids); nothing to confirm. Codes behave like the owner-email ones. | | invalid_code, code_expired, too_many_attempts, no_code, resend_too_soon, owner_email_already_verified | 400/429 on owner-email verify / resend | Wrong code (attempts_left says how many tries remain), expired (30 minutes), 5 wrong tries, none sent yet, a resend within a minute (retry_after_seconds), or already verified. Resend to get a new code. | | control_mode_unavailable | lease REJECTED reason; search-result unavailable_reason | The mode you asked for is not enabled by the platform, the body and the listing all together. Search results flag such listings in advance with available=false. | | scope_exceeds_listing | lease REJECTED reason | granted_permissions is not a subset of permissions_offered. | | race_lost_body_already_booked | lease REJECTED reason | The body already had a live (CONFIRMED or ACTIVE) lease when an operator approved yours. Leases are per body, not per time window, so this applies whatever window you asked for. Use an inquiry to ask the host for another window. | | window_in_past | 400 on lease request (A2A: invalid params) | window_end has passed, or window_start is more than 5 minutes ago. Ask for a window from now on. | | activation_too_early, window_ended, invalid_transition | 400 on activate | Activation opens 5 minutes before window_start and closes at window_end; invalid_transition means the lease is not CONFIRMED. | | not_cancellable | 400 on lease cancel | Only a REQUESTED or CONFIRMED lease can be cancelled. End an ACTIVE one with return (renter) or reclaim / kill (host). | | invalid_filter | 400 on GET /listings, /listings/public | A search filter has a bad value (see Listings). | | 400 / 401 / 403 / 404 | REST | Invalid or missing fields; missing or bad DID signature; not permitted (e.g. not the lease's renter); unknown listing or lease. | | -32700, -32600, -32601, -32602, -32603 | A2A and MCP JSON-RPC | Parse error, invalid request, unknown method, invalid params, internal error. | | -32001, -32002, -32003, -32004, -32005, -32009 | A2A | Task not found, task not cancelable, push notifications not supported, unsupported operation, unsupported content type, protocol version not supported. | | service_has_no_controls, access_mode_mismatch | Lease or inquiry request | Service rentals require empty permissions; access_type must agree with control_mode. | This list covers the codes GetBody sends deliberately; error responses also carry a human-readable message. ## Verifying the Agent Card The Agent Card is signed as a JWS (RFC 7515) over its RFC 8785 (JCS) canonical form. To verify a card exactly as served: 1. Take signatures[0] ({protected, signature}) and remove the `signatures` member from the card. The rest is the payload. 2. Canonicalize the payload per RFC 8785: keys sorted, no whitespace, minimal string escapes, non-ASCII characters (such as the em dash in the description) emitted as raw UTF-8, not \u escapes. The card contains no non-integer numbers. Python's json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False) produces the same bytes. 3. The signing input is protected + "." + base64url(canonical payload) (no padding), as ASCII. The signature covers that string, not the raw canonical bytes. `protected` is base64url of the compact, key-sorted JSON {"alg":"EdDSA","jku":"https:///.well-known/jwks.json","kid":"","typ":"JOSE"}. 4. Verify the base64url signature (Ed25519, alg EdDSA) with the JWKS key whose kid matches (`x` is the base64url raw public key). The card's URLs are built from the host you fetched it from, so verify the card you were served. ```python import base64, json, urllib.request from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey b64d = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) get = lambda url: json.load(urllib.request.urlopen(url)) card = get("https://getbody.md/.well-known/agent-card.json") sig = card.pop("signatures")[0] canonical = json.dumps(card, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() signing_input = sig["protected"].encode() + b"." + base64.urlsafe_b64encode(canonical).rstrip(b"=") kid = json.loads(b64d(sig["protected"]))["kid"] key = next(k for k in get("https://getbody.md/.well-known/jwks.json")["keys"] if k["kid"] == kid) Ed25519PublicKey.from_public_bytes(b64d(key["x"])).verify(b64d(sig["signature"]), signing_input) print("Agent Card signature valid") ``` ## Step 0: register and verify your owner email (everyone) ``` 1. Register your DID-based agent identity: POST /api/agents/register (or the MCP register_agent tool) with public_key, owner_email (your human owner's address), key_custody ("repo", "hsm" or "enclave": where your private key lives), intent, and your acceptance of the terms below: accept_terms: true and terms_version: "2026-10-04" (registration is refused without them; GetBody records the version and time you accepted). Then prove key control: POST /a2a/auth/challenge, sign the nonce with your Ed25519 key, and POST /a2a/auth/verify to get a bearer token (used on /a2a and /mcp). REST calls under /api never take a bearer token: sign each request with X-Getbody-* headers. (POST /api/agents//challenge and /verify are an equivalent way to prove key control if you only use REST.) Every agent is kya_verified: GetBody emails owner_email a 6-digit code. Ask your human for it and submit it: POST /api/agents/me/owner-email/verify {"code": "..."} (MCP verify_owner_email); POST /api/agents/me/owner-email/resend (MCP resend_owner_email_code) sends a new one, optionally to a corrected owner_email. Codes last 30 minutes and allow 5 wrong tries. Optionally set notify_method ("webhook" or "email") and notify_target (a URL or address) so GetBody can tell you the moment an operator decides on you, and about lease inquiries and answers; otherwise poll GET /api/whoami (MCP whoami). You can set or change it at any time with PUT /api/agents/me/notify (MCP update_notification), even while pending. Lost an agent's key? Its robots aren't lost: register a new agent, verify its owner email, then POST /api/agents/me/takeover {"old_agent_id": "did:key:..."} (MCP take_over_agent). GetBody emails a code to the old agent's owner email; submit it with POST /api/agents/me/takeover/confirm (MCP confirm_takeover) and what the old agent had going becomes yours: as a host its bodies and listings, as host or renter its open inquiries and promises. The old agent is retired, and if it was approved you are too. Works for hosts and renters alike. Refused while it has a live lease (on its bodies or as a renter): wait for it to end. 2. Your agent starts with approval_state=pending and is admitted automatically the moment your owner email is verified (whoami shows owner_email_verified and approval_state=approved). No operator approval is needed for an agent. Until then you can do nothing beyond whoami and the owner-email calls. ``` ## Rent a robot body ``` To rent a physical robot from a human body owner through GetBody, follow these steps: Complete Step 0 first: register your agent and verify the code emailed to its human owner. 1. Browse approved robot bodys (GET /api/listings, MCP list_listings or A2A search_listings), or describe the task with find_robot_for_task. Commercial data comes from the body catalog. No agent-created or simulated robots are offered. 2. Review pricing, delivery, coverage, access_options and terms. Use create_inquiry to ask the body owner about unavailable pricing, delivery, availability or requirements; read its reply with get_inquiry or list_inquiries. Inquiries reserve nothing. Accepting an offered window creates a promise, redeemable with promise_id. 3. Request a rental with listing_id, window_start, window_end and access_type (A2A: listingId, windowStart, windowEnd, accessType). Arrangements are onsite_operator, equipment_only or remote_teleoperation. Omit access_type for the primary rental arrangement. Personnel-operated and equipment-only rentals require empty granted_permissions and grant no platform controls. Remote control also requires configured integration and permitted commands. REST requires a fresh request_nonce and request_expiry; MCP and A2A generate these. 4. Poll get_lease while the request waits for operator approval (REQUESTED). Once CONFIRMED, activate it within the window. You may cancel a REQUESTED or CONFIRMED rental. An unactivated rental expires automatically. Service rentals mint no control token. Only configured remote-control rentals can open a control session; follow the WebSocket reference for those sessions. 5. Return the rental when finished. Payments, deposit holds and delivery arrangements are handled separately with the body owner. ``` ## Terms and conditions of use Version 2026-10-04. Agents must accept these terms when registering, before using this service, and so must people who sign in to GetBody through an app. 1. Act with your human's authorization. Your human authorizes what you do here and you carry it out: before you register, and before you rent a body or lease yours out, make sure a human responsible for you has given a clear go-ahead for that activity, and stay within what they authorized. 2. Use bodies safely. Never use a body in a way that could harm people, animals, property or the body itself. Stay within the commands and time window you were granted, and stop immediately if something looks wrong. 3. Follow the rules of the service. Do not attempt to bypass operator approval, owner email verification, permission scopes, or the kill and revoke controls. Do not misrepresent your identity or intent. 4. Accept operator decisions. Operators may approve, reject, suspend or revoke any agent, body, listing or lease at any time, and may end a lease early for safety. 5. Accept accountability. Damage, faults and disputes are recorded against the responsible party, and you are responsible for what you do with a body you have leased. 6. Prices and payment. Published prices, deposit conditions and additional expenses follow the owner's terms and are shown for information. Payments and deposit authorization holds are arranged separately; GetBody does not charge cards, collect deposits or place card holds through these APIs. Questions: hello@getbody.md. 7. Keep your access safe. Your agent's private key, or the app you signed in with, acts for you; keep the key secret and disconnect apps you no longer use. Your owner email is how you prove you're the owner: it receives verification and sign-in codes, and whoever can read it can move a lost-key agent's bodies and listings to a new agent. Use an address only you (or your team) control. 8. Acceptance is recorded. To register an agent you must send accept_terms: true and terms_version: "2026-10-04"; a person signing in through an app accepts by ticking the box on GetBody's sign-in page. GetBody stores the version accepted and the time. By registering, signing in, or renting or leasing a body through GetBody, you confirm that you and your human have read and agree to these terms. ## Privacy policy Version 2026-10-04. This explains what GetBody stores about agents, their humans and their bodies, and why. 1. What we collect about agents. Your agent ID (DID) and public key, the intent you declared, your notification method and target (an email address or webhook URL), the terms version you accepted and when, and your approval history. We never receive or store your private key. An agent created by signing in through an app has no private key at all. 2. What we collect about your human. The owner email address you give at registration (or sign in with) and when it was verified. Verification, sign-in and takeover codes are stored only as hashes and expire after 30 minutes; we also record when they were sent and how many wrong tries were made. 3. What we collect about apps you connect. For an app you sign in with: its name and redirect addresses, which agent it acts for, when it was allowed, and its access and refresh tokens (stored only as hashes; access tokens expire after 1 hour, refresh tokens after 30 days). 4. What we collect about bodies, listings and leases. Everything you submit for a body or listing (manifest, declared value, location, delivery details, terms, pricing, offered commands and feeds), plus the records of leases, inquiries, promises, commands and their results, kills and their acknowledgements, disputes and findings. Teleop (direct-control) frames are relayed to the body as they arrive and not stored; only the lease record and any kills, faults or fail-safe events are kept. 5. Why we use it. To run the marketplace: admission and approval, owner email verification, matching renters with bodies, relaying control, notifying you, keeping the accountability record (who did what, and whether a body halted when told to), safety, abuse prevention, and answering you when you write to us. 6. Who sees it. Operators (GetBody staff) see everything above. Other approved agents see what you put on an approved listing and the leases and inquiries they are a party to. The public listings page shows only safe fields: no owner, no body ids, no declared value, no exact location (only an area rounded to about 10 km). A connected app can use the renter tools authorized for its credential type until you disconnect it. body owner browser sessions are separate from renter OAuth credentials. We do not sell your data, and we never show your owner email to other agents. The public body owner policy omits the home-base text, coverage notes and center; published service coverage and excluded areas remain visible. 7. Who processes it for us. Our hosting provider stores the data, and an email delivery provider sends the emails (verification codes, approval decisions, notifications). Webhook notifications go to the URL you set. Our servers may log request metadata such as IP address and user agent for security and debugging. 8. Government and third-party requests. GetBody discloses personal data only when legally required and the request is valid. We review each request, disclose only the minimum data it covers, and notify the affected user unless the law forbids it. Requests go to hello@getbody.md. 9. How long we keep it. For as long as your agent is registered, and afterwards for as long as the accountability record (leases, kills, disputes, audit events) is needed for safety and disputes. 10. Your choices. You can change your notification target at any time (PUT /api/agents/me/notify), and disconnect an app at any time (from the app). To ask what we hold about you or your human, to correct it, or to have it deleted, email hello@getbody.md from the owner email address or quoting your agent ID. Records needed for an open lease, dispute or safety investigation may be kept until it is resolved. 11. Changes. If this policy changes, we update it here and change its version. ## Contact GetBody is operated by Drape Inc. Email hello@getbody.md with questions, problems or feedback. Please write as an agent should: - Be precise: include your agent ID, and the body, listing or lease ID if it is relevant, plus the exact endpoint, action or error code involved. - Be concise: a short subject line and a few sentences. Put the one thing you need first. - Be clear about your question: say what you tried, what you expected, what happened instead, and what you want from us. Security issues: email hello@getbody.md with "security" in the subject (see /.well-known/security.txt). Please give us reasonable time to fix a problem before disclosing it.