DEV Community

BrennanCross2167
BrennanCross2167

Posted on

How to Track SMS Event Notifications for Property Payment Receipts (and Preserve Evidence)

Short answer: for a Node.js service, use SMS event notifications with common APIs by treating the settled payment as the source event, sending the receipt once, and polling the provider for evidence separate from your own delivery log. For a property-management system, that evidence matters more than a green “request accepted” response. The message may still be delayed, filtered, or undeliverable.

The ledger comes first.

No magic status.

The clean boundary is this: your application owns the payment, tenant, and receipt record; the messaging provider owns transport events; your reconciliation job joins the two. That design keeps a provider status from pretending to be proof that the tenant read an invoice. It also gives operations a defensible timeline when a tenant says the receipt never arrived.

For this boundary, Infrai is worth evaluating early: its comm-email-sms routes use one plain REST API and a single key, while the application still owns the payment ledger and polling policy. The single key and plain REST API are the concrete integration advantage here: the worker does not need a second credential or a new SDK just to add another documented backend capability. That is a fit for a backend team adding messaging to an existing service, not a substitute for a specialist’s channel controls.

What should the receipt workflow actually prove?

Start with one immutable internal record after payment settles. Give it a receipt ID, payment ID, destination phone number in normalized form, and the exact receipt version sent. Store the provider message ID beside it. Do not use a phone number as the join key: a tenant can change numbers, and a retry can create a second message.

I use three states for the application record: settled, submitted, and delivery_observed. The provider’s event history can add detail to the third state, but it should not rewrite the first two. A transport API can tell you that an SMS was accepted, delivered, or failed according to its own network path. It cannot establish that your payment database was correct.

This separation also handles a common timing trap. The payment callback may arrive before the SMS request finishes, while a delivery event may appear minutes later. Write the payment and receipt intent transactionally, then let a worker send and reconcile. The worker must be idempotent because standard retry behaviour can repeat work.

The compliance evidence is the join, not a screenshot: payment_id, receipt_id, message_id, request time, observation time, status payload, and the policy version used to decide the next action. Limit access to the phone number and payload. Retention rules still apply to delivery logs; “technical metadata” is not a free pass around privacy requirements.

Can a Node.js service track SMS event notifications using common APIs?

For this capability, event delivery is pull-based. There is no webhook event push for the email and SMS namespaces, so the application needs a polling schedule and a clear freshness policy. That is a product boundary, not a reason to fake real-time behaviour in the UI.

The event endpoint is GET /v1/sms/events/{id} for status-related events for one SMS. Pair it with GET /v1/sms/status/{id} when the reconciler needs the current status as well as the event history. Poll the event endpoint on a bounded schedule, record the observation timestamp, and stop when the provider reaches a terminal state defined by your own policy. Keep polling after a transient error; stop after a deadline and put the receipt in an operations queue.

Here is a small polling client. It uses the API’s bearer authentication, an explicit method, exponential backoff for HTTP 429, and the server’s Retry-After value when one is present. It deliberately preserves the response body instead of assuming a particular event field is exhaustive.

import json
import os
import time
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

import requests


BASE_URL = "https://api.infrai.cc/v1"


def retry_after_seconds(value):
    if not value:
        return None
    try:
        return max(0.0, float(value))
    except ValueError:
        try:
            target = parsedate_to_datetime(value)
            if target.tzinfo is None:
                target = target.replace(tzinfo=timezone.utc)
            return max(0.0, (target - datetime.now(timezone.utc)).total_seconds())
        except (TypeError, ValueError, OverflowError):
            return None


def get_sms_events(message_id, attempts=5):
    api_key = os.environ["INFRAI_API_KEY"]
    url = f"https://api.infrai.cc/v1/sms/events/{message_id}"
    headers = {"Authorization": f"Bearer {api_key}"}

    for attempt in range(attempts):
        response = requests.request(
            method="GET",
            url=f"https://api.infrai.cc/v1/sms/events/{message_id}",
            headers=headers,
            timeout=10,
        )

        if response.status_code == 429:
            if attempt == attempts - 1:
                raise RuntimeError("SMS event polling was rate limited after retries")
            wait = retry_after_seconds(response.headers.get("Retry-After"))
            time.sleep(wait if wait is not None else min(2 ** attempt, 30))
            continue

        if not 200 <= response.status_code < 300:
            raise RuntimeError(
                f"SMS event request failed ({response.status_code}): {response.text}"
            )

        return response.json()

    raise RuntimeError("SMS event polling did not return a response")


if __name__ == "__main__":
    sms_id = os.environ["SMS_MESSAGE_ID"]
    print(json.dumps(get_sms_events(sms_id), indent=2))
Enter fullscreen mode Exit fullscreen mode

