A2P Messaging API · HTTP
SMS traffic metrics
Read the volume, delivery outcomes and cost of the SMS sent with your API tokens, per UTC day or month, with a breakdown by country, network and token, in a single call to GET /sms/metrics.
Delivery reports tell you what happened to each message, one webhook call at a time. Metrics answer the aggregate questions in one request: how many messages you sent last week, how many were delivered, to which countries and networks, through which token, and what they cost. You get the figures without storing and counting every DLR yourself, which makes this the endpoint for usage dashboards, delivery-rate monitoring and cost reconciliation.
Request
SMS traffic metrics of your API tokens over a window of UTC days or months.
Authenticate with an api_sms token, the same one you send with (see Authentication). The token you call with does not narrow the figures: any of your tokens reads the traffic of all your API tokens, deleted ones included, unless you pass tokenId.
# September 2026, one row per day
curl "https://api.instasent.com/transactional/v1/sms/metrics?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $INSTASENT_TOKEN"
# January to September 2026, one row per month, one token only
curl "https://api.instasent.com/transactional/v1/sms/metrics?granularity=month&from=2026-01&to=2026-09&tokenId=66f1a2b3c4d5e6f7a8b9c0d1" \
-H "Authorization: Bearer $INSTASENT_TOKEN"Query parameters
fromstringrequiredFirst day of the window, included, as YYYY-MM-DD in UTC. With granularity=month, YYYY-MM is accepted too, and a full date is taken as its whole month.
tostringrequiredLast day of the window, included, in the same format as from. It must not be earlier than from.
granularitystringdefault: daySize of each bucket: day or month. With day the window can span at most 92 days; with month, at most 1100 days (about three years).
tokenIdstringNarrows the figures to one token. Use the tokenId returned in breakdown.byToken. A token that is unknown or sent nothing in the window returns zeros, not an error.
What the figures count
- Only charged messages. A message that was not charged does not appear here, so these figures are your billed traffic, not every request you made. See What is billed for the cases where a message is blocked and not charged.
- By charge date, in UTC. Each message lands in the UTC day (or month) it was charged, whatever your local time zone. A message charged at 00:30 on 1 October in Madrid (UTC+2) counts on 30 September.
- Test traffic is left out.
costis what you were charged, in EUR, rounded to four decimals.- Each message is counted once, under its current status:
delivered,failed,error,sent,enqueued,expiredorrejected. They mean the same as in the DLR status list;enqueuedis a message still waiting to be handed to the carrier. Statuses without a counter of their own (such asbufferedoraccepted) only add tomessages, so the status counters need not add up tomessages. noDRcounts messages that have not received any delivery report. It overlaps the status counters (asentmessage with no report is in both), so never add it to them.
To compute a delivery rate, divide delivered by messages. On the most recent buckets that ratio can still rise as late reports arrive (see below).
Freshness: dataUpTo and finalBefore
The figures are aggregated periodically, not computed in real time: a message you sent a minute ago is not in them yet. Two fields of every response tell you how far to trust each bucket.
dataUpTois the instant (ISO 8601, UTC) up to which charged traffic is included. It isnullwhen that instant is not known at the time of the call; the figures are returned anyway.finalBeforeis the first bucket that may still change, in the same format as the buckets (YYYY-MM-DDby day,YYYY-MMby month). Every bucket before it is final. Buckets from it on are provisional, because carriers keep sending delivery reports after the send, sometimes days later, and each one can move a message fromsenttodeliveredorfailed.
Response
A 200 returns the metrics in entity:
{
"entity": {
"channelType": "sms",
"granularity": "day",
"from": "2026-09-28",
"to": "2026-09-30",
"timezone": "UTC",
"dataUpTo": "2026-09-30T22:00:00+00:00",
"finalBefore": "2026-09-29",
"totals": { "messages": 2000, "parts": 2170, "delivered": 1881, "failed": 27, "error": 3, "sent": 73, "enqueued": 3, "expired": 5, "rejected": 5, "noDR": 79, "cost": 86.8 },
"series": [
{ "date": "2026-09-28", "messages": 1200, "parts": 1310, "delivered": 1150, "failed": 18, "error": 2, "sent": 21, "enqueued": 0, "expired": 4, "rejected": 3, "noDR": 23, "cost": 52.4 },
{ "date": "2026-09-29", "messages": 0, "parts": 0, "delivered": 0, "failed": 0, "error": 0, "sent": 0, "enqueued": 0, "expired": 0, "rejected": 0, "noDR": 0, "cost": 0 },
{ "date": "2026-09-30", "messages": 800, "parts": 860, "delivered": 731, "failed": 9, "error": 1, "sent": 52, "enqueued": 3, "expired": 1, "rejected": 2, "noDR": 56, "cost": 34.4 }
],
"breakdown": {
"byCountry": [
{ "country": "ES", "messages": 1500, "parts": 1630, "delivered": 1412, "failed": 20, "error": 2, "sent": 55, "enqueued": 2, "expired": 4, "rejected": 3, "noDR": 59, "cost": 65.2 },
{ "country": "PT", "messages": 500, "parts": 540, "delivered": 469, "failed": 7, "error": 1, "sent": 18, "enqueued": 1, "expired": 1, "rejected": 2, "noDR": 20, "cost": 21.6 }
],
"byNetwork": [
{ "network": "21407", "messages": 900, "parts": 980, "delivered": 848, "failed": 12, "error": 1, "sent": 33, "enqueued": 1, "expired": 3, "rejected": 2, "noDR": 35, "cost": 39.2 },
{ "network": "21401", "messages": 600, "parts": 650, "delivered": 564, "failed": 8, "error": 1, "sent": 22, "enqueued": 1, "expired": 1, "rejected": 1, "noDR": 24, "cost": 26 },
{ "network": "26806", "messages": 500, "parts": 540, "delivered": 469, "failed": 7, "error": 1, "sent": 18, "enqueued": 1, "expired": 1, "rejected": 2, "noDR": 20, "cost": 21.6 }
],
"byToken": [
{ "tokenId": "66f1a2b3c4d5e6f7a8b9c0d1", "name": "Production", "deleted": false, "messages": 1800, "parts": 1950, "delivered": 1690, "failed": 25, "error": 3, "sent": 69, "enqueued": 3, "expired": 5, "rejected": 4, "noDR": 75, "cost": 78 },
{ "tokenId": "65a0b1c2d3e4f5a6b7c8d9e0", "name": "Old integration", "deleted": true, "messages": 200, "parts": 220, "delivered": 191, "failed": 2, "error": 0, "sent": 4, "enqueued": 0, "expired": 0, "rejected": 1, "noDR": 4, "cost": 8.8 }
]
}
}
}In this example the 28th is final, while the 29th and 30th may still change. The 29th had no traffic and still has its row.
channelTypestringAlways sms.
granularitystringday or month, as requested.
fromstringFirst bucket of the window: YYYY-MM-DD by day, YYYY-MM by month (even if you passed a full date).
tostringLast bucket of the window, in the same format.
timezonestringAlways UTC: the zone every bucket is cut in.
dataUpTostring | nullThe instant up to which charged traffic is included, ISO 8601. null when unknown.
finalBeforestringThe first bucket that may still change, in the bucket format; every earlier bucket is final.
totalsobjectThe counters of the whole window.
seriesobject[]Every bucket of the window in time order, including those without traffic, which come at zero. Each item is a date (bucket format) plus the counters.
breakdownobjectThe whole window split three ways. Rows are sorted by messages, busiest first, and each row carries its label plus the counters.
byCountryobject[]country: ISO 3166-1 alpha-2 code of the destination (ES), or null when unknown.
byNetworkobject[]network: the destination mobile network as its MCC-MNC code in one string (21407), or null when unknown.
byTokenobject[]tokenId (the value to pass as tokenId; it can be null for traffic not tied to a token), name (the token's name) and deleted (true when the token no longer exists; its past traffic still counts).
Counters
The same eleven counters appear in totals, in every series item and in every breakdown row.
messagesintegerCharged messages.
partsintegerCharged message parts. A long message is split into several parts, each charged, and Unicode characters lower the length at which that happens.
deliveredintegerMessages whose current status is delivered.
failedintegerMessages whose current status is failed.
errorintegerMessages whose current status is error.
sentintegerMessages whose current status is sent: sent, with no final delivery report yet.
enqueuedintegerMessages whose current status is enqueued: not yet handed to the carrier.
expiredintegerMessages whose current status is expired.
rejectedintegerMessages whose current status is rejected.
noDRintegerMessages that have not received any delivery report. Overlaps the status counters.
costnumberAmount charged, in EUR, rounded to four decimals.
Errors and limits
400 Bad Requestwhenfromortois missing or is not a valid date in the expected format (an impossible date such as2026-02-30included), whenfromis afterto, when the window is wider than the granularity allows, or whengranularityortokenIdis not a valid value. Fix the parameters; retrying the same request does not help.404 Not Foundwhen your account has no A2P Messaging API project. An unknowntokenIdis not a404: it returns zeros.401,403and429behave as everywhere else in the API; see Errors.- Rate limit: 10 requests per 60 seconds, counted like the rest of the API's limits (see Rate limits). Since the figures only refresh periodically, calling more often returns nothing new: cache the response and check
dataUpTobefore fetching again.
What's next
- Receiving DLRs — the per-message status, pushed to your webhook as it changes.
- SMS senders — what is billed when a message is blocked.
- Rate limits — per-endpoint limits and the
X-RateLimit-*headers. - API Reference — the full schema of
GET /sms/metrics.
Last updated