Recipes← all recipesView on GitHub

Send from a number group with sticky sender

Messagingsticky sender for SMS number pools

When you create a number group with sticky_sender: true, the platform picks From numbers out of that pool and holds one per recipient. A compat send names the group as MessagingServiceSid and carries no From.

sticky-sendernumbers

The claim

Three documented pieces. POST /api/relay/rest/number_groups creates the group. The vendored REST spec requires name and documents sticky_sender as a boolean, default false. Its description is “Whether the number group uses the same ‘From’ number for outbound requests to a number, or chooses a random one”. POST /api/relay/rest/number_groups/{id}/number_group_memberships adds a member by phone_number_id, its one required field. Then the compat create message takes the group in place of a number. https://signalwire.com/docs/compatibility-api/rest/messages/create-message describes MessagingServiceSid as “The ID of a number group to use when sending the message”. It adds “Either From or MessagingServiceSid must be provided”.

Why it holds

The SDK wraps the first two as client.number_groups.create and add_membership, and the send as client.compat.messages.create.

How it works

def create_pool(name, numbers):
    ids = [number_id(e164) for e164 in numbers]          # resolve first, so a bad number fails early
    group = client.number_groups.create(name=name, sticky_sender=True)
    for phone_number_id in ids:
        client.number_groups.add_membership(group["id"], phone_number_id=phone_number_id)
    return group["id"]

def send(group_id, to, body):
    return client.compat.messages.create(To=to, Body=body, MessagingServiceSid=group_id)

What the platform receives:

POST /api/relay/rest/number_groups
{"name": "repair-updates", "sticky_sender": true}

POST /api/relay/rest/number_groups/<group_id>/number_group_memberships
{"phone_number_id": "<id of +1555XXXXXXX>"}

POST /api/laml/2010-04-01/Accounts/<project>/Messages
{"To": "+1555YYYYYYY", "Body": "Your bike is ready.", "MessagingServiceSid": "<group_id>"}

Membership takes a phone number’s resource id, not its digits, so number_id looks it up with the spec’s filter_number query and compares the number exactly. The send has no From. Which member number the recipient sees is the platform’s choice, and sticky_sender is what makes that choice hold for them.

Limitations

You prove the requests. The pin itself, one From per recipient, is the platform’s behaviour and shows only in a live messaging log.

The vendored compat spec describes MessagingServiceSid only as a UUID. The sentence naming it as a number group id is from the live reference page linked above, fetched on 2026-09-02.

What to change first

Pass sticky_sender=False in create_pool and run the verifier. The exact-body assertion fails. The spec’s default says why nothing else would: false is what you get when you do not ask, and then the platform “chooses a random one”.