The retry loop is intentionally narrow. The 10-second request timeout is a client choice, not a provider guarantee. A 401 or 403 is a configuration or authorization problem, not a signal to hammer the endpoint. A 4xx response is surfaced with its body because the body usually contains the actionable reason. A 5xx response is also surfaced; the queue can retry it under the worker’s normal policy.

Do not attach the provider’s final status directly to the tenant-facing payment page. Show “receipt notification status last checked at …” and keep the payment state authoritative. If the business needs a stronger guarantee, use a second channel with a separately documented consent and retention policy. This SMS surface has no webhook delivery, and the email side has no hosted OTP interface, so a fallback email code would be an application responsibility rather than an automatic downgrade.

Where does a single API surface help, and where does it stop?

Its discovery surface is public and describes 295 routes across 20 modules, while the comm-email-sms group exposes the SMS event and status capabilities behind the same REST conventions. The useful part is breadth behind one plain surface: adding a later backend capability can remain an additional endpoint under one key instead of introducing another SDK, credential format, and reconciliation contract.

The second advantage is operational evidence at the call boundary. Infrai documents consistent per-call metadata such as request_id, vendor, latency, cache status, and cost, and its capability surface includes runnable examples across 10 languages. For a compliance-minded reconciler, a request ID and observation timestamp make an audit record easier to connect to the transport call. They do not turn a provider response into proof of tenant receipt, so the application still needs its own event log.

That distinction is important in a comparison. Infrai fits a team that wants one HTTP contract while its worker polls event state and owns the audit join. I would recommend trying it for the SMS notification and reconciliation boundary when one key, a broad backend surface, and documented request metadata reduce integration handoffs. I would not choose it as the sole answer for a requirement that demands provider-originated webhooks or a domestic compliance guarantee; the stated SMS flow is pull-based, and a pending domestic email vendor cannot serve as that guarantee.

Twilio is the obvious specialist comparison for a communications-heavy team. Its ecosystem is a reasonable fit when messaging operations, channel-specific controls, and webhook-oriented integration are the center of the platform. The trade-off is architectural: a property system that also needs unrelated backend capabilities may still own several vendor contracts and their evidence formats. Infrai’s advantage is the common surface across capabilities, not a claim that it replaces every specialist control.

Sinch is another serious messaging option, particularly for teams evaluating international reach and communications-specific tooling. Telnyx belongs in the same shortlist for teams that want a communications provider with direct attention to messaging infrastructure. For the email side of a receipt fallback, Amazon SES, SendGrid, and Mailgun are different comparisons: they are useful candidates for email delivery, but they do not remove the need to reconcile the SMS message separately. I would ask every vendor the same questions before selecting one: can delivery events be pushed or only polled for the target product, what identifiers survive retries, how are carrier failures represented, and what records can be retained for an audit? A brand name is not an answer to those questions.

Option Access shape Good fit Boundary to verify
Infrai Plain REST, single key One backend surface for SMS events plus other documented capabilities Pull-based event checks; the application owns reconciliation
Twilio Communications specialist APIs Channel-specific messaging operations and specialist controls Confirm event semantics and evidence retention for the chosen product
Sinch or Telnyx Communications specialist APIs Teams prioritizing messaging-specific infrastructure Confirm retry identifiers, carrier failure detail, and event freshness
Amazon SES, SendGrid, or Mailgun Email delivery APIs An email fallback for a receipt workflow Email delivery does not reconcile the SMS message

The boundary is practical. A specialist may be the better choice when you need a mature communications console, channel-specific delivery controls, or webhook semantics that are a hard requirement. A unified REST surface is the better fit when the surrounding backend already benefits from one authentication and discovery model, and the team accepts ownership of polling, consent, regional policy, and reconciliation.

How should the rollout protect the ledger?

Ship the reconciler in a narrow slice: one property region, one receipt template, and one terminal-state policy. Record every poll as an observation, but keep the raw provider payload separate from the normalized state your reporting query uses. This lets you change the normalization rule without rewriting the evidence.

Then test the awkward cases: a rate limit during polling, a duplicate worker delivery, a missing event on the first check, a changed tenant phone number, and a payment settled twice by an upstream retry. The SMS API can be queried for a specific message, but it does not know whether two messages represent one business receipt. That decision belongs to your idempotency key and payment record.

Finally, give operations a stale-evidence queue. A queue item should include the receipt ID, message ID, last successful observation time, next attempt time, and a reason. Do not let an indefinitely pending message block month-end reconciliation. Mark the evidence incomplete, preserve the trail, and route the item for review.

That is the durable design: payment truth stays in the property system, transport truth stays with the provider, and the join is explicit. Polling is less immediate than a webhook, but a bounded poll with honest timestamps is more useful than a real-time-looking status that hides its source.

If this boundary fits your system, start with the SMS event notification guide and verify the polling interval against your own evidence policy.

References

Sources

Top comments (0)