Skip to main content

Messaging services

A messaging service is a named configuration bundle you reference from a send. Instead of repeating a sender pool, a status callback, a validity period, and an inbound webhook on every request, you store them once on a service and pass messaging_service_id on the send. The service is also the unit that carries per-country sender routing, the per-second throughput cap, and the direct DID attachments that decide where inbound replies are delivered. This page defines the entity: what it bundles, how it differs from a sender pool, exactly when its defaults fill in on a send, and how it is created, changed, and retired. The full sender-precedence chain the service plugs into lives on Sender resolution; the compact two-sided summary (outbound picker plus inbound MO rules) is How routing picks a sender. Request and response schemas are owned by the Messaging credentials & services reference; this page covers the model.

Where this fits

  • This page (the entity) — what a messaging service is, what its fields mean, when its defaults apply, and its lifecycle. Read this when deciding whether to create a service, or when a field behaves unexpectedly.
  • Sender resolution — the authoritative precedence chain across all sender sources (explicit from, pools, services, sticky senders, fallbacks). Read that page when the question is “which sender will this one send use.”
  • The API reference — every field, enum value, and response shape for the /api/v1/messaging/services endpoints. This page defers to it for wire detail rather than repeating it.

1. What a messaging service is

Create one with POST /api/v1/messaging/services. Only label is required; everything else is a default the send pipeline can inherit: Two properties of the entity worth internalising:
  • Defaults, not overrides. Everything a send inherits from a service fills in only where the request left a blank. A messaging service never wins over a value you set on the send itself.
  • One id, one lookup. The send pipeline resolves the service once per message; the service id is stamped onto the message so delivery callbacks and inbound replies trace back to it.

2. A service references pools; it does not replace them

A sender pool owns one job: pick exactly one of its member senders per message, by strategy (sticky, round_robin, random, geomatch), with health-based member swaps and fail-safes. A messaging service has no member-selection logic of its own — it points at a pool (sender_pool_id) or at several (the country_sender_pools map), and the pool it lands on does the picking. That separation has two consequences:
  • The pool’s strategy is still the pool’s. If the service’s pool is sticky, sends through the service keep each recipient on a stable sender. Changing rotation behaviour means editing the pool, not the service. (One exception: area_code_geomatch on the service overrides the pool’s stored strategy with geo-matching — the one flag the service does push into pool resolution.)
  • A service without a pool is still valid. With no sender_pool_id and no per-country entry for the recipient, the service simply supplies no sender source, and the send continues down the rest of the precedence chain — sticky conversation sender, org default, platform fallback. A service is a bundle of any subset of its defaults, not an all-or-nothing wrapper.

3. How messaging_service_id resolves on a send

Passing messaging_service_id on POST /api/v1/messages/sms makes the send pipeline read the service and fill in the blanks the request left. The rule is strict about where the blanks are:
  • You passed from or from_extension → the service’s sender config (default pool and per-country map) is ignored for the from decision. You pinned a sender; the service cannot displace it.
  • You passed sender_pool_id → your pool wins; the service’s pool and per-country map are not consulted. (And a bare from sent together with sender_pool_id loses to the pool — see Sender resolution for that precedence rule.)
  • You passed neither → the service’s sender source fills in: first the recipient-country’s override pool from country_sender_pools, then the service’s default sender_pool_id.
The same fill-in-only-on-blank rule applies to the message-level fields: status_callback and validity_period on the service copy onto the send only when the request itself carries neither. Whatever the merge produces, the service id is stamped on the message so status callbacks and inbound replies can be attributed back to it. Two boundary rules:
  • The service must exist. Referencing an unknown id fails the send with 404 MESSAGING_SERVICE_NOT_FOUND — there is no silent skip-a-service path.
  • Flags are unconditional. sticky_sender, area_code_geomatch, mms_converter, and smart_encoding are not “fill in on blank”: they are on or off on the service and apply to every send routed through it.

4. The per-country country_sender_pools map

A service can carry a map from ISO 3166-1 alpha-2 country codes (uppercase, e.g. US, GB, DE) to sender-pool ids, up to 250 entries — room for the UN’s 195 countries plus non-recognised territories. Values must be pool ids in the same format as sender_pool_id. At send time the recipient’s country is resolved — taken from an explicit recipient_country on the request, otherwise derived from to — and the chain is:
  1. country_sender_pools[<recipient country>] — the per-country override pool, if an entry exists
  2. sender_pool_id — the service’s default pool
  3. Nothing — the service supplies no pool and the precedence chain continues
