Docs preview · flag spec v1
A static look at the API.
Defeat gives every AI agent a real mailbox and a verified identity card. These snippets describe the surface that is being built to deliver it: mailboxes, signed sends, webhooks, and the flag record that carries the identity.
There is no live API behind this page yet. Nothing here accepts a request. Request access and you get a sandbox key and this document as a contract the first release has to honour.
01
Authentication
Send a bearer token on every request. A key is scoped to one operator and carries that operator into every flag it signs. Rotate from the dashboard without interrupting a running agent, since the old key stays valid for an hour after you replace it.
Authorization: Bearer dft_live_…
- Two key types.
dft_test_keys run against the sandbox and can only deliver to addresses you have verified.dft_live_keys deliver to anyone. - Keys never sign on their own. The signing key for a flag lives on your domain. The API key only says which operator is asking.
02
Create a mailbox
Mint an address and attach a flag in one call. Set expires_in and the mailbox disappears with the run that owns it. Set public: true on the flag and the attribution page resolves for anyone, with no account.
curl -X POST https://api.defeatemail.net/v1/mailboxes \ -H "Authorization: Bearer $DEFEAT_KEY" \ -H "Content-Type: application/json" \ -d '{ "address": "triage@acme.defeatemail.net", "expires_in": "24h", "flag": { "agent": "triage", "operator": "acme.co", "accountable": "jordan@acme.co", "public": true } }'
{
"id": "mbx_3kQ7y2",
"address": "triage@acme.defeatemail.net",
"flag": {
"id": "agent_7Kq4x1",
"state": "published",
"url": "https://defeatemail.net/f/agent_7Kq4x1"
},
"expires_at": "2026-09-23T09:14:00Z"
}
03
Send
Send from the agent address. The recipient sees an ordinary message and can open the flag from the header. Pass mode: "review" to hold the draft for a person to approve, and the flag records who released it.
const sent = await defeat.messages.send({ from: 'triage@acme.defeatemail.net', to: 'jordan@acme.co', subject: 'Ticket 88213 needs a human', text: 'The refund is above my agent limit.', flag: { task: 'run_88213', model: 'claude-opus-5' }, }); console.log(sent.flag.url); // defeatemail.net/f/agent_7Kq4x1 console.log(sent.thread_id); // thr_4b21
- Idempotent. Pass
Idempotency-Keyand a retried send returns the original message instead of a duplicate. - Limits apply before delivery. An agent over its rate cap or outside its recipient allowlist gets
429or403, and nothing leaves.
04
Webhooks
Subscribe once and route by mailbox or by operator. Agents should not poll. Every delivery is signed with your endpoint secret and retried with backoff for 24 hours.
defeat.webhooks.on('message.received', async (event) => { if (event.verdict !== 'verified') { return defeat.messages.hold(event.message_id); } const thread = await defeat.threads.get(event.thread_id); return agent.handle(thread); });
05
Flags
A flag is the identity card for a mailbox. It answers three things for the recipient: who wrote the message, who operates the agent, and whether the address is a throwaway. Required fields are agent name, operator, accountable contact, verification state, and first_seen. Optional: purpose, website, and a contact for disputes.
{
"id": "agent_7Kq4x1",
"agent": "triage",
"operator": "acme.co",
"accountable": "jordan@acme.co",
"state": "published",
"key": "ed25519:9f3c…41ab",
"key_source": "https://acme.co/.well-known/defeat-keys",
"first_seen": "2026-03-04",
"disputes": "abuse@acme.co"
}
- A flag can sit in draft until the first outbound message. Publishing it is what makes the mailbox attributable.
- Keys live on your domain, not ours. Rotating a key at the well-known path does not invalidate mail signed under the previous one.
- The ledger keeps the flag id even when the flag was never published, so an unattributed send is still traceable internally.
- Address standing is part of the record.
first_seenis what tells a recipient the address is not a throwaway minted this morning.
06
Verdicts
Every inbound message carries exactly one verdict, resolved before it reaches your webhook. A verdict is a statement about proof, not about intent. Unsigned means nothing was proven, and it never means the sender is hostile.
| Verdict | Means | Default policy |
|---|---|---|
| verified | Signature checks out against a key the operator domain publishes, and the flag resolves. | Deliver |
| unsigned | No flag on the message. Could be a person, could be an agent. Nothing is proven either way. | Hold |
| quarantined | A flag was claimed and the signature failed, or the sending domain does not match the operator. | Quarantine and log |
Override any of these per mailbox with a rule. A recipient can also set a standing policy from their own side: no unsigned agents, no agents at all, or only operators they already know.
07
Errors and limits
Errors return a stable machine-readable code alongside the status. Rate limit state comes back on every response, so an agent can back off without guessing.
| Status | Code | When |
|---|---|---|
| 400 | invalid_address | The address is malformed or the domain is not one you control. |
| 401 | invalid_key | Missing, revoked, or wrong-environment bearer token. |
| 403 | recipient_not_allowed | The recipient is outside the allowlist set on this agent. |
| 409 | flag_unpublished | Sending from a mailbox whose flag is still in draft. |
| 422 | key_not_resolvable | The signing key is not served at the well-known path on the operator domain. |
| 429 | rate_limited | Over the per-agent send cap. Retry after the header says so. |
X-RateLimit-Limit: 600 X-RateLimit-Remaining: 584 X-RateLimit-Reset: 1790069126 Retry-After: 18
Build against this surface.
Request access and you get a sandbox key, three mailboxes, and the flag spec. Tell us what your agents send and we will tell you honestly whether Defeat is ready for it.