Contact Management
Contacts are the people you message. Every phone number you send to can be stored as a contact with a name, email, group memberships, and custom fields. Contacts are required for campaigns (which target groups) and useful for transactional messaging when you want to track recipients.
The API gives you three resources: Contacts (individual people), Contact Groups (named collections for campaign targeting), and Contact Fields (custom data fields you define).
Create or update a contact
POST /v1/contactsuses upsert behavior — if a contact with the same phone number exists, it's updated rather than duplicated. You can safely call this without checking whether the contact exists first.
curl --request POST \
--url https://a.eztexting.com/v1/contacts \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--user 'your_username:your_password' \
--data '{
"phoneNumber": "15551234567",
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"groupIdsAdd": [
"987654"
]
}'Contact fields
| Field | Type | Description |
|---|---|---|
phoneNumber | string | Phone number in E.164 format (e.g., 15551234567) |
firstName | string | First name |
lastName | string | Last name |
email | string | Email address |
note | string | Free-text note |
custom1–custom5 | string | Custom field values (define fields via Contact Fields API) |
groupIdsAdd | string[] | Group IDs to add this contact to |
groupIdsRemove | string[] | Group IDs to remove this contact from |
Filtering contacts
The GET /v1/contacts endpoint supports filters on phoneNumber, firstName, lastName, email, and groupName (all partial-match), plus exact-match filters on optOut (boolean) and source. The source field tells you where a contact originated:
| Source | Description |
|---|---|
Unknown | Origin not determined |
WebInterface | Added via the EZ Texting dashboard |
Upload | Imported via file upload |
WebWidget | Opted in via an embeddable web widget |
API | Created through the REST API |
Keyword | Opted in by texting a keyword |
Sort results by field name, ascending or descending (prefix with -). For example: sort=lastName or sort=-firstName. Pagination uses page (1-based) and size (10, 20, 50, 100, or 200).
Batch import contacts
Use POST /v1/contacts/batchwhen syncing multiple contacts at once — importing a subscriber list, syncing from your CRM, or processing a file. The contacts array is required. Both groupIdsAdd and groupIdsRemove apply to all contacts in the batch.
curl --request POST \
--url https://a.eztexting.com/v1/contacts/batch \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--user 'your_username:your_password' \
--data '{
"contacts": [
{
"phoneNumber": "15551234567",
"firstName": "Jane",
"lastName": "Smith"
},
{
"phoneNumber": "15559876543",
"firstName": "Bob",
"lastName": "Jones"
},
{
"phoneNumber": "15551112222",
"firstName": "Alice",
"lastName": "Lee"
}
],
"groupIdsAdd": [
"876543"
]
}'Contact groups
Groups organize contacts for campaign targeting. A campaign sends to one or more groups via the groupIds parameter on POST /v1/messages. Contacts can belong to multiple groups simultaneously.
# Create a group
curl --request POST \
--url https://a.eztexting.com/v1/contact-groups \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--user 'your_username:your_password' \
--data '{"name": "VIP Customers", "note": "High-value customers for exclusive offers"}'
# Add contacts to the group (phone numbers are query parameters)
curl --request POST \
--url 'https://a.eztexting.com/v1/contact-groups/987654/contacts?phoneNumbers=15551234567&phoneNumbers=15559876543' \
--header 'accept: application/json' \
--user 'your_username:your_password'
# Remove contacts from the group (phone numbers are query parameters)
curl --request DELETE \
--url 'https://a.eztexting.com/v1/contact-groups/987654/contacts?phoneNumbers=15559876543' \
--header 'accept: application/json' \
--user 'your_username:your_password'Group design patterns
- Lifecycle groups — "New Subscribers", "Active Customers", "At Risk", "Churned". Move contacts between groups as their status changes.
- Segment groups — "VIP Customers", "West Coast", "Enterprise". A contact can be in multiple segment groups. Target campaigns at any combination.
- Campaign-specific groups — "Black Friday 2026", "Product Launch". Add contacts as they qualify, then target the campaign.
Custom fields
Every contact has built-in fields: phoneNumber, firstName, lastName, email, and note.
Beyond those, there are two flavors of custom data — custom fields (the five built-in slots custom1–custom5) and additional custom fields (anything you define via the Contact Fields API). Both work, but they sit in different places on the contact record — knowing the difference is the single most common stumbling block in the Contacts API.
custom1–custom5) are top-level properties, like firstName. Additional custom fields (anything beyond the first five, created via the Contact Fields API) live inside a nested values object keyed by field ID. They are not a continuation of the customN numbering.Side-by-side on the contact record
| Field type | Where it lives | How many | How to set |
|---|---|---|---|
| Built-in | Top-level | 5 fixed (phoneNumber, firstName, lastName, email, note) | Set directly on the contact body |
Custom fields (custom1–custom5) | Top-level | Exactly 5, always present | Set directly on the contact body as custom1, custom2, etc. |
| Additional custom fields (Contact Fields API) | Nested inside values | Unlimited, each with its own ID | Create the field with POST /v1/contact-fields, then set the value in values[<fieldId>] |
A contact with both kinds of custom data
This is what a contact body looks like when it uses a built-in slot (custom1) and an additional custom field (fld_loyalty_points) at the same time:
{
"phoneNumber": "15551234567",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"note": "VIP customer",
"custom1": "Gold", // custom field — top level
"custom2": "West Coast", // custom field — top level
"custom3": null,
"custom4": null,
"custom5": null,
"values": { // additional custom fields created via
"fld_loyalty_points": "2450", // POST /v1/contact-fields
"fld_last_purchase": "2026-03-15"
}
}Picking which to use
- Use
custom1–custom5when five slots is enough. They're the simplest path — no setup, just set the value on the contact body. - Use additional custom fields (Contact Fields API) when you need more than five, want a human-readable field name instead of
customN, or want typed fields (text,number,date). Create the field once with alabelandtype, get back anid, then reference thatidinside the contact'svaluesobject. - Mixing both is fine. A contact can have
custom1set AND entries invalues— they don't collide.
Creating and using an additional custom field
# Create a custom field
curl --request POST \
--url https://a.eztexting.com/v1/contact-fields \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--user 'your_username:your_password' \
--data '{"label": "Membership Tier", "type": "TEXT"}'
# Use the field when creating a contact
curl --request POST \
--url https://a.eztexting.com/v1/contacts \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--user 'your_username:your_password' \
--data '{"phoneNumber": "15551234567", "firstName": "Jane", "custom1": "Gold"}'Listing and filtering
GET /v1/contacts returns contacts with pagination via page (zero-indexed) and size parameters. Use query parameters to filter rather than pulling all contacts and filtering client-side. Retrieve a specific contact by phone number with GET /v1/contacts/{phoneNumber}.
See Pagination for details on navigating large result sets.
Endpoints reference
| Endpoint | Method | Description |
|---|---|---|
/v1/contacts | POST | Create or update a contact (upsert) |
/v1/contacts | GET | List contacts with pagination |
/v1/contacts/batch | POST | Batch create or update contacts |
/v1/contacts/{phoneNumber} | GET | Get a specific contact |
/v1/contacts/{phoneNumber} | DELETE | Delete a contact |
/v1/contact-groups | POST | Create a group |
/v1/contact-groups | GET | List all groups |
/v1/contact-groups/{id} | GET / PUT / DELETE | Get, update, or delete a group |
/v1/contact-groups/{id}/contacts | POST | Add contacts to a group |
/v1/contact-groups/{id}/contacts | DELETE | Remove contacts from a group |
/v1/contact-fields | POST | Create a custom field |
/v1/contact-fields | GET | List all custom fields |
/v1/contact-fields/{id} | PUT / DELETE | Update or delete a custom field |
Error handling
| Status | Meaning | What to do |
|---|---|---|
400 | Bad Request | Check required fields. Verify phone number format (E.164). |
401 | Unauthorized | Credentials invalid or token expired. Refresh your token. |
404 | Not Found | The contact, group, or field doesn't exist. Verify the ID. |
409 | Conflict | Operation conflicts with current state (e.g., contact already in group). |
422 | Unprocessable | Invalid phone number format or field value. |
429 | Rate Limited | 200 req/min exceeded. Wait for X-Rate-Limit-Retry-After-Milliseconds header. |
500 | Server Error | Retry with exponential backoff. Contact support if it persists. |
Next steps
- Marketing Campaigns — Send campaigns to the groups you've created
- Transactional Messaging — Send to individual contacts by phone number
- Conversational Messaging — View conversation threads with your contacts
- Automations — Auto-add contacts to groups via keywords and webhooks
- API Reference — Full interactive endpoint documentation