Safe evaluation and financial controls
Physical-mail API with sandbox and spending limits
Read the capability instead of assuming it, rehearse with dryRun, and layer independent limits before giving an agent production access.
Sandbox mode exists in FrankKi source as a per-account grant, it is off by default, and a production mcp_health probe on 2026-08-13 returned it as status "unavailable" with details allowed:false. Whether any other account currently holds the grant is not measured. The documented substitute is dryRun: true on order_send, which runs the same validation, the same gates and the same pricing as a live send and returns a preview without printing, mailing or charging anything. For production, combine minimum scopes, an exact quote, maxCostEuros, account or key caps, wallet balance, idempotency, and separate human approval.
Read the capability instead of assuming it
Call mcp_health first. It returns capabilities.sandbox and a setup step keyed sandbox. On a production probe run on 2026-08-13 (requestId req_SF83SO42MGXYYERT) that step came back status "unavailable" with details allowed:false and tokenIsTest:false: sandbox mode is granted per account, it is off by default, and it is currently unavailable. Source enables it through a deployment flag or an active per-account stage-mode grant; this page does not claim that any particular account currently has either.
So do not plan an evaluation around a Test-Token. Create a narrowly scoped production token at https://frankki.app/dashboard/tokens, connect it to https://mcp.frankki.app, and rehearse with dryRun: true. If you do hold a sandbox grant, mcp_health says so. Never reinterpret an unavailable sandbox as permission to send live mail, and never drop dryRun to work around a failed capability check.
mcp_health({ "checkRender": true })
// Read setup.steps[key="sandbox"].status and capabilities.sandbox.
// "unavailable" means rehearse with dryRun, not with a Test-Token.Know exactly what the rehearsal exercises
A dryRun order_send runs the full request: address and content validation, the sender-profile and data-processing gates, and the real price for the real destination. It returns the same error codes a live send would and prints nothing, mails nothing and charges nothing. It proves the request would be accepted; it does not prove that a postal provider, the live approval queue, a wallet debit or a destination product was exercised. Those first happen on a live send.
- dryRun is a non-dispatch execution option on order_send. It is not sandbox access and does not grant it.
- Sandbox mode is a separate per-account credential grant, off by default and currently unavailable.
- A preview renders content but is not a dispatch approval.
- A dryRun result is not provider tracking and not proof of delivery.
Start with minimum scopes
The generated scope vocabulary separates reads from writes. Useful reads include address:read, letter:read, wallet:read, order:read, approval:read, and archive:read. order:send enables send-side actions. approval:decide is a separate write scope and should remain off model-operated keys when a human-operated approval session can hold it instead.
The source bundles are read_only, send_only, and full_mcp, but examples should prefer an explicit minimum set. Existing grants do not automatically gain newly introduced scopes, and token administration stays behind the human dashboard session.
Layer price and spending controls
No single number is the complete budget boundary. Quote the actual destination and reviewed letter, carry the returned price fields into the send, and set a caller-owned ceiling. Then retain account controls and funding checks around it.
- shipping_quote returns the current destination-aware estimate. A dry-run order_send is more precise for the final letter and attachments.
- priceVersion and quotedUnitPriceCents make a changed price fail with PRICE_CHANGED before charge.
- maxCostEuros is a per-call ceiling. It is not a daily budget or wallet balance.
- Wallet checks can enforce daily letter and cent caps. A supplied client or Mandant can have a narrower monthly cap.
- Sub-account controls should narrow account controls, never silently widen them.
- Wallet balance and reservation logic prevent unfunded dispatch. MCP does not collect card data in the agent interaction.
shipping_quote({
"letterId": "REVIEWED_LETTER_ID",
"pageCount": 1,
"color": false,
"deliveryType": "standard",
"country": "DE"
})
order_send({
"letterId": "REVIEWED_LETTER_ID",
"recipientName": "Example Recipient",
"recipientStreet": "Example Street",
"recipientHouseNumber": "1",
"recipientZip": "10115",
"recipientCity": "Berlin",
"recipientCountry": "DE",
"deliveryType": "standard",
"priceVersion": "VERSION_FROM_QUOTE",
"quotedUnitPriceCents": 123,
"maxCostEuros": 5,
"approvalMode": "review",
"clientOrderId": "case-482-send-v1",
"dryRun": true
})Boundary: 123 is an illustrative placeholder, not a price claim. Use the exact value returned for the exact request. Do not automatically raise a limit, top up a wallet, change the postal product, or retry PRICE_CHANGED without a new quote and renewed approval.
Fail closed and retry idempotently
Derive clientOrderId from a stable business operation before the first call and persist it outside model text. A retry of the same operation reuses the same key and payload. Never respond to an unknown timeout with a new random key. If the payload changes under the same key, IDEMPOTENCY_CONFLICT is the expected stop signal.
A failing dryRun, an unavailable quote, PRICE_CHANGED, insufficient wallet funds, a reached cap, or a rejected approval are stop conditions. Surface the returned error and recovery path to a human. Do not silently downgrade delivery type or swap credentials.
Next steps
Related documentation and decisions
Trust and live status
Machine-readable service facts and the current operational status of the partner interface are publicly available at all times, no login needed.