{"openapi":"3.1.0","info":{"title":"Mailroom API","version":"1.0.0","description":"Ship packages programmatically: quote → buy label → track → void. Two-step flow: create a quote (no side effects), then create a shipment from a quoteId+rateId. Shipments may return 202 pending_approval when org policy routes agent spend through human approval. All writes require an Idempotency-Key header. Errors carry a stable `code` and a `suggested_action` written for AI agents. Sandbox: mk_test_ keys run in SYNTHETIC mode right here on production — quotes/labels/tracking/voids are simulated end-to-end (responses stamped synthetic:true, prices deliberately not real, labels not shippable); no host change needed. Self-serve onboarding: POST /api/v1/signups returns a test key in one call."},"servers":[{"url":"https://app.mailroom.software"},{"url":"https://stage.mailroom.software","description":"sandbox (mk_test_ keys, carrier test environments)"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"API key: mk_live_… or mk_test_…, created by an org admin in Mailroom settings."}},"schemas":{"Address":{"type":"object","required":["name","line1","city","state","zip"],"properties":{"name":{"type":"string"},"company":{"type":"string"},"phone":{"type":"string"},"line1":{"type":"string"},"line2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string","minLength":2,"maxLength":2},"zip":{"type":"string"},"country":{"type":"string","default":"US"}}},"Error":{"type":"object","properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_request","unauthorized","forbidden_scope","policy_violation","approval_required","address_validation_failed","quote_expired","carrier_error","rate_limited","payment_required","insufficient_balance","idempotency_conflict","not_found","internal_error"]},"message":{"type":"string"},"suggested_action":{"type":"string","description":"What the calling agent should do next."},"details":{}}}}}}},"paths":{"/api/v1/legal":{"get":{"operationId":"getLegal","summary":"Current legal document versions and URLs","security":[],"responses":{"200":{"description":"Terms + privacy metadata"}}}},"/api/v1/signups":{"post":{"operationId":"createSignup","summary":"Self-serve org signup for agents — returns a test API key in ONE call, no human approval needed to start. Requires termsAccepted:true and a reachable human contact (name, email, phone). Rate-limited per IP.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["companyName","email","contactName","contactPhone","shipFrom","termsAccepted"],"properties":{"companyName":{"type":"string","minLength":2,"maxLength":100},"email":{"type":"string","format":"email"},"contactName":{"type":"string"},"contactPhone":{"type":"string"},"shipFrom":{"$ref":"#/components/schemas/Address"},"termsAccepted":{"type":"boolean","enum":[true]}}}}}},"responses":{"201":{"description":"orgId, apiKey (mk_test_, shown once), sandbox instructions, docs + legal URLs"},"400":{"description":"validation error (incl. termsAccepted must be true)"},"429":{"description":"signup rate limit"}}}},"/api/v1/limits/requests":{"post":{"operationId":"requestLimitIncrease","summary":"Ask for more headroom on THIS key’s caps. At/below the org’s auto-grant ceilings → granted instantly; otherwise a human operator reviews (may ask a follow-up question; outcome also arrives as a webhook). One open request per key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"maxCentsPerShipment":{"type":"integer"},"maxCentsPerDay":{"type":"integer"},"maxShipmentsPerDay":{"type":"integer"},"maxQuotesPerDay":{"type":"integer"},"justification":{"type":"string"}}}}}},"responses":{"201":{"description":"request { id, status: auto_granted|pending }"},"409":{"description":"a request is already open for this key"}}},"get":{"operationId":"listLimitRequests","summary":"List this key’s cap-increase requests and their status","responses":{"200":{"description":"requests[]"}}}},"/api/v1/limits/requests/{id}":{"get":{"operationId":"getLimitRequest","summary":"Poll one cap-increase request (status, operator question if any)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"request detail"}}}},"/api/v1/limits/requests/{id}/replies":{"post":{"operationId":"replyToLimitRequest","summary":"Answer the operator’s follow-up question on a pending cap request","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}},"responses":{"201":{"description":"reply recorded"}}}},"/mcp":{"post":{"operationId":"mcp","summary":"MCP endpoint (stateless streamable HTTP, spec 2026-07-28): 16 tools covering shipping, billing, limits, and settings — same auth and policy engine as REST. Unauthenticated calls return WWW-Authenticate with OAuth resource metadata (RFC 9728); dynamic client registration at /oauth/register (RFC 7591).","responses":{"200":{"description":"JSON-RPC response"},"401":{"description":"missing/invalid credentials + resource_metadata pointer"}}}},"/api/v1/quotes":{"post":{"operationId":"createQuote","summary":"Validate an address and get shipping rates (no side effects)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["shipTo","package"],"properties":{"shipTo":{"$ref":"#/components/schemas/Address"},"shipFrom":{"oneOf":[{"type":"string","format":"uuid","description":"saved addressId"},{"$ref":"#/components/schemas/Address"}],"description":"Omit to use the org default ship-from."},"package":{"type":"object","required":["weightLbs","dimensions"],"properties":{"weightLbs":{"type":"number"},"type":{"type":"string","enum":["package","envelope","pak","tube"],"default":"package"},"dimensions":{"type":"object","required":["lengthIn","widthIn","heightIn"],"description":"Exact box dimensions in inches (required — agents specify the real package; Mailroom does not estimate).","properties":{"lengthIn":{"type":"number"},"widthIn":{"type":"number"},"heightIn":{"type":"number"}}},"declaredValueCents":{"type":"integer"},"quantity":{"type":"integer","default":1}}},"itemDescription":{"type":"string"},"requestedDeliveryDate":{"type":"string","format":"date"},"validationOverride":{"type":"boolean","default":false}}}}}},"responses":{"201":{"description":"quoteId (15 min TTL) + rates[] each with rateId, carrier, service, amountCents, estimatedDeliveryDate"},"422":{"description":"address_validation_failed with candidates[]","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/shipments":{"post":{"operationId":"createShipment","summary":"Buy a label from a quote (REAL MONEY — requires Idempotency-Key)","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quoteId","rateId"],"properties":{"quoteId":{"type":"string"},"rateId":{"type":"string"}}}}}},"responses":{"201":{"description":"Label purchased (or purchasing): shipmentId, status, trackingNumber, labelUrl"},"202":{"description":"pending_approval — a manager was notified in Slack; poll GET /shipments/{id}"},"403":{"description":"policy_violation (spend/count caps) with suggested_action","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"idempotency_conflict"},"410":{"description":"quote_expired"}}},"get":{"operationId":"listShipments","summary":"List shipments","parameters":[{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"shipments[]"}}}},"/api/v1/shipments/{id}":{"get":{"operationId":"getShipment","summary":"Shipment detail + tracking events","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"shipment + trackingEvents[]"},"404":{"description":"not_found"}}}},"/api/v1/shipments/{id}/void":{"post":{"operationId":"voidShipment","summary":"Void a label (within carrier void window)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"voided"},"422":{"description":"carrier_error"}}}},"/api/v1/shipments/{id}/label":{"get":{"operationId":"getLabel","summary":"Download the label PDF for a shipment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"label PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"not_found (no label yet)"}}}},"/api/v1/usage":{"get":{"operationId":"getUsage","summary":"This key’s usage, spend (billed), and effective policy (rolling 30 days)","responses":{"200":{"description":"periodDays, shipments, spendCents (customer-billed), apiRequests, and the key policy (caps + approval threshold)"}}}},"/api/v1/reports":{"get":{"operationId":"getReport","summary":"Account shipping history with customer-billed amounts","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date"},"description":"ISO/YYYY-MM-DD, default 30 days ago"},{"name":"until","in":"query","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":500}}],"responses":{"200":{"description":"periodStart/End, totals { shipments, billedCents, byCarrier, byStatus }, and shipments[] with billedCents (what the org was charged, not carrier cost)"}}}},"/api/v1/webhooks":{"get":{"operationId":"listWebhooks","summary":"List this org’s webhook subscriptions","responses":{"200":{"description":"webhooks[] with id, url, events, active, secretHint"}}},"post":{"operationId":"createWebhook","summary":"Subscribe to events (self-serve). Deliveries are POSTed with X-Mailroom-Signature: sha256=HMAC(body, secret)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["shipment.label_created","shipment.approved","shipment.denied","shipment.delivered","shipment.exception"]},"minItems":1}}}}}},"responses":{"201":{"description":"{ id, url, events, secret } — secret shown once"}}}},"/api/v1/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","summary":"Delete a webhook subscription","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"ok"}}}},"/api/v1/contacts":{"get":{"operationId":"listContacts","summary":"List/search the org address book","parameters":[{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"contacts[] with addressId"}}},"post":{"operationId":"saveContact","summary":"Save a contact","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Address"}}}},"responses":{"201":{"description":"addressId"}}}},"/api/v1/addresses/validate":{"post":{"operationId":"validateAddress","summary":"Validate an address without quoting","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Address"}}}},"responses":{"200":{"description":"valid/ambiguous/corrected/candidates"}}}},"/api/v1/billing":{"get":{"operationId":"getBilling","summary":"Org billing status: prepaid balance, refill settings, payment method, live-key unlock state","responses":{"200":{"description":"billing { billing_mode, balance_cents, refill_*, payment_method { brand, last4 } | null, consent_on_file, live_keys_unlocked, plan }"}}}},"/api/v1/billing/transactions":{"get":{"operationId":"listBalanceTransactions","summary":"Balance ledger (top-ups, label charges, refunds, adjustments)","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"transactions[] with signed amount_cents and balance_after_cents"}}}},"/api/v1/billing/top-up":{"post":{"operationId":"createTopUp","summary":"Fund the balance by charging the card on file ($50 minimum). Requires Idempotency-Key. 202 → poll /billing/top-ups/{id}; the balance credits when the payment settles.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_cents"],"properties":{"amount_cents":{"type":"integer","minimum":5000,"maximum":1000000}}}}}},"responses":{"202":{"description":"top_up { id, status: processing }"},"402":{"description":"No payment method, or the card was declined (failure_code/failure_message)"}}}},"/api/v1/billing/top-ups/{id}":{"get":{"operationId":"getTopUp","summary":"Poll a top-up","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"top_up { id, status: processing|succeeded|failed, failure_code }"}}}},"/api/v1/billing/refill":{"patch":{"operationId":"updateRefill","summary":"Auto-refill settings (refill amount ≥ $50)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"refill_enabled":{"type":"boolean"},"refill_threshold_cents":{"type":"integer","minimum":500},"refill_amount_cents":{"type":"integer","minimum":5000}}}}}},"responses":{"200":{"description":"Updated billing status"}}}},"/api/v1/billing/setup-link":{"post":{"operationId":"createSetupLink","summary":"Mint a 1-hour URL where the org’s HUMAN admin adds a card (Stripe Elements) and signs the billing authorization — card details never pass through this API","responses":{"200":{"description":"setup_url + instructions"}}}},"/api/v1/settings-link":{"post":{"operationId":"createSettingsLink","summary":"Mint a 1-hour login URL for the org’s HUMAN admin (\"minder\") to open the full settings page and make changes agents cannot (policies, approvals, carriers, cards, keys). Optional section deep-link. Treat the URL like a password.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"section":{"type":"string","enum":["ship-from","carriers","rules","permissions","organization","billing","activity","api-keys"]}}}}}},"responses":{"200":{"description":"settings_url + expires_in_seconds + instructions"},"429":{"description":"Link-minting rate limit (10/hour per org)"}}}},"/api/v1/keys/activate-live":{"post":{"operationId":"activateLiveKey","summary":"Unlock live shipping: requires a payment method on file, the billing authorization, and a positive balance. Returns an mk_live_ key (shown once) with default caps.","responses":{"201":{"description":"api_key (store immediately), key_id, policy"},"402":{"description":"Lists the missing prerequisites"}}}}}}