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
| Method | Path | Purpose |
|---|---|---|
| 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
| Field | Type | Required | Description |
|---|---|---|---|
optInMode | string | No | One 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
| Mode | What happens |
|---|---|
DIRECT | Caller 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.). |
CONFIRM | The 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:
| State | Code | What it means |
|---|---|---|
| HARD opt-out | 33035 | The 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 groups | 33036 | The 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[]
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 repliesYES, 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
| Endpoint | Limit | Window |
|---|---|---|
POST / DELETE /api/v1/contacts/by-phone/{phoneNumber}/groups/{groupId} (write) | 150 requests | 60 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
| Code | HTTP | Description |
|---|---|---|
33003 | 400 | Invalid phone number. |
33034 | 400 | Invalid grp_* id format. |
33030 | 404 | Contact not found. |
33031 | 404 | Group not found (or not owned by your account). |
33035 | 409 | Contact is HARD-opted-out. Cannot be re-enrolled by the operator. |
33036 | 409 | Contact has pending memberships in other groups. Resolve those first. |
See Error Codes for the full reference.