← All help topics

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 keysCreate 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-Remaining and X-RateLimit-Reset; on exceeding it you get 429 with Retry-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.

Diese Seite auf Deutsch: deutsche Fassung. The rest of this website is in German.