← Human Mailbox · Use cases · Contact
API reference
Base URL https://humanmailbox.com/v1. JSON in, JSON out. Prices in euro. One location for now: DE-1, Germany.
Basics
Authentication
Register once with POST /v1/register. The response contains your API key (hm_…) exactly once. Send it on every other call:
Authorization: Bearer hm_YOUR_API_KEY
# or
X-API-Key: hm_YOUR_API_KEY
Lost or leaked key: POST /v1/account/rotate-key issues a new one and disables the old one.
Errors
Every error is JSON with a machine code and a sentence: {"error": "missing_field", "message": "Field \"agent_name\" is required.", "field": "agent_name"}.
| Status | Meaning |
|---|---|
| 400 | Invalid input (missing_field, bad_json, unknown_action, unknown_bundle, disposable_email …) |
| 401 | No or wrong API key |
| 402 | Payment required (payment_required) — see Payments; address_locked: the street address comes with the mailbox month (15 €) (see Address); or item_unpaid: the item waits for payment first |
| 403 | Account blocked or paused, or operator_email_unconfirmed (forward / courier) |
| 404 | Not found, or not yours |
| 409 | Not possible now: express_not_possible, not_cancellable, no_quote, item_gone, not_unpaid … |
| 410 | code_gone — a code expired (not retrieved within 24 months) or was voided |
| 413 | Body too large |
| 429 | Too many requests |
| 503 | address_not_ready — the street address is not configured yet |
Money and time
All amounts are euro. Fields ending in _eur are strings with two decimals ("5.00"). Dates such as due_date are YYYY-MM-DD in Europe/Berlin. Business days are Monday to Friday, except public holidays in Germany.
Pricing model and credits
Prices can change — GET /v1/prices is always authoritative. Current defaults:
| What | Price | Notes |
|---|---|---|
| Letter | 5 € | Received, envelope photo, opened and scanned within 3 business days, original kept 4 weeks |
| Mailbox | 15 € / 30 days | Address, intake, photos, webhooks, storage; renews from your balance |
| Parcel | 15 € | = 3 letter credits; unpacked, contents photographed |
| Same-day opening | +2 € | Or 1 same-day credit; opened and scanned on the day of arrival (business days) |
| Forward | 5 € + postage | Handling paid at order, postage at cost after weighing |
| Express | +5 € | Dispatched the same day if the item is here before 14:00 Europe/Berlin on a business day |
| Return to sender | 5 € + postage | No express |
| Code reveal | 1 € | Per gift card / voucher code from your mail; revealing again is free |
| Dispose | free | Shred or dispose |
| Courier, custom | quoted | The human names a price, you accept |
Credits
Every arriving item is paid when it arrives: first with letter credits (letter 1, parcel 2), otherwise from the money balance. With neither, the item is logged and photographed but waits unopened: payment_status: "unpaid" plus a payment_required object. It stays like that until you call POST /v1/items/{id}/pay, buy a bundle or top up — waiting items are then settled automatically, oldest first.
When mail is opened
Paid items are opened and scanned according to their open_mode:
standard— within 3 business days (included). The item'sdue_datesays by when.same_day— on the day of arrival (+2 € or 1 same-day credit). Without credits or balance it falls back to standard.never— kept sealed; order ascanlater if you change your mind.
The default is your account open_policy; a shipment's open_mode overrides it.
Public endpoints (no key)
POST/v1/demo
Free demo mailbox, no registration (body optional: agent_name, agent_model, found_via). Answers 201 {"demo": true, "api_key", "mailbox_code", "expires_at", "balance_eur", "credits", "address", "try_this": [...]}. Use the key with every endpoint: play money, a scanned sample letter, a sealed sample parcel, a sample code in the vault. A simulated human acts about 20 seconds later (shipments arrive, postage and quotes come back, orders get done). GET /v1/address returns a demo address ("demo": true), real top-ups and invoices answer 403 demo_account, nothing is shipped, the demo is deleted after 7 days. Webhooks are not sent for simulated steps; poll instead. Max. 10 demos per network and day.
GET/v1
Service summary, included services, rules, limits, available payment_methods, x402 details, first steps and links.
GET/v1/prices
{
"currency": "EUR",
"letter": { "price_eur": "5.00", "includes": "receive, envelope photo, open and scan within 3 business days", "open_within_business_days": 3 },
"parcel": { "price_eur": "10.00", "credits": 2 },
"same_day_opening": { "price_eur": "1.00", "note": "open and scan on the day of arrival (business days)" },
"codes": { "fee_eur_per_code": "1.00", "how": "…", "expire_after_months": 24 },
"forwarding": { "handling_eur": "5.00", "postage": "at cost; …", "express_eur": "5.00", "express_rule": "dispatched the same day if the item is here before 14:00 Europe/Berlin …" },
"bundles": [
{ "id": "letters_1", "kind": "letters", "qty": 1, "price_eur": "5.00", "per_unit_eur": "5.00" },
{ "id": "letters_10", "kind": "letters", "qty": 10, "price_eur": "30.00", "per_unit_eur": "3.00" },
{ "id": "letters_100", "kind": "letters", "qty": 100, "price_eur": "200.00", "per_unit_eur": "2.00" },
{ "id": "same_day_10", "kind": "same_day", "qty": 10, "price_eur": "8.00", "per_unit_eur": "0.80" },
{ "id": "same_day_100", "kind": "same_day", "qty": 100, "price_eur": "50.00", "per_unit_eur": "0.50" }
],
"actions": {
"scan": { "action": "scan", "pricing": "included", "price_cents": 0, "price_eur": "0.00", "needs": ["item"], "label": "…" },
"forward": { "action": "forward", "pricing": "forward", "price_cents": 500, "price_eur": "5.00", "needs": ["item", "address"], "label": "…" },
"return": { "action": "return", "pricing": "forward", "price_cents": 500, "price_eur": "5.00", "needs": ["item"], "label": "…" },
"courier": { "action": "courier", "pricing": "quote", "price_cents": null, "price_eur": null, "needs": ["address"], "label": "…" },
"dispose": { "action": "dispose", "pricing": "fixed", "price_cents": 0, "price_eur": "0.00", "needs": ["item"], "label": "…" },
"custom": { "action": "custom", "pricing": "quote", "price_cents": null, "price_eur": null, "needs": [], "label": "…" }
},
"included": ["…"], "not_accepted": ["…"],
"limits": { "parcel_max_weight_kg": 31.5, "parcel_max_size_cm": "120 x 60 x 60", "storage_free_days": 28,
"business_days": "Monday to Friday, except public holidays in Germany", "express_cutoff": "14:00 Europe/Berlin" },
"payment_methods": ["x402", "btc", "paypal"]
}
GET/v1/locations
Open locations. Today: DE-1 (Germany).
POST/v1/register
| Field | Required | Notes |
|---|---|---|
agent_name | yes | Name of the agent |
operator_name | no* | Responsible human or company — the contract partner |
operator_email | no* | Real, permanent address of the operator (disposable addresses are rejected); receives one confirmation link |
address_name | no | Name on your mail (2–60 chars; your own, artist or made-up name — no company, brand, bank, authority or famous person). Default without operator: agent_name |
accept_terms | yes | true — accepts the terms on behalf of the operator |
operator_type | no | individual | company |
operator_country | no | e.g. DE |
purpose | no | What the mailbox is for |
agent_model | no | Model or framework |
webhook_url | no | Public https URL for webhooks |
open_policy | no | standard (default) | same_day | never |
found_via | no | Where you found us (≤120 chars): mcp_registry, smithery, glama, pulsemcp, mcp_so, x402_bazaar, x402scan, search_engine, llms_txt, github, hacker_news, reddit, x, word_of_mouth, other |
Response 201:
{
"api_key": "hm_k3J9pQ2xVn7Rt5Wc8Ym4Za6Hb2Gd9Ef3Lu7Ns5Pq",
"webhook_secret": "whsec_…",
"mailbox_code": "HM-7K3QX",
"account": { "id": "acc_…", "mailbox_code": "HM-7K3QX", "operator": { "name": "Jane Doe", "email": "jane@yourcompany.com", "email_status": "ok", … },
"open_policy": "standard", "balance_eur": "0.00", "credits": { "letters": 0, "same_day": 0 }, … },
"address": null,
"address_unlocked": false,
"next_steps": ["Top up (POST /v1/topups), then GET /v1/address: it charges the first mailbox month (15 €) and returns the address.", "…"]
}
address is always null at registration: the street address comes with the mailbox month (15 €) (see Address).
POST/v1/interest
Want another country? {"kind": "agent"|"human", "name", "email", "country_wanted", "use_case", "found_via"} — at least a name, an email or a use case. Answers 201 {"ok": true, "id", "message"}.
POST/v1/contact
Ask a human anything that is not in the price list. No account needed. {"topic", "body", "subject", "email", "name", "budget_eur", "deadline", "location"} — only body is required.
topic:special_request(a job not in the price list — say what, where, when and your budget),is_it_possible(ask before you send mail for it),payment_method(your payment method is missing — addmethod_wanted, e.g.lightning,usdc-solana,sepa),feature_request,problem,question(default),other.GET /v1/contactexplains each.- Answer
201 {"inquiry": {"id": "inq_…", "topic", "status": "open", "replies": []}, "ticket": "hmt_…", "check": "…"}. - With your API key the inquiry is linked to your mailbox and the answer also arrives as a message (
GET /v1/messages, webhookmessage.received). Withemailthe answer is also emailed. Without an API key you get aticket.
GET/v1/contact/{id}
Status and answers of an inquiry: header X-Ticket: hmt_… (or your API key). {"inquiry": {"id", "topic", "status": "open"|"answered", "answered_at", "replies": [{"at", "subject", "body"}]}}. Web form: /request/.
Account
GET/v1/account
{"account", "address", "address_unlocked", "stats": {"items_in_storage", "open_requests", "items_waiting_for_payment"}, "payment_methods"}. address is null while address_unlocked is false. The account carries mailbox_code, address_unlocked, operator {name, email, type, country, email_status} (email_status: unchecked, ok, no_mx, disposable, invalid, verified), open_policy, status (active, paused, blocked), balance_eur, credits {letters, same_day}.
PATCH/v1/account
Change agent_name, agent_model, purpose, webhook_url, open_policy, auto_reveal_codes, address_name, budget_eur_month (monthly spending limit for orders; over it: 403 budget_exceeded; null = none), operator_name, operator_email, operator_type, operator_country. A new operator email has to be confirmed again (email_confirmation_sent: true).
POST/v1/account/rotate-key
Returns {"api_key": "hm_…"}. The old key stops working.
POST/v1/account/verify-email
Sends the confirmation link to the operator again (at most every 10 minutes). Forward and courier orders need email_status: "verified".
GET/v1/subscription
The mailbox: 15 € per 30 days. {"subscription": {"status": "active|past_due|cancelled|expired|none", "price_eur", "paid_until", "renews"}}. Renews from your balance; without money it goes past_due (webhook subscription.past_due, address works 14 more days).
POST/v1/subscription
Prepay {"months": 1–12} from balance or x402.
POST/v1/subscription/cancel
Stops renewing; usable until paid_until, later mail is returned to the sender.
GET/v1/summary
This month at a glance: balance_eur, credits, subscription, budget (limit, spent, left), this_month (by transaction kind), requests, items, shipments (counts by status).
Address
The street address comes with the mailbox: 15 € per 30 days (address, intake, envelope photos, webhooks, storage). The first GET /v1/address charges the first month from your balance; the mailbox renews from your balance every 30 days. Status: GET /v1/subscription · prepay: POST /v1/subscription {"months": 1–12} · stop renewing: POST /v1/subscription/cancel.
GET/v1/address
While locked:
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6Mi… (x402 for the first mailbox month)
{ "error": "address_locked",
"message": "Your street address comes with the mailbox (15 € per 30 days) …",
"price_eur": "15.00", "balance_eur": "0.00", "credits": { "letters": 0, "same_day": 0 },
"x402": { "x402Version": 2, "accepts": [ … ] },
"top_up": { "endpoint": "POST /v1/topups", "methods": ["x402", "btc", "paypal"] },
"bundles": "POST /v1/bundles" }
Top up and call it again, or (once x402 is live) repeat the same GET with PAYMENT-SIGNATURE: it pays the first month and returns the address in the same 200 response. Unlocked:
{
"location": "DE-1",
"mailbox_code": "HM-7K3QX",
"lines": ["Human Mailbox HM-7K3QX", "c/o …", "Street 1", "12345 City", "GERMANY"],
"lines_domestic": ["Human Mailbox HM-7K3QX", "c/o …", "Street 1", "12345 City"],
"formatted": "Human Mailbox HM-7K3QX\nc/o …\nStreet 1\n12345 City\nGERMANY",
"fields": { "name": "Human Mailbox HM-7K3QX", "care_of": "c/o …", "street": "Street 1", "postal_code": "12345", "city": "City", "country": "Germany", "country_code": "DE" },
"rules": ["Write the mailbox code HM-7K3QX in the first address line, exactly like that. …"]
}
Use lines for senders outside Germany, lines_domestic for senders inside Germany. Always keep the mailbox code in the first line.
Bundles
POST/v1/bundles
{"bundle": "letters_1" | "letters_10" | "letters_100" | "same_day_10" | "same_day_100"}. Paid from your balance, or inline via x402 (402 → repeat with PAYMENT-SIGNATURE).
201
{ "bundle": { "id": "letters_10", "kind": "letters", "qty": 10, "price_eur": "30.00" },
"paid_with": "x402", "settled_items": 1, "balance_eur": "0.00", "credits": { "letters": 9, "same_day": 0 } }
settled_items = items that were waiting for payment and are now paid and scheduled for opening.
Shipments
Announcing what is coming helps the human match it, especially parcels with tracking numbers.
POST/v1/shipments
{ "carrier": "DHL", "tracking": "00340434161094042557", "sender": "Example Electronics GmbH",
"description": "Small parcel, development board", "expected_at": "2026-10-28",
"instructions": "Photograph the contents", "open_mode": "same_day" }
Give at least a tracking number, a sender or a description. open_mode overrides your open_policy for this shipment. Returns 201 {"shipment": {…, "status": "announced"}, "tip"}; on arrival the status becomes arrived and item_id is set.
GET/v1/shipments · GET/v1/shipments/{id} · POST/v1/shipments/{id}/cancel
To forward something the moment it arrives (also express), create a request with shipment_id (see Requests).
Items and files
GET/v1/items · GET/v1/items/{id}
{ "id": "itm_…", "type": "letter", "status": "stored",
"payment_status": "unpaid",
"payment_required": { "letter_credits": 1, "price_eur": "5.00", "pay": "POST /v1/items/itm_…/pay", "or": "buy a bundle (POST /v1/bundles); unpaid items are settled automatically" },
"open_mode": "standard", "due_date": null,
"carrier": "Deutsche Post", "tracking": null, "sender": "Finanzamt …", "weight_g": 20, "size_cm": null,
"condition": "ok", "note": "…", "shipment_id": null,
"received_at": "2026-10-22T08:14:00.000Z", "stored_until": "2026-11-21T08:14:00.000Z",
"files": [ { "id": "fil_…", "kind": "photo", "mime": "image/jpeg", "url": "https://humanmailbox.com/v1/files/fil_…", "preview_url": "…?size=preview" } ] }
type: letter, large_letter, parcel, other. status: stored, opened, forwarded, delivered, returned, disposed, refused. payment_status: paid, unpaid, waived, refunded. GET /v1/items/{id} adds requests[]. Lists accept ?status=.
POST/v1/items/{id}/pay
Pays a waiting item with credits, balance, or inline x402. Returns {"item", "paid_with", "notes"}; the item is then scheduled for opening.
GET/v1/files/{id}
Downloads a photo, scan or document (JPEG/PNG/WebP/PDF) with your normal auth header. ?size=preview returns a smaller JPEG when one exists.
Requests (orders)
POST/v1/requests
| Action | Needs | Pricing | Options |
|---|---|---|---|
scan | item or shipment | included (same-day +2 €) | same_day |
forward | item or shipment + address | 5 € (letter) / 10 € (parcel) handling now + postage at cost | express, max_postage_eur, sender |
return | item or shipment | 5 € / 10 € handling now + postage at cost | max_postage_eur, sender |
send_letter | address + text or pdf_base64 (A4, max 5 MB) | 8 € now + postage at cost | registered (none, einschreiben_einwurf, einschreiben, einschreiben_rueckschein), max_postage_eur, sender |
ship_out | item + label_pdf_base64 or address | 20 € now + postage at cost | carrier, reference, max_postage_eur |
digitize | item + note (what to capture) | 15 € | output: photo | pdf | json (data in result_note) |
buy_voucher | product, value_eur (max 100) | quoted: value + 10 % (min 5 €) | shop, delivery: code_vault | send_card; max 250 € per month; needs a confirmed operator |
buy_item | what, max_price_eur (max 500) | quoted: price + 10 % (min 15 €) | where; delivered to your mailbox; needs a confirmed operator |
courier | address | quoted | note |
dispose | item or shipment | free | |
custom | note | quoted | mail, documents and parcels only — no calls, product tests or errands |
{ "action": "forward", "item_id": "itm_…", "express": true, "max_postage_eur": 15,
"address": { "name": "Example B.V.", "street": "Keizersgracht 1", "postal_code": "1015 CJ", "city": "Amsterdam", "country": "Netherlands", "phone": "+31 …" },
"sender": { "name": "Jane Doe", "company": "Your Company B.V.", "street": "…", "postal_code": "…", "city": "…", "country": "Netherlands" },
"note": "Tracked shipping please" }
- scan: arrived, paid items are scheduled automatically. Use
"same_day": trueto upgrade to opening today, or order a scan for an item kept sealed (open_mode: never). - forward / return: handling (+5 € with
express) is charged at order. The human weighs the item and enters the postage at cost: up tomax_postage_eurit is charged automatically (request.updated), above it you getquote.readyand accept. - express (forward only): dispatched the same day if the item is here before 14:00 Europe/Berlin on a business day. Pre-booked for a shipment that arrives later: the express fee is refunded and it leaves the next business day. For an item already here after the cut-off the call answers
409 express_not_possible. - sender (optional): return address printed on the forwarded item. It must be the operator or their company — never a third party.
- forward and courier need a confirmed operator email (
403 operator_email_unconfirmedotherwise). - shipment_id instead of
item_idpre-books for an announced shipment; it runs on arrival. - An item with
payment_status: "unpaid"answers402 item_unpaid— pay it first.
Response 201 {"request", "paid_with", "balance_eur", "message"}. Request fields: id, action, item_id, shipment_id, status, pricing (included | fixed | forward | quote), price_eur, paid_eur, postage_eur, max_postage_eur, quote_note, express, same_day, due_date, note, address, sender, result_note, result_tracking, timestamps.
Status flow
scan, dispose: queued → in_progress → done
forward, return: quote_pending (handling paid, waiting for postage)
→ queued (postage ≤ max_postage_eur, charged automatically)
→ or quote_ready → (accept) → queued
→ in_progress → done (result_tracking)
courier, custom: quote_pending → quote_ready → (accept) → queued → in_progress → done
any: refused (refunded to balance) · cancelled (refunded if not started)
GET/v1/requests · GET/v1/requests/{id}
One request includes files (scans, photos, receipts). Lists accept ?status=.
POST/v1/requests/{id}/accept
Accepts a request in quote_ready and pays the part not paid yet (balance or x402).
POST/v1/requests/{id}/cancel
Possible in quote_pending, quote_ready and queued. Paid money goes back to the balance (a same-day credit back to credits). Returns {"request", "refunded_eur"}.
Payments
With enough credits or balance, things are paid from them. Otherwise the call answers 402:
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6Mi… (base64 JSON)
{ "error": "payment_required",
"message": "This costs €40.00. Your balance is €0.00. …",
"price_eur": "30.00", "balance_eur": "0.00", "credits": { "letters": 0, "same_day": 0 },
"x402": { "x402Version": 2,
"resource": { "url": "https://humanmailbox.com/v1/bundles", "description": "Human Mailbox bundle letters_10 for HM-7K3QX — €40.00 = 46.60 USDC", … },
"accepts": [ { "scheme": "exact", "network": "eip155:8453", "amount": "34950000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x…", "maxTimeoutSeconds": 300,
"extra": { "name": "USD Coin", "version": "2" } } ] },
"top_up": { "endpoint": "POST /v1/topups", "methods": ["x402", "btc", "paypal"] },
"bundles": "POST /v1/bundles (prepaid letters are cheaper)" }
x402 v2, step by step
Decode
PAYMENT-REQUIRED(or use thex402field of the body). Prices are in euro;amountis the USDC equivalent at the current EUR→USD rate (refreshed every 15 minutes), andresource.descriptionshows both.Sign an EIP-3009
transferWithAuthorizationfor exactlyamount(USDC has 6 decimals) topayTo, using the token's EIP-712 domain fromextra. Any x402 client library does this for you.Repeat the same call (same method, path and body) with header
PAYMENT-SIGNATURE: base64 JSON{"x402Version": 2, "resource": …, "accepted": <the chosen requirements>, "payload": {"signature", "authorization"}}.We verify and settle through an x402 facilitator. Success: the normal response plus header
PAYMENT-RESPONSE(base64 JSON withtransaction,network,payer). Failure:402again with the reason in the message.
Top-ups
POST/v1/topups
{"method": "x402" | "btc" | "paypal", "amount_eur": "20.00"} (1–1000) or {"method": …, "bundle": "letters_10"} — the bundle is bought automatically once the money is in. At most 5 open invoices.
- x402: answers 402; repeat with
PAYMENT-SIGNATURE; credited at once (200 {"topup", "balance_eur", "credits"}; withbundlethe answer is the same asPOST /v1/bundles). - btc:
201 {"topup": {"id", "address", "amount_btc", "uri": "bitcoin:…?amount=…", "expires_at", "status": "open", "btc": {"rate_eur_per_btc", "received_sats", "txid", "explorer_url", "note"}}}. Pay exactly that amount to that address within 60 minutes (the rate is locked). Statusopen → seen → paid; credited after confirmation. - paypal:
201 {"topup": {"id", "approve_url", "status": "open"}}. Giveapprove_urlto your human operator. Credited once paid.
GET/v1/topups · GET/v1/topups/{id}
Reading one top-up re-checks the payment (blockchain or PayPal) and credits it if it arrived. We also check automatically.
GET/v1/balance
{ "balance_eur": "12.00", "currency": "EUR", "credits": { "letters": 8, "same_day": 0 }, "payment_methods": ["x402", "btc", "paypal"],
"transactions": [ { "id": "txn_…", "at": "2026-10-22T08:14:00.000Z", "kind": "usage", "amount_eur": null,
"credits_letters": -1, "credits_same_day": null, "balance_after_eur": "12.00", "method": "credits", "ref": "itm_…" } ] }
kind: topup, charge, refund, adjust, bundle, usage (credits used by an arriving item).
POST/v1/invoices · GET/v1/invoices · GET/v1/invoices/{id}
Optional. No invoice is created unless you ask for one. POST /v1/invoices creates one (free) for your incoming payments that have none yet, or for the given payment_ids, numbers it (INV-2026-0001 …) and emails it (default: operator email). billing.name defaults to the operator name; invoices over €250 need the full address. No VAT is charged (§ 19 UStG small business).
POST /v1/invoices
{ "billing": { "company": "Acme Agents Inc.", "street": "1 Main St", "postal_code": "10001", "city": "New York", "country": "USA" },
"email": "billing@acme.example" }
→ 201 { "invoice": { "id": "bill_…", "number": "INV-2026-0001", "total_eur": "35.00", "lines": [ … ], "email_to": "billing@acme.example", "text": "INVOICE INV-2026-0001 …" } }
GET /v1/invoices → { "invoices": [ … ], "billable_payments": [ { "id", "paid_at", "amount_eur", "method" } ] }. Errors: 409 nothing_to_invoice, 400 not_billable, 400 billing_address_required.
Code vault
Gift card, voucher and similar codes that arrive in your mail are read in by the human and stored encrypted. You get the webhook codes.ready with label, value_eur and hint (last 3 characters) — never the code itself. Unretrieved codes expire after 24 months.
GET/v1/codes
{ "codes": [ { "id": "code_…", "item_id": "itm_…", "label": "Gift card", "value_eur": "25.00", "status": "ready",
"hint": "…X7Q", "code": null, "fee_eur": null, "created_at": "…", "retrieved_at": null, "expires_at": "2028-10-29T…" } ],
"fee_eur_per_code": "1.00", "auto_reveal_codes": false }
status: ready (waiting), retrieved (includes code), expired, void.
POST/v1/codes/{id}/reveal · POST/v1/codes/reveal-all
1 € per waiting code, from your balance or inline via x402 (402 → repeat with PAYMENT-SIGNATURE). reveal-all takes an optional {"ids": [...]} (up to 100); without it, every waiting code is revealed. Already retrieved codes come back again for free. Answer: {"codes": [...], "charged_eur"}; an expired or void code answers 410 code_gone.
Automatic delivery
PATCH /v1/account {"auto_reveal_codes": true}: each code is revealed as soon as it is read in (fee from the balance) and sent to your webhook as codes.delivered, including the code. Without enough balance it waits as ready.
Messages
POST/v1/messages · GET/v1/messages
Write to the human: {"body", "subject", "item_id", "request_id"} (at most 30 per hour). direction is to_human for yours and from_human for replies (also sent as webhook message.received); read marks replies you already listed.
Webhooks
If your account has a webhook_url, we POST JSON for these events:
| Event | When / data |
|---|---|
item.received | Something arrived: item (with the envelope photo), scheduled requests, notes. If item.payment_status is unpaid, it waits for you. |
item.expiring | An original reaches the end of its 4 weeks in 7 days: item, expires_at, note. Order forwarding or return before then, otherwise the item and its contents are disposed of. |
codes.ready | Codes were read in: item_id, codes (label, value, last 3 characters — no code), fee_eur_per_code, reveal, expire_after_months |
codes.delivered | Automatic delivery (auto_reveal_codes): item_id, codes including the code |
item.refused | The human refused or returned an item: item_id, note |
request.updated | An order changed status, or postage was charged automatically: request (with files when done), refunded_eur, note |
quote.ready | A price is ready (quote, or postage above max_postage_eur): request, to_pay_eur, accept |
message.received | The human wrote to you: id, subject, body, item_id, request_id |
subscription.renewed · subscription.past_due | The mailbox was renewed from your balance (paid_until), or could not be (price_eur, top up) |
payment.received | A top-up was credited: method, amount_eur, balance_eur, credits, invoice_id |
POST https://your-agent.example/hooks/mail
Content-Type: application/json
X-HumanMailbox-Event: item.received
X-HumanMailbox-Signature: t=1792656000,v1=5f1c…
{ "id": "evt_…", "event": "item.received", "created_at": "2026-10-22T09:30:00Z", "mailbox_code": "HM-7K3QX", "data": { "item": { … } } }
Verify: v1 == hex(HMAC_SHA256(webhook_secret, t + "." + raw_body)) and reject old t values.
// Node.js
import crypto from 'node:crypto';
const [t, v1] = header.split(',').map((p) => p.split('=')[1]);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - Number(t)) < 300;
# Python
import hmac, hashlib, time
t, v1 = (p.split("=", 1)[1] for p in header.split(","))
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
ok = hmac.compare_digest(v1, expected) and abs(time.time() - int(t)) < 300
Answer with any 2xx within 8 seconds. Webhooks are a convenience — polling the API always works.
MCP tools (MCP server for physical mail, 32 tools)
Endpoint https://humanmailbox.com/mcp — Streamable HTTP, stateless, JSON responses. Header Authorization: Bearer hm_… (not needed for register_mailbox, start_demo, get_prices, contact_human, check_inquiry and express_interest; clients without custom headers can pass api_key as a tool argument).
| Tool | Same as |
|---|---|
register_mailbox | POST /v1/register (incl. open_policy, found_via) |
start_demo | POST /v1/demo (no API key needed) |
express_interest | POST /v1/interest (no API key needed) |
get_prices | GET /v1/prices |
contact_human | POST /v1/contact (no API key needed) |
check_inquiry | GET /v1/contact/{id} (with ticket or API key) |
get_account | GET /v1/account |
get_address | GET /v1/address (style: company | person = operator name + c/o Human Mailbox HM-… for forms that need a first and last name) — while locked: isError with the payment data; pay via _meta["x402/payment"] to unlock |
buy_bundle | POST /v1/bundles |
pay_item | POST /v1/items/{id}/pay |
announce_shipment | POST /v1/shipments |
list_shipments | GET /v1/shipments |
list_items | GET /v1/items |
get_item | GET /v1/items/{id} — envelope photos and scans come back inline as image content |
request_action | POST /v1/requests (express, max_postage_eur, sender, same_day) |
list_requests | GET /v1/requests |
get_request | GET /v1/requests/{id} |
accept_quote | POST /v1/requests/{id}/accept |
cancel_request | POST /v1/requests/{id}/cancel |
list_codes | GET /v1/codes |
reveal_codes | POST /v1/codes/reveal-all (ids optional) |
get_balance | GET /v1/balance |
get_summary | GET /v1/summary |
get_subscription · pay_subscription | GET / POST /v1/subscription |
update_account | PATCH /v1/account (budget, webhook, open_policy, address_name, operator) |
top_up | POST /v1/topups (amount_eur or bundle) |
check_top_up | GET /v1/topups/{id} |
send_message | POST /v1/messages |
list_messages | GET /v1/messages |
x402 over MCP (x402 v2 MCP transport): a paid tool returns isError: true with the PaymentRequired object in structuredContent (and as JSON text in content[0]). Call the tool again with the PaymentPayload in params._meta["x402/payment"]; the settlement receipt is in result._meta["x402/payment-response"].
{ "mcpServers": { "humanmailbox": { "type": "http", "url": "https://humanmailbox.com/mcp",
"headers": { "Authorization": "Bearer hm_YOUR_API_KEY" } } } }
Rules and limits
- Normally the agent names a responsible human or company operator; the operator is the customer.
- * Without a human: leave out both operator fields. Limits: 250 € in per month, 20 forwards per month, letters and documents only (parcels are not forwarded); 403
self_operated_limitbeyond that. Mail reads “<address_name>, c/o Human Mailbox HM-XXXXX”. Name a human later (PATCH /v1/account) to lift the limits. - A sender line on forwarded mail must be the operator's own identity (without operator: your
address_name). - Not accepted: illegal goods, weapons, ammunition, drugs, counterfeits; dangerous goods; live animals, perishable food; cash or cash on delivery; deliveries requiring an ID check of the recipient; goods bought with someone else's payment details or meant to be re-shipped to hide their origin.
- Suspicious items may be refused, returned, opened or reported to the authorities (
item.refused). - Parcels up to 31.5 kg and 120 × 60 × 60 cm. Non-EU shipments may need customs clearance; duties and fees are charged to your balance.
- Originals are kept 4 weeks after arrival, then letters are destroyed; scans stay available for 90 days after that. Business days: Monday to Friday, except public holidays in Germany. Express cut-off 14:00 Europe/Berlin.
Full terms: AGB / Terms.