Usage and email metrics API
Plan consumption and delivery, open and click metrics.
Check how much of the plan the team has used, and the delivery metrics for your emails.
#Plan usage
GET/usage
The sending quotas for the day and the month (days and months in UTC), domains, inboxes, team members, broadcasts this month (broadcasts: sent, sending or scheduled), automation runs started this month, AI credits, whether sending is paused, and the requests-per-second limit. limit: null means unlimited.
Which keys can call it: any key, full access or sending_access, and the dashboard. So an app that only has a sending key can read the plan's request limit (rate_limit), the daily and monthly quotas and the sending pause. For sending_access keys, email_verifications does not include the list of Validation add-on packs (grants), only the totals, and reputation has no history; the rest of the response is the same.
Fields: plan (the sending plan and the marketing plan), emails.daily and emails.monthly (used, limit and resets_at; in emails.daily, also plan_limit and limited_by, see below; sent repeats used and received is always 0), emails.credits (the Sends add-on reserve: see below), contacts (used, limit, plan_limit and packs of the marketing plan's contacts: see below), audience (see below), overage inside emails.monthly and contacts (whether overage is turned on for the product, enabled; whether it is in effect, active; and what went over the limit in the period, extra, in packs of 1,000, packs; for contacts, peak is the highest number of the period), domains, team_members, inboxes, broadcasts, automation_runs, ai_credits (with remaining and resets_at), email_verifications (the email validation credits: used, limit, remaining, monthly_remaining, grant_credits, grants, resets_at and free_preview_remaining), features, paused, sending, reputation (see below) and rate_limit. The monthly email quota and the contact limit follow the volume tier you subscribed to (see Plans).
emails.daily.limit is the daily limit in force: the lower of the plan's and the account reputation ceiling (null: unlimited). emails.daily.plan_limit is the plan's alone, and emails.daily.limited_by says which of the two limits: plan, reputation, or null when there is no limit. A daily limit set for the team by Postal's team replaces the reputation ceiling, and then limited_by is plan. When the limit is reached, sends receive 429 daily_quota_exceeded; when the reputation is what limits, the message is Your account is at reputation level 2 of 5, which allows 2000 emails a day while it builds its sending history. The limit grows with each day of sending without bounces or complaints. It resets at 00:00 UTC.
reputation is the account reputation: score (from 0 to max_score, 100), level (from 1 to max_level, 5), daily_limit (the level's ceiling; null at level 5, with no reputation ceiling), next (the next level, with level, score and daily_limit; null at level 5), levels (the table of levels, each with level, the minimum score, daily_limit and max_bounce_rate, the permanent bounce rate that pauses sending) and rules (the points per clean day by volume, gain, each step with the highest level it takes the account to, max_level; the rates of a clean day, clean, and of a severe day, severe; and the penalties warning_penalty, severe_penalty and pause_penalty). Rates are fractions. history holds up to 30 changes, newest first, each with day, kind, delta (the points added or taken), score (the score after it) and created_at. kind is day (the scoring of a day, with outcome: clean, quiet with fewer than 10 emails and no change, warning or severe; and with sent, bounce_rate, complaint_rate, content_rejected_rate and all_bounce_rate; and with capped_at_level when the gain stopped at the top of the level the day's volume allows), pause (a sending pause, with reason_code; outcome is null) or staff (an adjustment by Postal's team; the team's note is not shown). sending_access keys get the block without history. See Account reputation.
sending shows whether the team's sending is paused for reputation: paused, paused_at, reason (the reason as text, in the language of the request: Accept-Language: pt-BR or en, English without the header), reason_code (bounce_rate, complaint_rate, content_rejected, all_bounce_rate or manual; null when not paused and on old pauses), reason_details (the numbers behind the reason, with rates as fractions and the window in window_days or, for the last-24-hours window, window_hours; null when not paused, for manual and on old pauses: see sending pauses) and review_requested_at (when a review was requested, or null). The top-level paused lists the resources the current plan paused for being over the limit, as [{"key", "label", "count"}] (empty when nothing is paused; the label follows Accept-Language): see Plan limits and features.
audience is the contact Audience measured against the marketing plan's limit, so you can be warned before the capture of transactional emails stops: used (contacts), limit (null: unlimited), percent (a whole number, from 0 to over 100; null without a limit), capture_enabled (the team keeps the recipients of transactional emails in the Audience) and capture_paused (capture is on but the limit was reached, so new recipients are not being kept). The owner and admins also get emails at 80%, 90% and 100% of the limit.
contacts.limit is the limit in force: the plan's contacts plus the packs of the Contacts add-on, when the team has them; plan_limit is the plan's alone. contacts.packs has contacts (what the packs add; 0 without packs), effective (equal to limit), scheduled (null, or {"effective_at", "pack_contacts", "effective_limit"} when a reduction is waiting for the renewal: effective_limit is the limit after it) and, for full-access keys, items (the team's packs, with pack_id, name, contacts, quantity, scheduled_quantity, contacts_total, max_quantity, amount, currency and interval). sending_access keys see the totals, but not items. The audience block and the usage alerts use the same limit.
marketing_recipients counts the different contacts that received campaigns or automation emails in the cycle, which cannot go over the same contact limit: used, limit (null: unlimited), remaining, percent (a whole number; null without a limit), billed_above_limit (true when contact overage is in effect: the ones above the limit are billed, not refused), cycle_start and resets_at (when the cycle renews and the counter goes back to zero). Contacts that already received a campaign in the cycle keep counting until resets_at, even if deleted. In contacts.overage, recipients repeats used: contact overage is measured by the higher of peak and recipients.
emails.credits is the Sends add-on reserve, apart from the quota: emails.monthly.used and limit are still the plan's, and the credits are never added to the limit. It is only used once the monthly quota runs out. It has reserve (always true), balance (the credits that can be used now), used_this_month (what sends took from the reserve this month), in_use (true when the monthly quota is exhausted and there is a balance, so sends come out of the reserve), next_expires_at (the date the first pack with an expiry runs out, or null) and, for full-access keys, grants: the packs and bonuses with a balance, in the order they are used (the ones expiring soonest first, the ones without expiry last), each with id, source (purchase or staff), name, emails, remaining and expires_at (null: no expiry). sending_access keys see the totals, but not grants. In email_verifications.grants, expires_at is also null for a pack with no expiry.
cURL
curl https://api.postal.so/usage \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/usage', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/usage",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('usage', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "usage",
"plan": {
"id": "pro",
"name": "Pro",
"marketing_id": "free",
"marketing_name": "Free"
},
"emails": {
"daily": {
"used": 1840,
"limit": 5000,
"plan_limit": null,
"limited_by": "reputation",
"sent": 1840,
"received": 0,
"resets_at": "2026-10-25T00:00:00Z"
},
"monthly": {
"used": 31207,
"limit": 50000,
"sent": 31207,
"received": 0,
"resets_at": "2026-11-01T00:00:00Z",
"overage": {
"enabled": false,
"active": false,
"unit": "emails",
"extra": 0,
"packs": 0
}
},
"credits": {
"reserve": true,
"balance": 20000,
"used_this_month": 0,
"in_use": false,
"next_expires_at": "2027-01-15T12:00:00Z",
"grants": [
{
"id": "019a3f20-6c7d-7e8f-9a01-2b3c4d5e6f70",
"source": "purchase",
"name": "Pack A",
"emails": 20000,
"remaining": 20000,
"expires_at": "2027-01-15T12:00:00Z"
}
]
}
},
"contacts": {
"used": 3120,
"limit": 5000,
"plan_limit": 5000,
"packs": {
"contacts": 0,
"effective": 5000,
"scheduled": null,
"items": []
},
"overage": {
"enabled": false,
"active": false,
"unit": "contacts",
"extra": 0,
"packs": 0,
"peak": 3120,
"recipients": 2870
}
},
"marketing_recipients": {
"used": 2870,
"limit": 5000,
"remaining": 2130,
"percent": 57,
"billed_above_limit": false,
"cycle_start": "2026-10-25T14:00:00Z",
"resets_at": "2026-11-25T14:00:00Z"
},
"audience": {
"used": 3120,
"limit": 5000,
"percent": 62,
"capture_paused": false,
"capture_enabled": true
},
"domains": {
"used": 2,
"limit": 10
},
"team_members": {
"used": 4,
"limit": 10
},
"inboxes": {
"used": 1,
"limit": 10
},
"features": {
"custom_inbound_domains": true,
"postal_branding": false,
"ai_assistant": true,
"email_verification": true
},
"broadcasts": {
"used": 3,
"limit": null
},
"automation_runs": {
"used": 318,
"limit": 10000
},
"ai_credits": {
"used": 12,
"limit": 100,
"remaining": 88,
"resets_at": "2026-11-01T00:00:00Z"
},
"email_verifications": {
"used": 120,
"limit": 5000,
"remaining": 5880,
"monthly_remaining": 4880,
"grant_credits": 1000,
"grants": [
{
"id": "019a1d40-5b6c-7d7e-8f90-1a2b3c4d5e6f",
"source": "purchase",
"name": "1,000-credit pack",
"credits": 1000,
"remaining": 1000,
"expires_at": "2027-10-20T00:00:00Z"
}
],
"resets_at": "2026-11-01T00:00:00Z",
"free_preview_remaining": 880
},
"paused": [],
"sending": {
"paused": false,
"paused_at": null,
"reason": null,
"reason_code": null,
"reason_details": null,
"review_requested_at": null
},
"reputation": {
"score": 23,
"max_score": 100,
"level": 3,
"max_level": 5,
"daily_limit": 5000,
"next": {
"level": 4,
"score": 30,
"daily_limit": 10000
},
"levels": [
{
"level": 1,
"score": 0,
"daily_limit": 500,
"max_bounce_rate": 0.03
},
{
"level": 2,
"score": 5,
"daily_limit": 2000,
"max_bounce_rate": 0.04
},
{
"level": 3,
"score": 15,
"daily_limit": 5000,
"max_bounce_rate": 0.05
},
{
"level": 4,
"score": 30,
"daily_limit": 10000,
"max_bounce_rate": 0.05
},
{
"level": 5,
"score": 60,
"daily_limit": null,
"max_bounce_rate": 0.05
}
],
"rules": {
"gain": [
{
"sent": 10,
"points": 3,
"max_level": 3
},
{
"sent": 100,
"points": 5,
"max_level": 4
},
{
"sent": 1000,
"points": 6,
"max_level": 5
}
],
"clean": {
"bounce_rate": 0.02,
"complaint_rate": 0.001,
"content_rejected_rate": 0.01,
"all_bounce_rate": 0.1
},
"severe": {
"bounce_rate": 0.05,
"complaint_rate": 0.003,
"content_rejected_rate": 0.05,
"all_bounce_rate": 0.2
},
"warning_penalty": 10,
"severe_penalty": 25,
"pause_penalty": 40
},
"history": [
{
"day": "2026-10-23",
"kind": "day",
"outcome": "clean",
"delta": 5,
"score": 23,
"sent": 340,
"bounce_rate": 0.0058,
"complaint_rate": 0,
"content_rejected_rate": 0,
"all_bounce_rate": 0.0117,
"created_at": "2026-10-24T01:00:12Z"
},
{
"day": "2026-10-22",
"kind": "day",
"outcome": "quiet",
"delta": 0,
"score": 18,
"sent": 6,
"bounce_rate": 0,
"complaint_rate": 0,
"content_rejected_rate": 0,
"all_bounce_rate": 0,
"created_at": "2026-10-23T01:00:09Z"
},
{
"day": "2026-10-21",
"kind": "staff",
"outcome": null,
"delta": 3,
"score": 18,
"created_at": "2026-10-21T14:32:40Z"
}
]
},
"rate_limit": {
"limit": 10,
"duration": "1s"
}
}
{
"sending": {
"paused": true,
"paused_at": "2026-10-24T15:20:02Z",
"reason": "Bounce rate of 6.40% in the last 7 days (limit: 5.00%).",
"reason_code": "bounce_rate",
"reason_details": {
"bounce_rate": 0.064,
"limit": 0.05,
"window_days": 7
},
"review_requested_at": null
}
}
{
"sending": {
"paused": true,
"paused_at": "2026-10-24T15:20:02Z",
"reason": "Rate of bounces of every type of 17.20% in the last 24 hours (limit: 15.00%).",
"reason_code": "all_bounce_rate",
"reason_details": {
"all_bounce_rate": 0.172,
"limit": 0.15,
"window_hours": 24
},
"review_requested_at": null
}
}
{
"emails": {
"monthly": {
"used": 50210,
"limit": 50000,
"sent": 50210,
"received": 0,
"resets_at": "2026-11-01T00:00:00Z",
"overage": {
"enabled": false,
"active": false,
"unit": "emails",
"extra": 0,
"packs": 0
}
},
"credits": {
"reserve": true,
"balance": 24790,
"used_this_month": 210,
"in_use": true,
"next_expires_at": "2027-01-15T12:00:00Z",
"grants": [
{
"id": "019a3f20-6c7d-7e8f-9a01-2b3c4d5e6f70",
"source": "purchase",
"name": "Pack A",
"emails": 20000,
"remaining": 19790,
"expires_at": "2027-01-15T12:00:00Z"
},
{
"id": "019a3f21-7d8e-7f90-8b12-3c4d5e6f7081",
"source": "purchase",
"name": "Pack B",
"emails": 5000,
"remaining": 5000,
"expires_at": null
}
]
}
}
}
{
"contacts": {
"used": 3120,
"limit": 25000,
"plan_limit": 5000,
"packs": {
"contacts": 20000,
"effective": 25000,
"scheduled": {
"effective_at": "2026-11-20T12:00:00+00:00",
"pack_contacts": 10000,
"effective_limit": 15000
},
"items": [
{
"pack_id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"name": "Pack A",
"contacts": 10000,
"quantity": 2,
"scheduled_quantity": 1,
"contacts_total": 20000,
"max_quantity": 10,
"amount": 4900,
"currency": "brl",
"interval": "month"
}
]
},
"overage": {
"enabled": false,
"active": false,
"unit": "contacts",
"extra": 0,
"packs": 0,
"peak": 3120
}
}
}
#Email metrics
GET/emails/metrics
Counts and rates calculated from the events in the period. Without dimensions, only the totals are returned; with period, you get one row per period (empty ones included). With domain or broadcast, each row also carries the name (domain_name, broadcast_name). Results are cached for up to 15 minutes.
Counts: sent, delivered, delivery_delayed, bounced, bounced_permanent, bounced_transient, bounced_undetermined, complained, failed, suppressed, opened, unique_opened, clicked, unique_clicked, received, unsubscribed. Rates (from 0 to 1): delivery_rate (delivered / sent), open_rate and click_rate (unique / delivered), bounce_rate (bounces / sent), complaint_rate and unsubscribe_rate (over delivered).
Query parameters
start_datestringStart, in ISO 8601. A date without a time means 00:00 in thetimezone. Default: 6 days before the end, at midnight.end_datestringEnd, in ISO 8601. A date without a time means 00:00 in thetimezone: to include the whole last day, send2026-10-22T23:59:59. Default (and maximum): now. If it comes beforestart_date, the response is422.timezonestringIANA time zone for dates without one and for the periods, such asAmerica/Sao_Paulo. Default:UTC.granularitystringhourly,daily(default),weeklyormonthly. At most 10,000 periods.metricsstringMetrics separated by commas (or the parameter repeated). Default: all.dimensionsstringGroupings:period,domain,emailand/orbroadcast.domain_idstringUp to 100 domain IDs, separated by commas.email_idstringUp to 100 email IDs, separated by commas.broadcast_idstringUp to 100 broadcast IDs, separated by commas.
Errors
- 422
validation_errorInvalid date, time zone, granularity, metric or dimension, or a period too long for the granularity.
cURL
curl 'https://api.postal.so/emails/metrics?start_date=2026-10-20&end_date=2026-10-22T23:59:59&timezone=America/Sao_Paulo&metrics=sent,delivered,delivery_rate&dimensions=period' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/metrics?start_date=2026-10-20&end_date=2026-10-22T23:59:59&timezone=America/Sao_Paulo&metrics=sent,delivered,delivery_rate&dimensions=period', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/emails/metrics",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"start_date": "2026-10-20",
"end_date": "2026-10-22T23:59:59",
"timezone": "America/Sao_Paulo",
"metrics": "sent,delivered,delivery_rate",
"dimensions": "period",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('emails/metrics', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'start_date' => '2026-10-20',
'end_date' => '2026-10-22T23:59:59',
'timezone' => 'America/Sao_Paulo',
'metrics' => 'sent,delivered,delivery_rate',
'dimensions' => 'period',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "metrics",
"start_date": "2026-10-20T03:00:00.000Z",
"end_date": "2026-10-23T02:59:59.000Z",
"metrics": ["sent", "delivered", "delivery_rate"],
"dimensions": ["period"],
"granularity": "daily",
"totals": {
"sent": 3120,
"delivered": 3087,
"delivery_rate": 0.9894
},
"data": [
{
"period": "2026-10-20T00:00:00-03:00",
"sent": 1004,
"delivered": 995,
"delivery_rate": 0.991
},
{
"period": "2026-10-21T00:00:00-03:00",
"sent": 1130,
"delivered": 1116,
"delivery_rate": 0.9876
},
{
"period": "2026-10-22T00:00:00-03:00",
"sent": 986,
"delivered": 976,
"delivery_rate": 0.9899
}
]
}