Flowstates messaging platform logo
    All posts
    SMSDevelopersAPI

    Programmable SMS APIs: Integration Details That Matter

    The integration decisions that decide whether an SMS API is maintainable: request contracts, idempotency, encoding, status mapping, webhook handling, retries and exit readiness.

    Flowstates Team·Customer messaging operations20 May 2026 · 10 min read

    Sending your first SMS through a provider API takes an afternoon. The work that follows — deciding what a "sent" message means, what happens when a request times out, how a message gets attributed to a business outcome, and what it costs to change provider — is where integrations either stay maintainable or turn into a permanent tax on the roadmap.

    This is a practitioner's list of the decisions that matter, in roughly the order they bite.

    Keep a stable internal contract, not vendor SDK calls

    The most consequential early decision is whether your application code calls a provider SDK directly. If it does, provider-specific detail leaks everywhere: status strings in business logic, error codes deciding retry behaviour, request shapes baked into call sites.

    Define one internal contract instead. A send request carries a destination, a message body or template reference, a traffic class, a sender identity selector and your own client reference. A status callback carries that client reference, a canonical status, a canonical reason and the provider's raw payload for debugging only. Everything provider-specific lives behind an adapter.

    This is worth doing with a single provider. It converts the second provider from a refactor into a new adapter.

    Authentication and secret rotation

    Most SMS APIs use either a static key or basic credentials, sometimes with IP allow-listing or mTLS on SMPP binds. Whatever the mechanism, plan rotation before you need it:

    • Store credentials in a secret manager, never in application config or repository files.
    • Support two active credentials at once so rotation is not an outage. If the provider only issues one, rotation becomes a maintenance window — find that out now, not during an incident.
    • Keep separate credentials per environment, and separate credentials per traffic class if the provider supports it, so a leak has a bounded blast radius.
    • Log the credential identifier used, never the credential.

    Client references, idempotency and ambiguous timeouts

    Generate your own reference for every message before you call the provider, and store it with the message record. You need it because provider message IDs only exist after a successful response, and the interesting failures are the ones where you never get a response.

    The ambiguous case is the one to design for: your HTTP request times out. The message may have been accepted, may have been sent, may have been delivered. Retrying blindly means duplicate messages to a real person; not retrying means silent loss.

    Options, in order of preference:

    1. Provider-supported idempotency keys. Send your client reference as the idempotency key so a retry is a no-op. Confirm the key's retention window — an idempotency guarantee that expires in minutes is not much use to a queue that backs up for an hour.
    2. Lookup by client reference. Before retrying, query the provider for a message carrying your reference. This needs a provider API that supports the lookup and a reference field that survives round-trip.
    3. Bounded, class-aware policy. Where neither exists, decide per traffic class. A duplicated OTP is an annoyance and a support call. A duplicated payment reminder or a duplicated marketing message is worse. Record the decision explicitly rather than letting a generic HTTP client library decide it.

    Sender identity, registration and traffic class

    Sender identity is not a string you choose at send time. Depending on the destination market it may be a registered alphanumeric sender ID, a long number, a short code or a number pool, and it may require registration with a regulator, a carrier or the provider — with lead times measured in weeks, and rules that differ by market and by whether traffic is transactional or promotional.

    Two consequences for the integration:

    • The sender identity used for a given destination and traffic class is configuration, resolved by the platform, not a value passed from application code.
    • Traffic class must be an explicit field on every send, because it determines sender selection, route selection, throttling and whether the message is permitted at all in some markets. If your API makes traffic class optional, it will be wrong.

    Encoding, segmentation and billing implications

    SMS bodies are encoded either in the GSM-7 alphabet (160 characters per single segment) or UCS-2/Unicode (70 characters per single segment). Concatenated messages use a user-data header, so segments are 153 and 67 characters respectively.

    The operational trap is that a single character can flip the whole message to Unicode. A curly apostrophe from a CMS, an emoji, an accented name in a merge field, a non-breaking space pasted from a document — any of these can turn a 160-character GSM-7 message into a multi-segment Unicode message. Since billing is per segment, this changes cost and can change delivery behaviour.

    What to build:

    • Encoding and segment-count calculation in your own code, before submission, using the same rules the provider applies.
    • Segment count stored on the message record so reporting and cost attribution can use it.
    • Validation at template save time, not send time, with an explicit warning when a template is near a segment boundary.
    • Optional normalisation of characters that have GSM-7 equivalents (curly to straight quotes, for example), applied deliberately and visibly rather than silently.

    Note that GSM-7 also has an extension table — characters such as {, }, [, ], \, ~, ^, | and count as two characters each.

    Canonical status and error mapping

    Every provider has its own status vocabulary and its own error taxonomy. If you store those raw, every report and every alert becomes provider-specific.

    Map to a small canonical set you control — for example: accepted, submitted, delivered, failed, expired, rejected, unknown. Map reasons to canonical categories: invalid destination, unreachable handset, blocked or filtered, sender not permitted, insufficient balance, provider error, timeout, unknown. Keep the raw provider status and code alongside for debugging, and treat unmapped values as an explicit "unmapped" state that raises a low-priority alert rather than silently collapsing into "failed". New provider status codes appear without notice, and silent collapse hides real regressions.

    Webhooks: signatures, replay, ordering, duplicates

    Status callbacks are the part of the integration most often built once and then trusted more than it deserves.

    • Verify signatures. If the provider signs callbacks, verify with a constant-time comparison over the exact raw body. Parsing before verifying, or re-serialising, breaks the signature and tempts people to disable the check.
    • If there is no signature, treat the endpoint as untrusted input: a secret path segment plus IP allow-listing is weak but better than nothing, and never act on the payload's own claim about which message it refers to without checking it against your own records.
    • Replay protection. Reject callbacks with timestamps outside a tolerance window, and keep a short-lived record of seen event identifiers.
    • Assume duplicates. Providers retry callbacks, and a duplicate is not an error. Make handling idempotent on (message reference, status, provider event id).
    • Assume out-of-order arrival. A delivered event can arrive before the submitted event. Guard transitions with a status precedence rule so a late "submitted" cannot overwrite a recorded "delivered".
    • Respond fast, process asynchronously. Acknowledge with a 2xx, enqueue, and do the work outside the request. Slow handlers cause provider-side retry storms.
    • Reconcile. Callbacks get lost. Run a periodic job that queries final status for messages stuck in a non-final state past a threshold, and treat the reconciliation delta as a monitored metric — a rising delta usually means a broken endpoint or a provider callback problem, well before anyone reports it.

    Retries and timeouts: decide who owns them

    Retry decisions exist in at least three places — your application, your messaging platform and the provider's own network-level retry — and if nobody owns the policy, all three retry at once.

    Make it explicit:

    • Set a submission timeout you are willing to defend, and treat timeout as ambiguous rather than failed (see above).
    • Retry only canonical error categories that are plausibly transient. Do not retry invalid destination or sender-not-permitted.
    • Use bounded attempts with backoff and jitter, and a hard deadline after which the message is dropped rather than delivered late. A late appointment reminder or a late OTP is worse than none.
    • Know whether the provider retries at the carrier layer and for how long, because that interacts with your own deadline.

    Throughput, queueing and burst behaviour

    Provider throughput is rate-limited per account, per connection or per sender, and limits differ by market. A campaign or a batch job will exceed them.

    Queue on your side with an explicit per-route rate limiter, separate queues by traffic class so a marketing batch cannot delay verification traffic, and decide what happens when the queue depth exceeds what can be delivered inside the message's useful lifetime — shedding low-priority traffic is usually better than delivering it hours late. Handle 429 and provider throttle responses as backpressure that slows the queue, not as message failures.

    Correlation IDs, logs and measuring outcomes

    Attach a correlation ID that spans the triggering business event, the send request, the status callbacks and any click on a link in the message. Without it, questions like "did the users who did not complete signup receive their message?" are unanswerable.

    Delivery receipts are a weak proxy for outcome. DLR semantics vary by carrier and route, some routes return optimistic or synthesised receipts, and "delivered" says nothing about whether the message was read or acted on. Measure at the application layer instead: completion of the action the message asked for, time from send to that action, click-through on tracked links where you use them, and reply, complaint and opt-out rates. Compare those application-layer outcomes across routes and providers — that comparison is far more useful than comparing delivery percentages.

    Sandbox limits, production test traffic and failure injection

    Provider sandboxes typically cannot reproduce the things that break in production: carrier filtering, sender registration rejection, handset-level blocking, encoding surprises on real devices, or callback delays. Plan for that:

    • Keep a small set of real destination numbers across your live markets and send scheduled canary traffic through each route, alerting on canonical status and on end-to-end latency.
    • Build a fault-injection mode in your adapters — forced timeout, forced 429, forced provider error, delayed callback, duplicate callback, out-of-order callback — and run it in CI. Most webhook bugs are found this way rather than in staging.
    • Test encoding with real names and real template data, including accented characters and emoji.

    Provider abstraction, migration and exit readiness

    Exit readiness is testable. Ask:

    • Can you move a country, channel or traffic class to another provider through configuration, without an application release?
    • Is there any provider status string, error code or SDK type in application code?
    • Do you hold your own copy of message history, consent and suppression records, in a format you can export?
    • Are sender registrations held in a way that allows another provider to be onboarded in parallel, rather than requiring a cutover?
    • Can you run two providers simultaneously for the same traffic and compare application-layer outcomes?

    Flowstates supplies messaging routes and can operate the layer described here; customers can also keep their own vendor contracts through BYOV, or run a hybrid — supplied routes in some markets, their own in others — with one contract and one operational owner regardless of which model applies.

    The security limits of SMS, and what never belongs in a message

    Ordinary SMS is not end-to-end encrypted. Transport between your application and the provider can be secured with TLS, and links between operators are typically protected, but the message content is available to intermediaries in the delivery chain and is stored in plain text on the handset, often visible on a lock screen and synced to other devices. Treat SMS as a low-assurance channel to a phone number, not a confidential channel to a person.

    Practical consequences:

    • Never send passwords, full account numbers, card data, health details or authentication credentials that grant standing access.
    • Keep one-time codes short-lived and single-use, bind them to the session that requested them, rate-limit both issuance and verification, and never include the account identifier alongside the code.
    • Remember that a phone number can be reassigned or ported, and that SIM-swap and social-engineering attacks target SMS specifically. For high-value actions, SMS verification is a signal, not proof.
    • Do not put personal data in link query strings; use opaque, expiring tokens.
    • Redact message bodies and destination numbers in logs by default, with a deliberate, audited path to see them during an investigation.

    Integration checklist

    • One internal send/status contract; no provider SDK calls in application code.
    • Secrets in a secret manager; rotation possible without downtime.
    • Application-generated client reference on every message, stored before submission.
    • Explicit policy for ambiguous timeouts, per traffic class.
    • Traffic class mandatory on every send; sender identity resolved from configuration.
    • Encoding and segment count computed and stored before submission; templates validated at save time.
    • Canonical status and reason mapping, with raw provider values retained and unmapped values alerted.
    • Signature verification, replay protection, duplicate and out-of-order tolerance on callbacks.
    • Asynchronous callback processing plus a reconciliation job with a monitored delta.
    • Retry ownership decided in one place; bounded attempts; hard delivery deadlines.
    • Per-route rate limiting with traffic-class queue separation and backpressure handling.
    • Correlation IDs spanning business event, send, callbacks and clicks; outcomes measured at the application layer.
    • Canary traffic on live routes; fault injection in CI.
    • Exit readiness verified: route change by configuration, exportable history, parallel provider capability.
    • No credentials, personal data or standing secrets in message content or logs.

    Want to talk through your messaging stack?

    Book a 30-minute review with our team. No pitch deck - we'll look at what you have and tell you where the operational risk is.