Two rules bound the map:
  • The map never overrides you. The override and the default pool both fill in only when the request carries no sender_pool_id of its own.
  • Keep entries corridor-correct. Point each country at the pool you maintain for that corridor — numeric-only pools for US/Canada entries, since +1 carriers reject alphanumeric MT — for the same reasons the pools themselves are kept corridor-correct.
The end-to-end walkthrough — a GB recipient hitting a country_sender_pools: { "GB": "pool_gb" } entry and a US recipient falling through to the default pool — is the worked example on Sender resolution; it is the canonical illustration of this lookup and is not repeated here. Update the map with PATCH /api/v1/messaging/services/:id: send the full replacement map, send {} to clear it, or omit the field to leave it untouched.

5. Inbound binding: where replies to the service’s numbers go

The inbound fields set the destination for replies, not for sends. Attach tenant-owned numbers to a service (POST /api/v1/messaging/services/:id/phone-numbers with a phone_number_id — the same number may be attached to several services, and attaching a number that reports no SMS capability is refused with 422 NOT_SMS_CAPABLE). When an inbound SMS arrives on any attached number, Orbit delivers it to the service’s inbound_webhook_url using inbound_method (POST by default, or GET), and calls fallback_url once if the primary fails. Delivery of that webhook is fail-soft by design: a slow, down, or misconfigured endpoint never blocks the inbound message itself from persisting, and the service-level webhook sits underneath the tenant-level inbound rules — the rules that match on number, sender, keyword, or regex and route to a webhook, queue, inbox, team, or SMS menu. Routing precedence between the two layers belongs to Inbound message routing; this page only notes that the service binding is the per-numbers web address you manage under the service’s lifecycle.

6. Lifecycle: create, update, delete, attach numbers

All endpoints live under /api/v1/messaging/services; the full schemas are in the Messaging credentials & services reference.
  • CreatePOST with a label and any subset of defaults. Creating is additive: a service with no pool and no callback is a legal starting point, and you backfill fields later.
  • UpdatePATCH /:id is partial: omitted fields are untouched, an empty body is rejected, and nullable fields (sender_pool_id, status_callback, validity_period_seconds, mps_cap, external_id, inbound_webhook_url, fallback_url) accept null to clear. country_sender_pools clears via {}. Updates propagate to the send path within a few seconds (the resolved config is cached briefly).
  • DeleteDELETE /:id removes the service and its number attachments; deleting an already-deleted service returns 404. Sends still referencing the id start failing with 404 MESSAGING_SERVICE_NOT_FOUND from the next request, so re-map or delete the usages first.
  • Number attachmentsPOST / DELETE on /:id/phone-numbers; re-attaching the same number is idempotent.
  • Import correlationexternal_id maps a provider service id (a Twilio MG… SID or another vendor’s opaque id) onto ours; GET /:id-shaped GET /lookup?external_id=… resolves one, and the migration import upserts by it, so a re-import updates rather than duplicates. external_id is unique per tenant — a duplicate returns 409.
Composition with per-request fields, in one sentence: the service is the fallback layer — sender_pool_id and bare from on the request always beat it, and the service’s pool/callback/validity defaults fill in only where the request is silent. The full precedence table, including what happens when you pass both from and sender_pool_id, is section 1–3 of Sender resolution.

7. The per-service throughput cap

mps_cap is the operator-set, per-messaging-service ceiling on send rate: an integer between 1 and 1000 messages per second, or no cap when unset. When a service carries a cap, every send through that service increments a per-second counter; crossing the cap rejects the send with 429 MESSAGING_SERVICE_MPS_EXCEEDED and Retry-After: 1, so one bursty integration cannot starve the tenant’s other services. Test-mode sends bypass the cap so you can probe the pipeline from a staging tenant without tripping it. Because the cap is per service, it is the service — not the tenant — that owns this knob: GET and PATCH /api/v1/messaging/services/:id/throughput read and write just the cap (sending null clears it), and GET /:id/throughput-stats plus the account-wide GET /throughput-stats report live send rate, over-cap queue depth, and recent throttle events per service with per-service labels. Where this cap sits relative to every other limit in the platform — tenant ceilings, provider limits, cooldowns — is the rate-limit and cooldown taxonomy.

See also