Group Membership


Add or remove a contact from a group. Membership is the bridge between a contact and your messaging — a contact must have an active membership in at least one group before any messaging endpoint will accept them as a recipient.

Endpoints

MethodPathPurpose
POST/api/v1/contacts/by-phone/{phoneNumber}/groups/{groupId}Add a contact to a group
DELETE/api/v1/contacts/by-phone/{phoneNumber}/groups/{groupId}Remove from a group

The URL encodes the relationship: phoneNumber identifies the contact (E.164), groupId identifies the group (grp_* prefix from a Groups response).

POST — Add a contact to a group

Adds the contact to the group. The optInMode in the body chooses the enrollment flow:

curl -X POST \
  https://platform.textingline.com/api/v1/contacts/by-phone/+18005551234/groups/grp_DsAdb4EBYBh3z25sBA1q \
  -u "your_key_id:your_key_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "optInMode": "CONFIRM"
  }'

Request body

FieldTypeRequiredDescription
optInModestringNoOne of DIRECT (default) or CONFIRM. See Opt-in modes below.

The body may be omitted entirely; the default optInMode: "DIRECT" is used.

Opt-in modes

ModeWhat happens
DIRECTCaller asserts prior consent. The contact is added as an active member immediately and appears under groups[] on subsequent reads. Use this only when you have a documented consent artifact (web form, kiosk, etc.).
CONFIRMThe platform sends an opt-in SMS to the contact. They land under pendingGroups[] until they reply YES, at which point they move to groups[]. Use this when you don't have prior consent on file. See Confirm-mode opt-in flow.

Response

Returns 200 OK with the full contact projection after the change. Inspect groups[] and pendingGroups[] to verify the resulting state.

Idempotency

If the contact is already an active member of the group, the call is a no-op and returns the current contact state (still 200). The same applies if a CONFIRM-mode enrollment is in flight — re-issuing the POST does not send a second opt-in SMS.

Consent gate (cannot be overridden)

Two states block enrollment regardless of optInMode:

StateCodeWhat it means
HARD opt-out33035The contact has explicitly refused messages (texted STOP, opted out at a kiosk, or was flagged via carrier propagation). The operator cannot re-enroll a HARD-opted-out contact — only the contact themselves can re-engage by replying YES to a new opt-in or opting back in via a hosted form.
Pending in other groups33036The contact has pending memberships in groups other than the target group. Resolve those first (wait for the contact to reply YES or DELETE the pending membership) before adding them to a new group.

Both return 409 Conflict with the error code in the body. See Error Codes for the full details.

DELETE — Remove a contact from a group

Removes the contact from the group. Always a soft removal — records MANUAL_REMOVAL / USER_REMOVED on the membership log; the contact and the group both stay intact.

curl -X DELETE \
  https://platform.textingline.com/api/v1/contacts/by-phone/+18005551234/groups/grp_DsAdb4EBYBh3z25sBA1q \
  -u "your_key_id:your_key_secret"

Cannot record a HARD opt-out

DELETE removes the membership but cannot mark the contact as HARD-opted-out. Only the contact themselves can do that (by texting STOP, replying STOP to a message, etc.). If you need to flag a contact as opted out, instruct them to text STOP to your sender number — see Compliance.

Idempotency

If the contact is not a member of the group (whether they never were, or they were already removed), the call is a no-op and returns the current contact state (still 200).

Response

Returns 200 OK with the full contact projection after the change.

Understanding groups[] vs pendingGroups[]

The contact response surfaces both arrays so you can see the full enrollment state at a glance:

{
  "id": "con_BoZHt0bxx5Qg2qN0",
  "phoneNumber": "+18005551234",
  "groups": [
    {
      "id": "grp_DsAdb4EBYBh3z25sBA1q",
      "name": "VIP Customers",
      "optInAt": "2026-04-01T00:00:00.000Z",
      "tags": [...]
    }
  ],
  "pendingGroups": [
    {
      "id": "grp_PqRsT5uVwXyZaB6c7DeF",
      "name": "Spring Promo",
      "pendingSince": "2026-04-15T14:30:00.000Z"
    }
  ]
}
  • groups[] — fully opted-in memberships. The contact can be sent messages addressed to these groups.
  • pendingGroups[]CONFIRM-mode enrollments awaiting reply. The platform has sent the opt-in SMS; until the contact replies YES, they don't receive messages targeted to the group.

A pending membership transitions to active when the contact replies YES. The transition is silent from the API's perspective — there is currently no webhook that fires for membership state changes. To detect the transition, poll GET /contacts/by-phone/{phoneNumber} and watch for the group ID to move from pendingGroups[] to groups[].

Rate limit

EndpointLimitWindow
POST / DELETE /api/v1/contacts/by-phone/{phoneNumber}/groups/{groupId} (write)150 requests60 seconds

These writes share a single contactApiWrite bucket with Contacts POST and PATCH. A burst of contact updates and membership changes counts against one pool. See Rate Limits.

Common errors

CodeHTTPDescription
33003400Invalid phone number.
33034400Invalid grp_* id format.
33030404Contact not found.
33031404Group not found (or not owned by your account).
33035409Contact is HARD-opted-out. Cannot be re-enrolled by the operator.
33036409Contact has pending memberships in other groups. Resolve those first.

See Error Codes for the full reference.