Scheduling and limits
Send later, and what happens when you reach a daily or monthly limit.
Schedule an email
Pass scheduled_at, between 1 minute and 30 days ahead, in ISO 8601 with an offset:
const { id, status } = await pm.emails.send({
from: "Acme Store <hello@acme.ng>",
to: "ada@example.com",
subject: "Your appointment is tomorrow",
text: "See you at 10:00.",
scheduled_at: "2026-10-28T09:00:00+01:00",
});
// status: "scheduled"
await pm.emails.update(id, { scheduled_at: "2026-10-28T08:00:00+01:00" }); // move it
await pm.emails.cancel(id); // or cancel itNatural language ("tomorrow at 9") isn't accepted.
Limits
Each organisation has a daily and a monthly limit: the lower of its verification tier's and its plan's. The daily limit resets at midnight in your organisation's time zone. GET /v1/account shows the limits, what you've used and when they reset:
const { limits } = await pm.account.get();
console.log(`${limits.daily_used} of ${limits.daily_limit} today, ${limits.waiting} waiting`);Send responses also carry X-PM-Daily-Remaining and X-PM-Credits-Remaining headers.
When you reach a limit, emails wait
By default nothing fails. The email is accepted and waits:
{ "id": "em_01JB…", "status": "deferred", "deferred_reason": "daily_limit", "deferred_until": "2026-10-28T23:00:00Z" }It sends as soon as the limit opens up: when the day resets, or sooner if you upgrade or buy credits. deferred_until is the earliest expected time. A waiting email still goes through every check when it's released, and one that's still waiting after 30 days fails with reason deferred_expired. You get an email.deferred webhook for each, and one account.quota_reached per limit window.
You can see and cancel waiting emails in the dashboard, or cancel one with POST /v1/emails/{id}/cancel. If too many are already waiting, new ones fail with 429 deferred_queue_full.
Login codes and OTPs: reject instead
A login code that arrives tomorrow is useless. Pass quota_behavior: "reject" and the request fails straight away, so your app can fall back to SMS:
try {
await pm.emails.send({ from, to, subject: `${code} is your login code`, text: `Your code is ${code}.`, quota_behavior: "reject" });
} catch (err) {
if (err instanceof PadiMailError && (err.status === 429 || err.status === 402)) await sendSms(phone, code);
else throw err;
}With reject, a limit returns 429 (daily_quota_exceeded, monthly_quota_exceeded, warmup_limit_reached, sandbox_limit_reached) or 402 (insufficient_credits, plan_limit_reached). You can make reject your organisation's default in Settings.
Request rate
Separately from sending limits, each key may make 10 requests a second, with bursts to 20 (more on paid plans). Over that you get 429 rate_limited with a Retry-After header. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.