Flowstates messaging platform logo
    All posts
    ArchitectureStrategyOperations

    Portable A2P Messaging Architectures: BYOV, Supplied Routes and Hybrid Models

    How to design A2P messaging so it isn't welded to one vendor — application abstraction, routing policy, sender identity, and BYOV/hybrid commercial models.

    Flowstates Team·Customer messaging operations4 August 2026 · 12 min read

    Most teams find out their A2P stack isn't portable at the worst possible time — a vendor degrades in one country, or a contract renewal goes badly, and moving traffic turns out to require an application release, a new registration cycle, and three weeks nobody budgeted for. Portability in A2P messaging isn't an architecture diagram. It's a set of decisions about where vendor-specific detail is allowed to live, and what it costs you when it leaks into places it shouldn't.

    What portability means here

    For customer messaging, "portable" doesn't mean transport-agnostic sockets or swappable brokers — that's an internal-systems problem, and it isn't the one A2P teams actually have. The A2P version of portability is narrower and more practical: can you change which vendor carries a given slice of traffic — a country, a channel, a traffic class — without touching application code, without a release, and without re-registering everything from scratch?

    Most stacks fail this test not because they lack redundancy, but because vendor-specific detail is scattered through the application: SDK calls in the codebase, vendor status strings in business logic, vendor error codes driving retry decisions. None of that is wrong on day one. It becomes expensive the day you need to move.

    Put one interface between your app and the vendors

    The starting point is unglamorous: your application should call one internal messaging interface — send a message, get a status callback — and never import a vendor SDK directly. Everything vendor-specific sits behind a provider adapter that the application never sees.

    A provider adapter is responsible for:

    • Translating your canonical send request into that vendor's API shape (auth, payload format, required fields, rate-limit handling).
    • Mapping the vendor's delivery statuses and error codes into your canonical vocabulary (below).
    • Handling that vendor's specific retry and backoff behaviour — some vendors want you to back off on 429s for minutes, others recover in seconds.
    • Reporting vendor-specific capacity and health signals upward, without leaking vendor-specific data types into the rest of the system.

    This is the cheap 80% of portability. It doesn't require multi-vendor routing or BYOV to be worth doing — even a single-vendor setup benefits, because it means the eventual second vendor is an adapter, not a rewrite.

    A canonical message and status model

    Every vendor has its own vocabulary for what happened to a message: delivered, undelivered, expired, rejected, blocked, unknown, and a long tail of numeric or string error codes that mean subtly different things per carrier and per vendor. If your application logic branches on a specific vendor's status strings, you've made that vendor a permanent dependency even after you've built an adapter layer for sending.

    Define your own small set of statuses — something like queued, submitted, delivered, failed_temporary, failed_permanent, unknown — and require every adapter to map into it. Keep the vendor's raw status and code alongside the canonical one for debugging and reconciliation, but make sure nothing outside the adapter ever has to know what "05" means on a given route. This is also what makes cross-vendor reporting possible: you can't compare delivery performance across two vendors if their status models don't roll up to the same categories.

    Routing policy as configuration, not code

    Once statuses are canonical, routing can be a policy document instead of a branch in your codebase: which vendor (or vendors, in priority order) carries which combination of country, channel and traffic class, what triggers a fallback, and what the fallback path is. Store it as data your routing layer reads at send time — a config service, a database table, whatever fits your stack — not as if statements shipped in a release.

    The test for whether you've actually achieved this: can an operations person change the primary vendor for one country's marketing SMS this afternoon, without a deploy? If the answer involves a pull request, the policy isn't actually externalised yet, whatever the architecture diagram says.

    Sender identity is the part that doesn't move

    This is where portability plans usually collide with reality. Sender identity — long code and short code numbers, alphanumeric sender IDs, 10DLC-style brand and campaign registration in the US, WhatsApp Business sender onboarding, RCS agent registration — is tied to the vendor, the market, and often a specific regulatory or carrier relationship. None of it travels with you when you switch providers. A new vendor for a market usually means a new registration cycle for that market, run in parallel with the old one until it clears.

    Treat this as a constraint to plan around, not a detail to discover mid-incident:

    • Track registration status per sender, per market, per vendor as its own piece of state — not folded into generic vendor config.
    • If failover to a second vendor is part of your resilience story, that vendor's sender identities need to already be registered and warm before you need them, not applied for during an incident.
    • Budget real time for registration cycles when planning a vendor change or a new market launch. The timelines vary by market and channel, and they are set by carriers and registries, not by you — build slack into the plan rather than a fixed number into the roadmap.

    Separate traffic classes before you separate vendors

    OTP and transactional traffic have a different failure tolerance than marketing traffic: a late OTP is a failed login, a late marketing message is mostly irrelevant. If they share a route, a marketing send spike degrades OTP delivery at exactly the moment volume is highest — which is usually also when it matters most.

    Separate traffic classes at the routing layer regardless of how many vendors you use. This does two things for portability: it lets you apply different failover rules to each class (OTP fallback should trigger fast and land on a route you already trust; marketing fallback can tolerate a slower, cheaper path), and it means a vendor problem affecting one class doesn't force you to reroute traffic that was working fine.

    Observability that survives a vendor swap

    If your monitoring is built around one vendor's dashboard, you lose visibility exactly when you need it most — during a migration or a failover. Instrument at the canonical layer instead:

    • Attach a correlation ID to every message at the point your application creates it, and thread it through the adapter, the vendor call, and the status callback, so you can reconstruct the full lifecycle of a message regardless of which vendor carried it.
    • Track delivery outcomes by canonical status, per vendor, per country, per traffic class — that's the view that lets you compare vendors honestly and spot degradation before customers do.
    • Reconcile end to end: what your application thinks it sent should match what the vendor confirms, which should match downstream signals like OTP verification or link clicks. Gaps between those numbers are usually where real incidents live, not in the delivery receipt alone.

    Retries, idempotency and circuit breaking

    Retry logic is where portability quietly breaks, because it is the place vendor-specific behaviour is most tempting to hard-code. Keep the decision in the routing layer and keep it canonical: a failed_temporary outcome is retryable, a failed_permanent one is not, and the adapter is responsible for deciding which bucket a vendor code lands in. Nothing above the adapter should be reading vendor codes to make that call.

    Three rules keep retries safe:

    • Idempotency at the edge. Give every send an application-generated key and treat submission as idempotent against it, so an ambiguous timeout does not become two OTPs. If a vendor supports an idempotency key, the adapter maps yours onto theirs; if it does not, the adapter has to deduplicate itself.
    • Bound the retry, per traffic class. OTP retries are only useful inside the window where the user is still waiting; after that a retry is a support ticket, not a recovery. Marketing retries can be slower and more patient. Encode the window in policy rather than in a shared default.
    • Break the circuit before you drown in retries. When a route's temporary failures cross a threshold, stop sending to it, mark it unhealthy, route new traffic to the fallback, and probe with a trickle before restoring it. Without a breaker, a degraded route absorbs your retries and turns one vendor's bad hour into a queue backlog across every market it touches.

    Failover, and why OTP failover needs its own rules

    A safe fallback path is one you've already tested with live traffic, not one you're depending on for the first time during an outage. Sending a trickle of real traffic through a secondary vendor on an ongoing basis — a "warm" backup — means its sender identities stay registered, its behaviour under your actual traffic pattern is known, and the failover switch is a routing change, not a cold start.

    OTP failover needs tighter rules than marketing failover: the fallback route has to already be registered and warm for that market, the trigger for switching has to be fast (seconds to low minutes, not hours), and it should not depend on a human noticing a dashboard. Marketing failover can tolerate a slower, manually-triggered switch to a cheaper or less-proven route, because the cost of a delay is much lower.

    Commercial models: supplied routes, BYOV, hybrid

    Portability is also a commercial question, and the three models change what you're responsible for.

    Flowstates-supplied routes. You send through routes Flowstates sources and operates. You get vendor selection, contracting, capacity management and failover handled for you; you give up direct vendor relationships for that traffic. This is the fastest path to a working multi-country setup and the one with the least operational load on your team.

    BYOV (bring your own vendor). You hold the vendor contracts; Flowstates operates the routing, monitoring, canonical status mapping and escalation layer on top of connections you own. You keep your existing commercial terms and any vendor relationships you've already built, and Flowstates becomes the operational layer rather than the commercial one. This suits teams with negotiated vendor pricing or compliance reasons to hold contracts directly.

    Hybrid. Some traffic runs on Flowstates-supplied routes, some on your own vendor contracts, under one routing and monitoring layer. This is common when a team has strong existing relationships in a few large markets but wants supplied routes to expand into markets where they have no vendor relationship at all, or wants a second, independently-sourced route as failover for a vendor they already bring themselves.

    What doesn't change across any of the three: someone still has to own routing policy decisions, monitor delivery health per vendor and market, and hold the vendor relationship that gets a human on the phone during an incident. BYOV moves the contract, not the operational work — decide upfront who's doing that work, because "the vendor will handle it" is rarely a complete answer under either model.

    Migrating without a release

    Moving live traffic to a new vendor or route without breaking anything follows roughly this sequence:

    1. Shadow first. Send the new route real traffic in parallel, without switching production decisions on it, and compare canonical delivery outcomes against your existing route before anyone downstream depends on the result.
    2. Cut over narrow. Move one country and one traffic class first — ideally not OTP first — rather than flipping every market at once. This limits the blast radius of anything the shadow phase didn't catch.
    3. Watch the reconciliation numbers, not just DLRs. A route can show healthy delivery receipts while conversion or verification rates on the other end quietly drop.
    4. Keep rollback cheap. If cutover is a routing-policy change rather than a code change, rollback is the same policy change in reverse — which is the whole point of having built it that way.
    5. Expand once the narrow cut is stable, market by market and traffic class by traffic class, rather than all at once.

    Lock-in tests worth running on your own stack

    A few concrete questions expose how portable a stack actually is, more reliably than an architecture review:

    • Could you move one country's marketing traffic to a second vendor this month, without an application release?
    • If your primary OTP vendor degraded right now, is there a registered, warm fallback route, or would you be registering a sender ID during the incident?
    • Does anything in your application code branch on a specific vendor's status string or error code?
    • Can someone in operations see, per vendor and per market, what "delivered" actually correlates to downstream — without asking an engineer to query logs?
    • If you wanted to add BYOV for one market while keeping supplied routes everywhere else, does your routing layer support that, or is it wired for a single vendor relationship?

    If more than one of those has an uncomfortable answer, the gap is worth closing before you need it, not after.

    Exit readiness

    Portability is only real if you could leave. That is partly architecture and partly record-keeping, and the record-keeping half is usually the weaker one:

    • Message and status history in your own store. If reconstructing last quarter's delivery performance requires a vendor's dashboard or a support request, you do not own your history. Keep canonical events on your side, with the raw vendor payload attached.
    • Registrations and sender identities documented as your assets — who submitted them, under whose brand, with what supporting evidence, and what would have to be re-submitted under a different vendor.
    • Consent, suppression and opt-out lists held centrally, not inside one vendor's platform. These are the records you cannot afford to rebuild, and a vendor-held suppression list is a compliance dependency as much as a technical one.
    • Templates and content approvals inventoried per market and channel, because re-approval is often the longest step in a channel migration.
    • A written exit path per market: which fallback vendor, what registration lead time, what the rollback looks like if the move goes badly.

    Reviewing that list once a year is cheap. Discovering it during a contract dispute is not.

    Where this fits together

    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.