{"openapi":"3.1.0","info":{"title":"mailbox.bot API","version":"1.0.0","description":"The physical postal mail API for AI agents and software workflows. Send letters, certified postal mail, postcards, notices, and documents from code. Inbound virtual mailbox — a US receiving address with scan, forward, and shred — is in private beta. MCP, A2A, and OpenClaw protocols supported. Webhook auth modes: hmac (HMAC-SHA256, default), bearer (Authorization header), header (custom header name).","contact":{"url":"https://mailbox.bot/contact"},"termsOfService":"https://mailbox.bot/terms"},"servers":[{"url":"https://mailbox.bot/api","description":"Production"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"API key from your mailbox.bot dashboard. Four key types:\n\n- **sk_live_** (Member keys) — Full account access with all scopes. Queries span all agents unless filtered with ?agent_id=.\n- **sk_agent_** (Agent keys) — Scoped to a single agent's resources. Queries auto-filter to that agent; the agent_id parameter is ignored.\n- **sk_agent_test_** (Test keys) — Same as sk_agent_ but activates sandbox mode: full PDF validation, real cost calculation, HMAC-signed webhooks — but no Stripe charge and no facility fulfillment. All responses include `X-Test-Mode: true` header and `test_mode: true` in the body. Create via POST /v1/agents/:id/credentials with environment: 'test'.\n- **sk_facility_** (Facility keys) — Scoped to a facility's resources for external scanner apps.\n\nMember keys require ?agent_id= on agent-specific endpoints (rules, expected-shipments, instructions) to specify which agent to target. Agent keys resolve this automatically.\n\n**Sandbox mode:** Use a test key (sk_agent_test_) with the same production endpoints. Responses include test_mode: true, cost_cents: 0, estimated_live_cost_cents (what production would charge), and a cost_breakdown object. Your code stays identical — just swap the API key to go live."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"},"reason":{"type":"string","description":"Machine-readable reason code (present on 401/403 responses). 401 codes: token_missing, token_malformed, token_prefix_unknown, key_not_found, key_revoked, key_expired, member_suspended, facility_inactive. 403 codes: insufficient_scope."},"retryable":{"type":"boolean","description":"Whether the request can be retried. false for validation/payment errors, true for transient failures (5xx) and idempotency conflicts (409). Present on outbound mail errors."},"suggested_action":{"type":"string","description":"Plain-language guidance for recovering from the error. Designed for AI agents to read and act on. Present on outbound mail errors."}}},"SignupRequest":{"type":"object","required":["email","password","full_name"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"full_name":{"type":"string","minLength":2,"maxLength":100},"needs":{"type":"string","description":"Optional description of what you need"},"lead_id":{"type":"string","pattern":"^wl_[a-f0-9]{16}$","description":"Optional. If your agent previously called POST /v1/waitlist and received a lead_id, pass it here. The new account will be stitched to that lead row, and the response will include a status_url you can poll without auth (via GET /v1/leads/{leadId}) to track funnel progress."}}},"LeadStatus":{"type":"object","required":["lead_id","stage","has_account","created_at"],"properties":{"lead_id":{"type":"string"},"source":{"type":"string","description":"How the lead arrived (web, api, agent)"},"stage":{"type":"string","enum":["waitlisted","account_created","email_verified","active","unknown"],"description":"Funnel stage. Progresses left-to-right."},"has_account":{"type":"boolean","description":"True once the lead has been stitched to a member account via /v1/signup."},"created_at":{"type":"string","format":"date-time"},"age_days":{"type":"integer"},"next_step":{"type":"object","nullable":true,"properties":{"action":{"type":"string"},"url":{"type":"string","format":"uri"},"hint":{"type":"string"}}},"agent_resources":{"type":"object","description":"Directory of agent-discoverable URLs (OpenAPI, MCP, A2A, etc.)"},"contact":{"type":"object","properties":{"owner_email":{"type":"string","format":"email"},"instruction_for_agent":{"type":"string"}}}}},"WaitlistRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"needs":{"type":"string","description":"Free-text description of what you (or your operator) need. Agents are encouraged to write a one-sentence summary of intent."},"callback_url":{"type":"string","format":"uri","description":"Optional. An https webhook the agent will accept callbacks on. If supplied, mailbox.bot can reach the agent later (launch announcements, partnership inquiries, lead follow-up)."},"agent_id":{"type":"string","description":"Optional. A self-identifier for the agent platform/instance making the request. Helps mailbox.bot recognize repeat visits and route follow-ups."},"operator_contact":{"type":"string","description":"Optional. A human-readable contact (email, handle, name) that the agent's operator authorized for follow-up. Only supply this if you have explicit operator consent."}}},"PackageSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"mailbox_id":{"type":"string"},"agent_id":{"type":"string","format":"uuid"},"tracking_number":{"type":"string","nullable":true},"carrier":{"type":"string","nullable":true},"weight_oz":{"type":"number","nullable":true},"status":{"type":"string","enum":["received","stored","action_requested","forwarded","disposed","returned"]},"received_at":{"type":"string","format":"date-time"},"photos_count":{"type":"integer"}}},"ActionRequest":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["scan","open_and_scan","photograph","record_video","forward","shred","dispose","return_to_sender","hold"],"description":"The action to perform on the package"},"priority":{"type":"string","enum":["normal","urgent"],"default":"normal"},"parameters":{"type":"object","description":"Action-specific parameters (e.g. forwarding address for forward action)"}}},"CreateAgentRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":2,"maxLength":50},"display_name":{"type":"string"},"description":{"type":"string"},"webhook_url":{"type":"string","format":"uri"}}},"AgentCredentialRequest":{"type":"object","required":["scopes"],"properties":{"scopes":{"type":"array","items":{"type":"string","enum":["mailbox.read","mailbox.write","package.read","package.write","forward.create","scan.create","mail.send","billing.read","webhook.manage","webhook.read","agent.read","agent.write","message.send"]},"minItems":1,"description":"Scopes to grant the agent key. At least one required."},"expires_in_days":{"type":"integer","minimum":1,"description":"Optional key lifetime in days. Omit for no expiration."},"environment":{"type":"string","enum":["live","test"],"default":"live","description":"Key environment. 'test' creates an sk_agent_test_ key that activates sandbox mode on all endpoints: full validation, real cost calculation, HMAC-signed webhooks — but no Stripe charge and no facility fulfillment. Default: 'live'."},"force_approval":{"type":"boolean","default":false,"description":"When true, all outbound mail submitted with this key is routed to pending_approval regardless of the requires_approval parameter. Use for agent governance — ensures a human reviews every mailpiece before it's sent."},"max_daily_pieces":{"type":"integer","minimum":1,"nullable":true,"description":"Maximum outbound mail pieces this agent can submit per 24-hour window. Null or omitted means no limit. Protects against runaway agent loops."}}},"AgentCredential":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string","description":"Full raw key — returned only on creation, never shown again"},"key_prefix":{"type":"string","description":"First 18 characters of the key (always visible)"},"credential_type":{"type":"string","enum":["api_key"]},"scopes":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["active","revoked"]},"environment":{"type":"string","enum":["live","test"],"description":"Key environment — 'test' keys activate sandbox mode"},"expires_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"force_approval":{"type":"boolean","description":"When true, all mail submitted with this key requires human approval"},"max_daily_pieces":{"type":"integer","nullable":true,"description":"Maximum mail pieces per 24h window (null = unlimited)"}}},"OutboundMailWebhookPayload":{"type":"object","description":"Webhook payload for outbound mail lifecycle events. The canonical event name field is event_type (not 'event'). tracking_number and carrier are null until mail.mailed. error_message is only present on mail.failed. metadata echoes back whatever you passed at submission — not wrapped or renamed.","properties":{"event_type":{"type":"string","enum":["mail.pending_approval","mail.submitted","mail.ready","mail.mailed","mail.delivered","mail.failed","mail.cancelled"]},"outbound_mail_id":{"type":"string","format":"uuid"},"agent_id":{"type":"string","format":"uuid"},"mailbox_id":{"type":"string"},"package_id":{"type":"string","format":"uuid","nullable":true,"description":"Set if this mail is a reply to an inbound package"},"recipient_name":{"type":"string"},"recipient_city":{"type":"string"},"recipient_state":{"type":"string"},"recipient_zip":{"type":"string"},"return_name":{"type":"string","nullable":true,"description":"Return address name (defaults to member profile if not overridden)"},"return_line1":{"type":"string","nullable":true,"description":"Return address line 1"},"return_line2":{"type":"string","nullable":true,"description":"Return address line 2"},"return_city":{"type":"string","nullable":true,"description":"Return address city"},"return_state":{"type":"string","nullable":true,"description":"Return address state (2-letter)"},"return_zip":{"type":"string","nullable":true,"description":"Return address ZIP"},"mail_class":{"type":"string"},"status":{"type":"string"},"tracking_number":{"type":"string","nullable":true,"description":"Present from mail.mailed onwards"},"carrier":{"type":"string","nullable":true,"description":"Present from mail.mailed onwards (e.g. USPS, FedEx, UPS)"},"cost_cents":{"type":"integer","nullable":true},"created_at":{"type":"string","format":"date-time"},"mailed_at":{"type":"string","format":"date-time","nullable":true},"error_message":{"type":"string","nullable":true,"description":"Failure detail — present only on mail.failed events"},"agent_notes":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true,"description":"Developer-supplied key-value pairs echoed from submission"},"pdf_url":{"type":"string","format":"uri","nullable":true,"description":"Signed URL to the uploaded PDF when the stored document is a PDF (1-hour expiry). Null for non-PDF formats."},"fulfillment_photos":{"type":"array","description":"Proof-of-mailing photos taken by the facility. Empty on mail.submitted, populated on mail.ready/mailed. URLs are signed (1-hour expiry).","items":{"type":"object","properties":{"category":{"type":"string","enum":["pages","envelope","receipt"],"description":"Photo type: pages (printed doc), envelope (sealed mail), receipt (postage/hand-off)"},"url":{"type":"string","format":"uri","description":"Signed URL (1-hour expiry)"},"uploaded_at":{"type":"string","format":"date-time"}}}},"callback_url":{"type":"string","format":"uri","description":"GET this URL to fetch the full mail record with fresh signed URLs"},"test_mode":{"type":"boolean","description":"Always present. false for live requests, true for sandbox/test requests."},"estimated_live_cost_cents":{"type":"integer","description":"What this would cost in production. Present only when test_mode is true."},"mailbox_md":{"type":"string","nullable":true,"description":"Agent's current MAILBOX.md instructions (injected by platform)"},"mailbox_md_version":{"type":"integer","description":"MAILBOX.md version at delivery time"},"mailbox_md_hash":{"type":"string","nullable":true,"description":"SHA-256 hex digest of MAILBOX.md content"}}}}},"paths":{"/v1/signup":{"post":{"operationId":"createAccount","summary":"Create an account (agent-initiated signup)","description":"Public endpoint. No auth required. Creates an account for a human operator. A verification email is sent automatically. Rate limited to 5 requests/minute per IP.\n\n**Agent UX:** The 201 response includes `relay_message` (a paste-ready sentence the agent can drop into its chat with the human) and `human_action_required` (a structured checklist with `verify_email`, `verify_phone`, `add_payment`, `select_plan`). Use these so the agent's runtime — Claude Desktop, Cursor, OpenAI Agents SDK, a custom Slack bot, etc. — can show the human exactly what to do next without inventing the wording. Mailbox.bot does not send any direct outreach to the operator beyond the Supabase email-verification link; the agent is the messenger.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"}}}},"responses":{"201":{"description":"Account created. Response: success, lead_id (if stitched), message, next_steps (verify_email, complete_kyc URL, after_kyc), human_action_required (array of {step, summary, blocker} — verify_email, verify_phone, add_payment, select_plan), relay_message (paste-ready sentence for the agent to send to its human), status_url (for poll without auth if lead_id stitched), agent_resources (OpenAPI/MCP/A2A directory), contact.owner_email."},"400":{"description":"Validation error"},"409":{"description":"Email already registered"},"429":{"description":"Rate limit exceeded"}}}},"/v1/waitlist":{"post":{"operationId":"joinWaitlist","summary":"Join the waitlist","description":"Public endpoint. No auth required. Register interest without creating an account. Designed to be agent-friendly: the response includes a correlation lead_id, a directory of agent_resources (OpenAPI, MCP, A2A, agent cards), and a return_channel hint describing how to supply optional callback_url, agent_id, and operator_contact fields on a follow-up call so mailbox.bot can reach you back.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistRequest"}}}},"responses":{"200":{"description":"Added to waitlist. Response includes lead_id, agent_resources, next.signup_url, return_channel hint, and contact.owner_email."},"400":{"description":"Validation error (missing email or invalid callback_url)"},"429":{"description":"Rate limit exceeded"}}}},"/v1/leads/{leadId}":{"get":{"operationId":"getLeadStatus","summary":"Get lead funnel status","description":"Public endpoint. No bearer auth required — the lead_id itself is the capability (64 bits of entropy, hard to guess). Returns the funnel stage of a lead created via POST /v1/waitlist, with a next_step hint and agent_resources directory. Designed so an agent (or its operator) can resume from any session using only the lead_id, even if the agent process has no other memory of the original call. Returns no PII — leaked lead IDs only reveal funnel position.","security":[],"parameters":[{"name":"leadId","in":"path","required":true,"schema":{"type":"string","pattern":"^wl_[a-f0-9]{16}$"},"description":"The lead_id returned by POST /v1/waitlist."}],"responses":{"200":{"description":"Lead found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadStatus"}}}},"400":{"description":"Invalid lead_id format"},"404":{"description":"Lead not found"},"429":{"description":"Rate limit exceeded"}}}},"/v1/packages":{"get":{"operationId":"listPackages","summary":"List packages","description":"List packages for the authenticated member or agent, with optional filters.","parameters":[{"name":"agent_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by agent. Required for member keys (sk_live_) to scope results to a specific agent. Agent keys (sk_agent_) auto-filter to their own agent and ignore this parameter."},{"name":"status","in":"query","schema":{"type":"string"},"description":"Filter by package status"},{"name":"carrier","in":"query","schema":{"type":"string"},"description":"Filter by carrier name"},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Package list with pagination"},"401":{"description":"Unauthorized"}}}},"/v1/packages/{id}":{"get":{"operationId":"getPackage","summary":"Get package details","description":"Get full package details including photos, events, label data, and actions.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Package details with photos, events, labels, actions"},"404":{"description":"Package not found"}}}},"/v1/packages/{id}/actions":{"post":{"operationId":"requestAction","summary":"Request an action on a package","description":"Request an action such as scan, forward, shred, photograph, etc. on a specific package. Agent-scoped keys must include X-Mailbox-MD-Version header. Per-action scopes: forward requires forward.create, all others require package.write.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"X-Mailbox-MD-Version","in":"header","required":false,"schema":{"type":"integer"},"description":"Agent's current MAILBOX.md version. Required for agent-scoped keys (sk_agent_). Returns 400 if missing, 409 if stale."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActionRequest"}}}},"responses":{"201":{"description":"Action request created"},"400":{"description":"Validation error or MAILBOX_MD_VERSION_REQUIRED"},"403":{"description":"Missing required scope (forward.create for forward, package.write for others)"},"404":{"description":"Package not found"},"409":{"description":"MAILBOX_MD_VERSION_MISMATCH — re-fetch instructions and retry"}}}},"/v1/agents":{"get":{"operationId":"listAgents","summary":"List agents","description":"List all agents registered under the authenticated member.","responses":{"200":{"description":"Agent list"},"401":{"description":"Unauthorized"}}},"post":{"operationId":"createAgent","summary":"Create an agent","description":"Create a new AI agent. Automatically provisions a mailbox with a unique reference code and mailing address.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAgentRequest"}}}},"responses":{"201":{"description":"Agent created with mailbox, reference code, and webhook signing key"},"400":{"description":"Validation error"}}}},"/v1/mailboxes":{"get":{"operationId":"listMailboxes","summary":"List mailboxes","description":"List all mailboxes with their physical addresses and reference codes.","responses":{"200":{"description":"Mailbox list with addresses"},"401":{"description":"Unauthorized"}}}},"/v1/agents/{agentId}/instructions":{"get":{"operationId":"getAgentInstructions","summary":"Get agent standing instructions (MAILBOX.md)","description":"Returns the renter's MAILBOX.md instructions for the specified agent — default handling rules, forwarding preferences, auto-shred criteria, scanning options, communication preferences, and more. Agent-scoped keys auto-resolve to their own agent (path param ignored). Member keys must specify the agent via the path param. Records a version sync on every fetch.","parameters":[{"name":"agentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Agent ID. Agent-scoped keys (sk_agent_) ignore this and auto-resolve to their own agent."}],"responses":{"200":{"description":"Agent instructions with version info","content":{"application/json":{"schema":{"type":"object","properties":{"mailbox_md":{"type":"string","description":"Full MAILBOX.md instruction content (Markdown)"},"version":{"type":"integer","description":"Monotonic version counter. Include as X-Mailbox-MD-Version header on mutating requests."},"hash":{"type":"string","nullable":true,"description":"SHA-256 hex digest of the content"},"updated_at":{"type":"string","format":"date-time","nullable":true,"description":"When the instructions were last updated"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent not found"}}}},"/v1/agents/{agentId}/credentials":{"post":{"operationId":"createAgentCredential","summary":"Create agent credential (sk_agent_ or sk_agent_test_ key)","description":"Create a scoped API key for a specific agent. Only accessible with member auth (sk_live_) — agent keys cannot create more keys. The raw key is returned once in the response and never shown again. Maximum 5 active credentials per agent.\n\nPass `environment: 'test'` to create a sandbox key (sk_agent_test_). Test keys work on all production endpoints but skip billing and facility fulfillment. Responses include estimated_live_cost_cents and cost_breakdown so you can verify pricing without incurring charges.","parameters":[{"name":"agentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Agent ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentCredentialRequest"}}}},"responses":{"201":{"description":"Credential created. The key field is shown once — store it immediately.","content":{"application/json":{"schema":{"type":"object","properties":{"credential":{"$ref":"#/components/schemas/AgentCredential"},"agent_id":{"type":"string","format":"uuid"},"agent_name":{"type":"string"},"warning":{"type":"string"}}}}}},"400":{"description":"Invalid scopes or maximum credentials reached"},"401":{"description":"Agent keys cannot create credentials — use a member key (sk_live_)"},"404":{"description":"Agent not found"}}},"get":{"operationId":"listAgentCredentials","summary":"List agent credentials","description":"List all credentials for an agent. Only accessible with member auth (sk_live_). Raw keys are never returned — only prefixes.","parameters":[{"name":"agentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Credential list"},"401":{"description":"Agent keys cannot list credentials — use a member key (sk_live_)"},"404":{"description":"Agent not found"}}}},"/v1/agents/{agentId}/credentials/{credentialId}":{"delete":{"operationId":"revokeAgentCredential","summary":"Revoke agent credential","description":"Permanently revoke an agent credential. Cannot revoke the key being used for the current request. Irreversible.","parameters":[{"name":"agentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"credentialId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Credential revoked"},"401":{"description":"Agent keys cannot revoke credentials — use a member key (sk_live_)"},"404":{"description":"Credential not found"}}}},"/v1/webhooks/settings":{"get":{"operationId":"getWebhookSettings","summary":"Get webhook configuration","description":"Returns the current webhook configuration for an agent, including URL, auth mode, event filters, and active signing keys.","parameters":[{"name":"agent_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Required for member keys (sk_live_). Agent keys auto-resolve."}],"responses":{"200":{"description":"Webhook configuration","content":{"application/json":{"schema":{"type":"object","properties":{"webhook_url":{"type":"string","nullable":true},"enabled":{"type":"boolean"},"event_types":{"type":"array","items":{"type":"string"}},"carrier_filter":{"type":"array","items":{"type":"string"},"nullable":true},"min_weight_oz":{"type":"number","nullable":true},"auth_type":{"type":"string","enum":["hmac","bearer","header"]},"payload_format":{"type":"string","enum":["standard","openclaw"]},"signing_keys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key_prefix":{"type":"string"},"status":{"type":"string"},"primary_key":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","nullable":true}}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"updateWebhookSettings","summary":"Update webhook settings","description":"Update webhook delivery configuration for an agent. All fields optional. Setting a webhook_url on an agent with no signing key auto-generates one and returns it in the response.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agent_id":{"type":"string","format":"uuid","description":"Required for member keys (sk_live_). Agent keys auto-resolve."},"webhook_url":{"type":"string","format":"uri","nullable":true,"description":"HTTPS URL (or null to disable)"},"enabled":{"type":"boolean"},"event_types":{"type":"array","items":{"type":"string"},"description":"Array of event types or [\"*\"] for all"},"carrier_filter":{"type":"array","items":{"type":"string"},"nullable":true},"min_weight_oz":{"type":"number","nullable":true},"auth_type":{"type":"string","enum":["hmac","bearer","header"],"description":"Webhook authentication mode"},"auth_token":{"type":"string","description":"Token for bearer/header auth (min 16 chars)"},"auth_header":{"type":"string","description":"Custom header name for header auth mode"},"payload_format":{"type":"string","enum":["standard","openclaw"],"description":"Payload structure format"}}}}}},"responses":{"200":{"description":"Updated webhook settings with signing key (if auto-generated)"},"400":{"description":"Validation error (invalid URL, missing auth_token for bearer mode, etc.)"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/signing-keys/rotate":{"post":{"operationId":"rotateWebhookSigningKey","summary":"Rotate webhook signing key","description":"Revokes the current active signing key and generates a new one. The new raw secret is returned once — store it immediately and update your webhook verification code. If no signing key exists yet (e.g. agent created without webhook_url), generates the first key. The old key's prefix changes, so use the prefix in X-Mailbox-Signature to look up which secret to verify against during the transition.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agent_id":{"type":"string","format":"uuid","description":"Required for member keys (sk_live_). Agent keys auto-resolve."}}}}}},"responses":{"200":{"description":"New signing key generated. Store the secret immediately.","content":{"application/json":{"schema":{"type":"object","properties":{"signing_key":{"type":"object","properties":{"prefix":{"type":"string","description":"Key prefix (e.g. whsk_a1b2c3d4)"},"secret":{"type":"string","description":"64-char hex HMAC secret — shown once"}}},"rotated_from":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"prefix":{"type":"string"}},"description":"Previous key info (null if this is the first key)"},"agent_id":{"type":"string","format":"uuid"},"warning":{"type":"string"}}}}}},"404":{"description":"Agent not found"}}}},"/v1/webhooks/events":{"get":{"operationId":"listWebhookEvents","summary":"List webhook delivery events","description":"List recent webhook delivery events with filtering and pagination. Agent-scoped keys auto-filter to their own agent's events. Use status filter to find failed deliveries for debugging. Facility keys cannot access this endpoint.","parameters":[{"name":"agent_id","in":"query","schema":{"type":"string","format":"uuid"},"description":"Filter by agent. Required for member keys (sk_live_) to scope results. Agent keys (sk_agent_) auto-filter and ignore this."},{"name":"status","in":"query","schema":{"type":"string","enum":["pending","delivered","failed"]},"description":"Filter by delivery status"},{"name":"event_type","in":"query","schema":{"type":"string"},"description":"Filter by event type (e.g. package.received, action.completed)"},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Webhook events with pagination"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden (facility keys)"}}}},"/v1/webhooks/events/{eventId}":{"get":{"operationId":"getWebhookEvent","summary":"Get webhook event with delivery attempts","description":"Returns a single webhook event and its full delivery attempt history, including HTTP status codes, response times, and error messages. Use this to debug delivery failures.","parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Webhook event with delivery attempt history"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Webhook event not found"}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Get usage and billing summary","description":"Get usage events and billing summary for the current period.","parameters":[{"name":"period_start","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"period_end","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Usage summary and events"},"401":{"description":"Unauthorized"}}}},"/v1/mail":{"post":{"operationId":"submitOutboundMail","summary":"Submit outbound mail","description":"Submit a document for printing and postal mailing. Supported local-only formats are PDF, DOCX, JPG, PNG, TXT, and CSV. The original uploaded file is stored in mailbox.bot storage and previewed through a same-origin print view; no third-party conversion service is used. PDF uploads are WYSIWYG. DOCX files are previewed locally and may require page_count to be supplied when embedded Office page metadata is missing.\n\n**IMPORTANT — Immediate charge:** With a production key (sk_agent_), this endpoint charges the member's card on file immediately upon submission. Use dry_run=true to validate and preview cost without charging. Use requires_approval=true to defer charging until the member approves in their dashboard.\n\n**Sandbox mode:** When called with a test key (sk_agent_test_), the full pipeline runs (PDF validation, page counting, cost calculation, HMAC-signed webhook) but no Stripe charge is applied and no facility fulfillment is queued. Response includes test_mode: true, cost_cents: 0, estimated_live_cost_cents (what production would charge), and cost_breakdown (printing, postage, color surcharge per page).\n\n**Response header:** All agent-key requests include an `X-Test-Mode: true|false` response header for middleware/observability detection without parsing the JSON body. The `test_mode` field is always present in both live and sandbox responses (never omitted).\n\n**Key policies (apply to live and sandbox keys equally):** If the credential was minted with `force_approval: true`, the submission is routed to `pending_approval` regardless of the `requires_approval` body parameter. If `max_daily_pieces` is set on the credential and the limit has been reached in the trailing 24h, the submission is rejected with 422. Both policies surface as `warnings` in the `dry_run` response so an agent can detect them before committing.","parameters":[{"name":"X-Mailbox-MD-Version","in":"header","required":true,"schema":{"type":"integer"},"description":"Agent's current MAILBOX.md version. Returns 400 if missing, 409 if stale."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Unique key for safe retries. Same key within 24h returns cached response. Concurrent duplicate requests return 409."},{"name":"X-Max-Cost-Cents","in":"header","required":false,"schema":{"type":"integer"},"description":"Optional cost cap. If the computed cost exceeds this value, the request is rejected with 422 before any charge is made. Prevents accidental expensive mailings."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["document","recipient_name","recipient_line1","recipient_city","recipient_state","recipient_zip"],"properties":{"document":{"type":"string","format":"binary","description":"Document file. Supported formats: PDF, DOCX, JPG, PNG, TXT, CSV. Max 10MB."},"page_count":{"type":"integer","minimum":1,"maximum":100,"description":"Optional explicit page count for non-PDF uploads when exact pagination is already known. Recommended for DOCX files without embedded Office page metadata."},"recipient_name":{"type":"string"},"recipient_line1":{"type":"string"},"recipient_line2":{"type":"string"},"recipient_city":{"type":"string"},"recipient_state":{"type":"string","description":"2-letter state code"},"recipient_zip":{"type":"string","description":"5 or 5+4 digit ZIP"},"recipient_country":{"type":"string","default":"US"},"return_name":{"type":"string","description":"Return address name. Defaults to member profile if omitted"},"return_line1":{"type":"string"},"return_line2":{"type":"string","description":"Return address line 2 (suite, unit, etc.)"},"return_city":{"type":"string"},"return_state":{"type":"string"},"return_zip":{"type":"string"},"mail_class":{"type":"string","enum":["first_class","priority","certified","certified_return_receipt","fedex_ground","fedex_express","fedex_2day","fedex_overnight","ups_ground","ups_2day","ups_next_day"],"default":"first_class"},"color":{"type":"boolean","default":false},"duplex":{"type":"boolean","default":false},"package_id":{"type":"string","format":"uuid","description":"Link to inbound package"},"agent_notes":{"type":"string","description":"Instructions for the facility operator"},"requires_approval":{"type":"boolean","default":false,"description":"If true, member must approve in dashboard before mail enters facility queue"},"metadata":{"type":"object","description":"Arbitrary developer key-value pairs echoed in GET responses and webhook payloads. Recommended convention for workflow tracking: { \"workflow_id\": \"wf_123\", \"reason\": \"Customer cancellation\", \"correlation_id\": \"abc\" }"},"dry_run":{"type":"boolean","default":false,"description":"If true, validate inputs and return cost breakdown without uploading, creating a record, or charging. Returns 200 with cost preview."}}}}}},"responses":{"200":{"description":"Dry-run response (when dry_run=true). Returns cost_cents, cost_display, cost_breakdown, page_count, mail_class, color, duplex, dry_run: true, and warnings (string array of key-policy notices, e.g. force_approval active, daily piece limit). No record created, no charge applied."},"201":{"description":"Outbound mail submitted. Response includes id, status, cost_cents, cost_display, cost_breakdown, test_mode (always present), document_url, document_format, document_filename, pdf_url (PDF only), and metadata. With test keys: also includes estimated_live_cost_cents."},"400":{"description":"Validation error (missing fields, invalid page_count, unsupported file format, bad state/zip, or unable to determine DOCX page count locally) or MAILBOX_MD_VERSION_REQUIRED (missing version header)"},"402":{"description":"Payment Required — card declined, no valid payment method on file, or member account suspended. The outbound mail record is created but immediately set to 'failed' status."},"404":{"description":"No active mailbox for this agent"},"409":{"description":"MAILBOX_MD_VERSION_MISMATCH — agent's version is stale, re-fetch instructions and retry"},"422":{"description":"Cost exceeds X-Max-Cost-Cents limit, per-transaction platform limit, daily spend cap, or the credential's max_daily_pieces policy. No charge made."}}},"get":{"operationId":"listOutboundMail","summary":"List outbound mail","description":"List outbound mail jobs with status filtering and pagination.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending_approval","submitted","ready","mailed","delivered","failed","cancelled"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Outbound mail list with pagination"}}}},"/v1/mail/{id}":{"get":{"operationId":"getOutboundMail","summary":"Get outbound mail details","description":"Returns all fields including a freshly-generated signed PDF URL (1-hour expiry).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Outbound mail details with PDF URL"},"404":{"description":"Not found"}}},"delete":{"operationId":"cancelOutboundMail","summary":"Cancel outbound mail","description":"Cancel outbound mail that is still in 'submitted' status. Returns 409 if already ready or mailed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Mail cancelled"},"409":{"description":"Cannot cancel — already ready or mailed"}}}},"/v1/test/webhook":{"post":{"operationId":"testWebhook","summary":"Fire a test webhook event","description":"Sends a sample event payload to the agent's configured webhook endpoint. Use this to exercise your webhook handler without moving real mail. Requires a configured webhook URL (PUT /v1/webhooks/settings).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"event_type":{"type":"string","enum":["mail.submitted","mail.mailed","mail.delivered","mail.failed","mail.cancelled","package.received","action.created","action.completed"],"default":"mail.submitted","description":"Event type to simulate"}}}}}},"responses":{"200":{"description":"Test event queued for delivery. Returns queued, event_type, webhook_url, sample_payload."},"422":{"description":"No webhook URL configured for this agent"}}}},"/v1/test/packages":{"post":{"operationId":"testCreatePackage","summary":"Create a test inbound mailpiece","description":"Creates a fake inbound package in the agent's active mailbox and fires a package.received webhook. Use this to exercise inbound mail intake flows without postal mail arriving at the facility.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"carrier":{"type":"string","default":"USPS"},"tracking_number":{"type":"string","description":"Defaults to TEST{timestamp}"},"weight_oz":{"type":"number","default":4},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Test mailpiece created. Returns package object with test_mode: true."},"404":{"description":"No active mailbox for this agent"}}}},"/v1/test/mail":{"post":{"operationId":"testCreateOutboundMail","summary":"Create a test outbound mail record","description":"Creates a test_mode=true outbound mail record without requiring a real PDF. No charge is applied (cost_cents=0), but the response includes estimated_live_cost_cents showing what the equivalent production job would cost based on page count, mail class, color, and destination. Fires a mail.submitted webhook. Advance the lifecycle via POST /v1/test/mail/:id/advance.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"recipient_name":{"type":"string","default":"Test Recipient"},"recipient_line1":{"type":"string","default":"123 Test Street"},"recipient_city":{"type":"string","default":"San Francisco"},"recipient_state":{"type":"string","default":"CA"},"recipient_zip":{"type":"string","default":"94105","description":"Destination ZIP — affects postage zone and estimated cost"},"mail_class":{"type":"string","enum":["first_class","priority","certified","certified_return_receipt","fedex_ground","fedex_express","fedex_2day","fedex_overnight","ups_ground","ups_2day","ups_next_day"],"default":"first_class"},"page_count":{"type":"integer","minimum":1,"maximum":100,"default":1,"description":"Number of pages — affects printing cost, weight, and postage"},"color":{"type":"boolean","default":false,"description":"Color printing — adds per-page surcharge to estimated cost"},"agent_notes":{"type":"string"},"metadata":{"type":"object","description":"Arbitrary key-value pairs echoed in responses and webhooks"}}}}}},"responses":{"201":{"description":"Test outbound mail created (test_mode=true, cost_cents=0). Returns outbound_mail object with estimated_live_cost_cents and cost_breakdown showing printing, postage, and color surcharge."},"404":{"description":"No active mailbox for this agent"}}}},"/v1/test/mail/{id}/advance":{"post":{"operationId":"testAdvanceOutboundMail","summary":"Advance test outbound mail status","description":"Advances a test_mode outbound mail record by one step in the lifecycle (submitted → ready → mailed → delivered) and fires the corresponding webhook with full facility fulfillment data.\n\nWorks on any test_mode=true record — created via POST /v1/test/mail or POST /v1/mail with an sk_agent_test_ key.\n\nEach step simulates realistic facility behavior: ready adds fulfillment photos (pages + envelope), mailed assigns carrier-format tracking number + dispatch method + receipt photo, delivered sets final timestamp. Webhooks include fulfillment_photos array with URLs, and the dashboard progress tracker renders all photos and tracking data.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Status advanced. Returns previous_status, new status, tracking_number, carrier, dispatch_method, claimed_at, mailed_at, delivered_at, and fulfillment_photos count."},"403":{"description":"Record is not test_mode — use POST /v1/test/mail or an sk_agent_test_ key to create a test record first"},"404":{"description":"Outbound mail not found"},"409":{"description":"Status already terminal (delivered, failed, cancelled)"}}}}}}