← 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"}.

StatusMeaning
400Invalid input (missing_field, bad_json, unknown_action, unknown_bundle, disposable_email …)
401No or wrong API key
402Payment 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
403Account blocked or paused, or operator_email_unconfirmed (forward / courier)
404Not found, or not yours
409Not possible now: express_not_possible, not_cancellable, no_quote, item_gone, not_unpaid …
410code_gone — a code expired (not retrieved within 24 months) or was voided
413Body too large
429Too many requests
503address_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:

WhatPriceNotes
Letter5 €Received, envelope photo, opened and scanned within 3 business days, original kept 4 weeks
Mailbox15 € / 30 daysAddress, intake, photos, webhooks, storage; renews from your balance
Parcel15 €= 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)
Forward5 € + postageHandling 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 sender5 € + postageNo express
Code reveal1 €Per gift card / voucher code from your mail; revealing again is free
DisposefreeShred or dispose
Courier, customquotedThe 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:

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

FieldRequiredNotes
agent_nameyesName of the agent
operator_nameno*Responsible human or company — the contract partner
operator_emailno*Real, permanent address of the operator (disposable addresses are rejected); receives one confirmation link
address_namenoName 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_termsyestrue — accepts the terms on behalf of the operator
operator_typenoindividual | company
operator_countrynoe.g. DE
purposenoWhat the mailbox is for
agent_modelnoModel or framework
webhook_urlnoPublic https URL for webhooks
open_policynostandard (default) | same_day | never
found_vianoWhere 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.

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

ActionNeedsPricingOptions
scanitem or shipmentincluded (same-day +2 €)same_day
forwarditem or shipment + address5 € (letter) / 10 € (parcel) handling now + postage at costexpress, max_postage_eur, sender
returnitem or shipment5 € / 10 € handling now + postage at costmax_postage_eur, sender
send_letteraddress + text or pdf_base64 (A4, max 5 MB)8 € now + postage at costregistered (none, einschreiben_einwurf, einschreiben, einschreiben_rueckschein), max_postage_eur, sender
ship_outitem + label_pdf_base64 or address20 € now + postage at costcarrier, reference, max_postage_eur
digitizeitem + note (what to capture)15 €output: photo | pdf | json (data in result_note)
buy_voucherproduct, value_eur (max 100)quoted: value + 10 % (min 5 €)shop, delivery: code_vault | send_card; max 250 € per month; needs a confirmed operator
buy_itemwhat, max_price_eur (max 500)quoted: price + 10 % (min 15 €)where; delivered to your mailbox; needs a confirmed operator
courieraddressquotednote
disposeitem or shipmentfree
customnotequotedmail, 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" }

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

  1. Decode PAYMENT-REQUIRED (or use the x402 field of the body). Prices are in euro; amount is the USDC equivalent at the current EUR→USD rate (refreshed every 15 minutes), and resource.description shows both.

  2. Sign an EIP-3009 transferWithAuthorization for exactly amount (USDC has 6 decimals) to payTo, using the token's EIP-712 domain from extra. Any x402 client library does this for you.

  3. 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"}}.

  4. We verify and settle through an x402 facilitator. Success: the normal response plus header PAYMENT-RESPONSE (base64 JSON with transaction, network, payer). Failure: 402 again 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.

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:

EventWhen / data
item.receivedSomething arrived: item (with the envelope photo), scheduled requests, notes. If item.payment_status is unpaid, it waits for you.
item.expiringAn 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.readyCodes were read in: item_id, codes (label, value, last 3 characters — no code), fee_eur_per_code, reveal, expire_after_months
codes.deliveredAutomatic delivery (auto_reveal_codes): item_id, codes including the code
item.refusedThe human refused or returned an item: item_id, note
request.updatedAn order changed status, or postage was charged automatically: request (with files when done), refunded_eur, note
quote.readyA price is ready (quote, or postage above max_postage_eur): request, to_pay_eur, accept
message.receivedThe human wrote to you: id, subject, body, item_id, request_id
subscription.renewed · subscription.past_dueThe mailbox was renewed from your balance (paid_until), or could not be (price_eur, top up)
payment.receivedA 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).

ToolSame as
register_mailboxPOST /v1/register (incl. open_policy, found_via)
start_demoPOST /v1/demo (no API key needed)
express_interestPOST /v1/interest (no API key needed)
get_pricesGET /v1/prices
contact_humanPOST /v1/contact (no API key needed)
check_inquiryGET /v1/contact/{id} (with ticket or API key)
get_accountGET /v1/account
get_addressGET /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_bundlePOST /v1/bundles
pay_itemPOST /v1/items/{id}/pay
announce_shipmentPOST /v1/shipments
list_shipmentsGET /v1/shipments
list_itemsGET /v1/items
get_itemGET /v1/items/{id} — envelope photos and scans come back inline as image content
request_actionPOST /v1/requests (express, max_postage_eur, sender, same_day)
list_requestsGET /v1/requests
get_requestGET /v1/requests/{id}
accept_quotePOST /v1/requests/{id}/accept
cancel_requestPOST /v1/requests/{id}/cancel
list_codesGET /v1/codes
reveal_codesPOST /v1/codes/reveal-all (ids optional)
get_balanceGET /v1/balance
get_summaryGET /v1/summary
get_subscription · pay_subscriptionGET / POST /v1/subscription
update_accountPATCH /v1/account (budget, webhook, open_policy, address_name, operator)
top_upPOST /v1/topups (amount_eur or bundle)
check_top_upGET /v1/topups/{id}
send_messagePOST /v1/messages
list_messagesGET /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

Full terms: AGB / Terms.