API for agencies For agencies
Query delivery status of individual mails, bounces and usage from your own application, with an API key, read-only.
With the MailBridge API you query from your own application what happened to a mail you sent: delivered, deferred or undeliverable, including the verbatim response of the receiving server. On top of that come bounce lists and usage per end client.
The API is read-only and meant for server-to-server calls. An API key never belongs in a website or app that runs on the user's device.
Creating a key
Settings → API keys → Create key. Give it a label, optionally an end client (the key then sees only their data) and optionally an expiry date.
The key is shown exactly once. We store only a checksum of it. Lost means create a new one. A key can be revoked at any time; requests using it are refused immediately after.
Authentication
Every request carries the key in the Authorization header:
curl -H "Authorization: Bearer mbk_live_…" \
https://my.mailbridge.email/api/v1/me
Alternatively X-API-Key: mbk_live_…. Your first call should always be /api/v1/me. The response tells you which account the key belongs to, whether it is restricted to an end client, what your rate limit is and how many days of sending history are available.
Tracking a single mail
To find an individual mail again later you need its queue ID. It is in the SMTP response your mailer receives on submission: 250 2.0.0 Ok: queued as 4ZxK7p2Yz3.
Nodemailer returns it in info.response:
const info = await transporter.sendMail({ /* … */ })
const queueId = info.response.match(/queued as (\S+)/)?.[1]
// store queueId together with the recipient address in your own database
With PHPMailer you read it from $mail->getSMTPInstance()->getLastReply(); in other libraries the field is usually called response or smtp_reply.
Then:
curl -H "Authorization: Bearer $KEY" \
https://my.mailbridge.email/api/v1/messages/4ZxK7p2Yz3
{
"queue_id": "4ZxK7p2Yz3",
"status": "delivered",
"sender": "info@baeckerei-brotzeit.de",
"recipients": ["kunde@example.com"],
"first_seen": "2026-07-31T09:12:04Z",
"last_seen": "2026-07-31T09:41:18Z",
"attempts": [
{ "status": "deferred", "occurred_at": "2026-07-31T09:12:04Z", "smtp_response": "451 4.7.1 try again later",
"reason": { "code": "4.7.1", "permanent": false, "provider_block": null, "key": "greylisted_throttled" } },
{ "status": "sent", "occurred_at": "2026-07-31T09:41:18Z", "target_server": "mx01.example.com",
"reason": null }
]
}
Why several attempts? The sending server logs every delivery attempt per recipient. A mail that was first deferred and then delivered has two entries. status is the result across all attempts:
status |
Meaning |
|---|---|
delivered |
At least one attempt succeeded, the mail is out. |
deferred |
Still in delivery, retried automatically. |
bounced |
Permanently undeliverable (5.x.x). |
rejected |
Refused by the receiving server. |
auth_failed |
Login to the sending server failed. |
The reason field
For every attempt, reason says why it ended the way it did, except when it worked:
"reason": null
null means: no reason needed, the mail was delivered. It is not a missing value and not a failure of the request. Only status: "sent" returns null.
For every other status reason is filled:
| Field | Meaning |
|---|---|
code |
DSN status reported by the receiving server, e.g. 5.1.1 or 4.7.1. |
permanent |
true = permanent (5.x.x) → put the address on your suppression list. false = temporary (4.x.x) → delivery continues, do not suppress. |
provider_block |
Recognised block by a large provider (microsoft, google, yahoo, t-online, united-internet), otherwise null. |
key |
Stable machine key of the explanation, e.g. unknown_recipient, mailbox_full, greylisted_throttled. Program against this value; the texts in the portal may change. |
The order in your own application: check status first, then read reason.
const res = await fetch(`${BASE}/api/v1/messages/${queueId}`, { headers })
const mail = await res.json()
if (mail.status === 'delivered') {
markDelivered(queueId) // reason is null here
}
else {
const last = mail.attempts.at(-1)
if (last.reason?.permanent) suppress(last.recipient) // permanent: stop writing to it
else scheduleRecheck(queueId) // temporary: look again later
}
If you do not know the queue ID, query by recipient:
curl -H "Authorization: Bearer $KEY" \
"https://my.mailbridge.email/api/v1/messages?recipient=kunde@example.com&date_from=2026-07-01T00:00:00Z"
Further filters: status, sender, queue_id, search (free text), date_from, date_to, client (end client), account and page/per_page (max. 200).
Bounces
curl -H "Authorization: Bearer $KEY" \
"https://my.mailbridge.email/api/v1/bounces?per_page=100"
Here reason is always filled: every entry is a failure or a deferral, and the null case only occurs on /messages. What matters is reason.permanent: on true (5.x.x) the address should move to your suppression list, on false (4.x.x) the attempt continues, so do not suppress.
The portal keeps its own list of permanently undeliverable addresses under Blocklist. It is information, like this field: neither the portal nor the API holds any mail back.
Usage
GET /api/v1/usage returns the current month: mails sent, plan limit, booked extra packs, the currently effective limit and the split per end client (clients[].sent, clients[].share_cents), which is the basis for your onward billing. GET /api/v1/usage/history?months=12 returns up to twelve months.
GET /api/v1/clients lists your end clients with their IDs; that ID is the value for the client filter.
Limits you should know
- Sending history: 30 days. Individual events live on the sending servers and are kept there for 30 days. Older mail cannot be queried; only usage reaches back 12 months.
- No subject, no message ID of your own. The sending servers log sender, recipient and queue ID, not the content. Store the queue ID and the recipient in your application and you will find every mail again.
degraded: if a sending server was unreachable during the request, its name appears in this field and the list may be incomplete. Check for[]before drawing conclusions from an empty result.capped: on accounts with many accesses, very deep pages cannot be paginated exactly. Narrow the period or query per end client instead.- Rate limit: 120 requests per minute and key. Every response carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; on exceeding it you get429withRetry-After.
Error codes
| HTTP | message |
Cause |
|---|---|---|
| 401 | api_key_missing |
No Authorization or X-API-Key header. |
| 401 | api_key_invalid |
Key unknown, revoked or expired. |
| 403 | agency_required |
The API belongs to the agency line. |
| 403 | api_access_blocked |
Access blocked for the account, please contact support. |
| 403 | subscription_inactive |
No active subscription. |
| 403 | trial_expired |
The trial ended without a subscription. Once payment is set up in the portal, existing keys work again. |
| 404 | message_not_found |
Queue ID not found in the period (older than 30 days?). |
| 405 | method_not_allowed |
The API is read-only, only GET is allowed. |
| 429 | rate_limited |
Rate limit reached, observe Retry-After. |
No account yet? Try it free for 30 days, payment details are only needed afterwards.
Diese Seite auf Deutsch: deutsche Fassung. The rest of this website is in German.