Skip to content

Hold queue

No email is ever silently dropped. When Frontmail cannot send a message right now – because your organization is out of credits, or because your email service rejected its credentials – the message is accepted, stored safely in the hold queue (Held messages in the dashboard) and sent automatically as soon as the problem is solved.

Reason Status Triggered by
No credits held No grant with remaining credits (quota, packs and overdraft all used up) and the organization uses hold mode
Service error held_service_error The provider rejected the credentials (authentication error) and the template has no working fallback service

For credit holds, the API answers right away:

HTTP/1.1 202 Accepted
{ "message_id": "msg_01J9Z3K7Q2", "status": "held", "status_token": "…" }

The SDKs resolve with status: 'held' – it is a success, not an error. Show your visitor a normal confirmation; the email will go out once credits are available.

Service holds happen later, in the sender: the message was accepted as queued, the provider refused the credentials, the credit was refunded and the message moved to held_service_error.

Plan Hold duration
Free 3 days
Starter, Pro, Business 14 days

The duration is counted from the moment the message was held, using the plan you are on at that time. When held messages will expire within the next 24 hours, the owner, admins and billing members get a “held emails expire soon” email (at most once a day). Messages that reach the end of the hold duration are discarded (status discarded), and the same people get an email listing them, with a link to download the full list as CSV. A held message is never discarded without anyone being told.

One organization can have at most 10 % of its plan’s monthly quota on hold – at least 1,000 and at most 10,000 messages (Free, Starter, Pro: 1,000; Business 50k: 5,000; Business 100k and 200k: 10,000). When the queue is full, new requests without credit are rejected with 402 insufficient_credits and details.reason: "hold_queue_full" instead of being held. Someone who copied your public key therefore can’t fill the queue with junk that – released oldest first – would use up every new credit before your real messages. Release or discard held messages, or buy credits, to make room.

Frontmail releases the queue automatically whenever credits appear:

  • a credit pack is purchased,
  • an auto top-up succeeds,
  • you upgrade your plan,
  • a new billing period starts (subscription renewal; Free: new calendar month),
  • Frontmail support adds bonus credits.

…and, for held_service_error messages, when the service becomes healthy again (credentials updated, OAuth reconnected, or a successful health check).

Release processes held messages oldest first. For each message it reserves a credit, moves it back to queued and hands it to the sender. It stops at the first message for which there is no credit – the rest stay held, still in order. Release is safe to run concurrently; a message is never sent twice.

Messages sent with the public key are released automatically only from credits other than the overdraft, unless you allow public-key traffic to use it in Billing → Buy credits → Spending by public-key traffic. If only overdraft is left, such messages stay held while other messages are released. A manual Release in the dashboard may use the overdraft for them.

Held messages are rendered with the template version that was current when they were accepted.

Held messages in the dashboard lists every held message, oldest first, with the columns Held since, template (with the subject), recipient, Reason and Expires. Rows that expire within 24 hours are highlighted. Filter by reason with All / No credits / Service error. Owners, admins and developers can:

  • Release N – release the selected messages. Useful after fixing a service, or to send the most important messages first when you only have a few credits. Messages are released oldest first while there are credits; the rest stay held and the dashboard tells you so.
  • Release all – release every held message, oldest first, as far as credits allow.
  • Discard N – discard the selected messages after a confirmation. They are never sent and don’t use credits. This can’t be undone.

Owners and billing members also see a Buy credits button on the page. If your organization uses reject mode, the page shows a note that new requests are rejected instead of held.

The top bar of the dashboard always shows how many credits are left (N requests left). Click it for the breakdown by source (subscription, credit packs, bonus, overdraft) and the nearest expiry. When messages are held, a red badge with their number appears next to it and links to Held messages.

Each organization chooses what happens when there are no credits, in Security → When you run out of credits (owners and admins):

Mode Response Use when
Hold emails (hold, default, recommended) 202 with status: "held", message stored and sent later You’d rather deliver late than never – contact forms, orders, bookings
Reject requests (reject) 402 insufficient_credits, nothing stored Late delivery is worse than none (one-time codes, time-sensitive alerts) and your code handles the error

In reject mode the SDKs throw an InsufficientCreditsError. Service holds (held_service_error) happen in both modes, because the request was already accepted.

Email When Sent to
Emails are waiting – you’re out of credits The first time a message is held in a calendar month owner, admins, billing
Held emails expire soon At most once a day while held messages will expire within 24 hours owner, admins, billing
Held emails discarded After expiry, with the list of discarded messages and a CSV link owner, admins, billing
Service not working / working again When a service fails authentication / recovers owner, admins

See Notifications for all emails.

const res = await send('svc_01J9…', 'tpl_order', params);
if (res.status === 'held') {
// Accepted and safe – it will be sent automatically.
}
// Later (public-key clients need the status token from the send result):
const status = await getStatus(res.messageId, { token: res.statusToken });
console.log(status.status); // "held" → "queued" → "sent" …