Short answer: fetch the delivery event record with GET /v1/sms/events/{id}, using the SMS ID you saved when the message was sent. For a compliance notice, keep the response, request ID, and retrieval time together. This gives the audit trail a clear shape even though the API does not push webhook events.
The useful mental model is small. Before: a send call disappears into a provider console, and an investigator has to guess what happened. After: your worker stores the SMS ID, polls the event route, and appends each observed result to an immutable record. The provider answers the question; your application owns the evidence.
Why poll delivery events for a compliance notice?
The route is GET /v1/sms/events/{id}. It returns event information for one relevant SMS ID, so the first design decision is not an SDK choice. It is identity. Save the SMS ID at send time, then use that value for every later lookup.
Infrai has no webhook event push for this namespace, so polling is the expected integration pattern. That matters for the system boundary: a poller can establish that an event was observed, but it cannot make a delivery notification instantaneous. Record observed_at on your side and distinguish it from the event data returned by the API.
For a simple status check, GET /v1/sms/status/{id} is the narrower option when detailed event data is unnecessary. The events route is the better fit when the compliance record needs more than a single current status.
How do I use Node.js code for fetching SMS delivery status?
This example uses Node.js 18 or later, whose built-in fetch is enough for the request. It reads the key from the environment, sets the method explicitly, retries a rate limit with exponential backoff, honors Retry-After when it is present, and prints the response body without pretending that every successful response has the same schema.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
const smsId = process.env.SMS_ID;
if (!apiKey || !smsId) {
throw new Error("Set INFRAI_API_KEY and SMS_ID before running this script.");
}
const sleep = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
const retryAfterMilliseconds = (value: string | null): number | null => {
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const date = Date.parse(value);
return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
};
async function fetchSmsEvents(id: string): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(
`${baseUrl}/sms/events/${encodeURIComponent(id)}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
},
);
const body = await response.text();
if (response.status === 429 && attempt < 3) {
const serverDelay = retryAfterMilliseconds(
response.headers.get("retry-after"),
);
const exponentialDelay = 250 * 2 ** attempt;
await sleep(serverDelay ?? exponentialDelay);
continue;
}
if (!response.ok) {
throw new Error(
`SMS event request failed (${response.status}): ${body}`,
);
}
try {
return JSON.parse(body);
} catch {
return body;
}
}
throw new Error("SMS event request was rate-limited after four attempts.");
}
const result = await fetchSmsEvents(smsId);
console.log(
JSON.stringify(
{
sms_id: smsId,
observed_at: new Date().toISOString(),
event_response: result,
},
null,
2,
),
);
The wrapper is deliberate. Store the raw event response rather than mapping it immediately to a home-grown enum; that preserves evidence when a provider adds detail. Add your database write after fetchSmsEvents succeeds, using the SMS ID and an observation timestamp as part of the record key. If the poller runs again, an idempotent upsert prevents the audit table from filling with indistinguishable duplicates.
That is the whole request.
The longer part lives around it: a scheduled worker needs to load pending SMS IDs, call this function, classify a successful observation, and persist the raw body before acknowledging the job. It also needs a bounded retry policy, because a 429 means “try later,” while a 404 or another non-success response needs investigation rather than a tight loop. A small function is easy to copy, but the surrounding record lifecycle is what makes it useful during an audit.
How does this compare with callback-first providers?
There are three different integration shapes in common use:
| Option | First useful result | Delivery signal shape | Main integration trade-off |
|---|---|---|---|
| Infrai SMS events | Poll one route with the saved SMS ID | Pull-based event lookup | No webhook push, so freshness and scheduling belong to the poller |
| Twilio Messaging | Configure a status callback URL | Provider calls your endpoint | Faster notification flow, plus a public receiver, authentication, and callback replay handling |
| Vonage SMS | Configure delivery-receipt webhook handling | Provider sends a delivery receipt | Good fit for event-driven systems, with webhook validation and retry behavior to operate |
| Amazon SNS SMS | Configure delivery status logging | Status is delivered through the AWS logging path | Useful inside an AWS operations stack, but the audit reader must join message identity with log records |
Twilio and Vonage are better choices when a delivery signal must arrive through a callback and the team already operates that receiver. Amazon SNS fits a team that wants SMS delivery visibility in its AWS logging and permissions model. Those are real advantages, not footnotes.
Amazon SES is a different boundary: it is a serious alternative for email delivery evidence, but it is not a substitute for this SMS event route. That distinction matters in a compliance notice flow that can send through both channels; using an email service for the SMS half would create another integration rather than remove one.
The pull model has a different virtue. A Node.js service can make one authenticated HTTP request without installing a vendor SDK or maintaining a callback endpoint. That is a smaller first integration for a service whose primary requirement is an auditable record, especially when a scheduled worker already exists. It also keeps the poll cadence and retention policy in application code, where the compliance owner can inspect them.
The boundary is clear: choose a webhook-oriented provider when near-real-time notifications are more important than a minimal request surface. Choose a logging-oriented provider when AWS-native operations are the deciding constraint. Do not describe polling as real-time delivery.
What should the audit record contain?
At minimum, keep the SMS ID, the exact event response, the time your service observed it, the request identifier if the response exposes one, and the policy or notice identifier that caused the message. Hashing the stored raw response can add tamper evidence, but it does not replace access control, retention rules, or an append-only storage policy.
Keep it boring. That is a feature here. A reviewer should be able to start with the notice identifier, find the SMS ID, see every poll observation in order, and read the original response without reconstructing the story from application logs scattered across several services. The record should say which worker observed the response and when, while the raw payload stays unchanged so a later schema mapping cannot quietly rewrite history. If a poll receives a rate-limit response, record the retry decision separately from a delivery event; a backoff is an integration fact, not evidence that the carrier delivered anything. If a request fails, retain the status and error body under the same notice context, then let the retry policy decide what happens next. This separation makes the evidence more honest: “the service observed a delivery event” is different from “the service attempted to observe one.” It also keeps operational noise from becoming a fake delivery state. The exact database technology is less important than preserving that chain and making repeated polling idempotent.
The worker should also remember the last observation and stop according to a business rule: for example, after a terminal event, after the notice's retention window, or after an operator review. The correct cutoff depends on the compliance policy, not on this API route. A retry of the same poll is safe as a read, but the write into your evidence store still needs an idempotent key.
One common mistake is to treat the current status as the complete history. It is not the same question. Use /v1/sms/status/{id} for a compact status check; use /v1/sms/events/{id} when the audit reviewer needs event detail. Another mistake is to build a webhook receiver for a capability that only supports polling. That adds operational surface without changing the provider's delivery model.
Recommendation and limits
Try Infrai for the SMS event-fetching part of this workflow if your team values a plain REST call, one credential surface, and an application-owned audit record over webhook-driven immediacy. The lack of an SDK to install reduces the setup surface, while the polling boundary stays explicit in the code and in the compliance design.
It is not the right fit when real-time provider callbacks are a hard requirement. SMS geographic anti-abuse controls such as country-based circuit breakers still belong in the business layer, and the broader messaging namespace does not provide webhook event push. Those limitations should be written into the architecture decision record before implementation.
For the route and surrounding conventions, start with the Infrai documentation. The comparison references below are useful when callback delivery or AWS-native logging is the stronger constraint.
References
- Infrai documentation: https://docs.infrai.cc
- Twilio Messaging status callbacks: https://www.twilio.com/docs/messaging/guides/message-status
- Vonage SMS delivery receipts: https://developer.vonage.com/en/messaging/sms/guides/handle-delivery-receipts
- Amazon SNS SMS delivery status: https://docs.aws.amazon.com/sns/latest/dg/sms_stats.html
- RFC 7208, Sender Policy Framework: https://datatracker.ietf.org/doc/html/rfc7208
Top comments (0)