Contacts
Create, read, and update contacts in your account. Contacts are addressed by E.164 phone number — the phone is the canonical identifier and appears in the URL of every operation in this section.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/contacts/by-phone/{phoneNumber} | Create a contact |
| GET | /api/v1/contacts/by-phone/{phoneNumber} | Read a contact |
| PATCH | /api/v1/contacts/by-phone/{phoneNumber} | Update profile fields |
Group memberships are managed at a separate path — see Group Membership.
phoneNumber in every URL must be a valid E.164 phone number (+ followed by country code and subscriber digits). URL-encode the + as %2B if your HTTP client doesn't handle it automatically.
POST — Create a contact
Registers a new contact at the given phone number. The phone is taken from the URL — do not send it in the body. Creation does not trigger any consent flow on its own; the contact becomes messageable only after you add them to a group (see Group Membership).
curl -X POST https://platform.textingline.com/api/v1/contacts/by-phone/+18005551234 \
-u "your_key_id:your_key_secret" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Alice",
"lastName": "Doe",
"email": "[email protected]",
"birthDate": "1985-04-12",
"address": {
"street": "1 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
},
"isFavorite": false,
"customFields": {
"Favorite Color": "Red",
"Age": 42,
"Interests": ["Sports", "Music"]
}
}'A successful response returns 201 Created with the contact projection (see Response shape below).
Request body
All fields are optional. Unknown properties are rejected with 400.
| Field | Type | Description |
|---|---|---|
firstName | string | First name. Up to 40 characters. |
lastName | string | Last name. Up to 40 characters. |
email | string | RFC 5322 email address. |
birthDate | string (YYYY-MM-DD) | ISO date. |
gender | string | One of MALE, FEMALE, OTHER. |
address | object | Mailing address. See Address shape below. |
isFavorite | boolean | Operator flag — purely an annotation, has no effect on messaging. |
customFields | object | Custom-field values keyed by label. See Custom fields. Discover labels via Custom Fields. |
Read-only fields
tzis not accepted in the request body. The server derives it from the contact'saddress.postalCode(when present, US) or from the area code of the phone number, returning one of the 6 supported US zones. See Timezone derivation.phoneNumberis taken from the URL only — don't put it in the body. Including it returns400.
Idempotency
If a contact already exists at this phone number, POST returns 409 ContactAlreadyExists (33037) and does not modify the existing record. To merge new data into an existing contact, fetch it with GET and then PATCH instead:
# Common upsert pattern
curl -X GET https://platform.textingline.com/api/v1/contacts/by-phone/+18005551234 \
-u "your_key_id:your_key_secret"
# If 404 → POST a new contact.
# If 200 → PATCH any changed fields.See Upsert a contact from a CRM for a worked example.
GET — Read a contact
Returns the full contact projection including profile fields, derived timezone, group memberships (active + pending), and the customFields map keyed by label.
curl https://platform.textingline.com/api/v1/contacts/by-phone/+18005551234 \
-u "your_key_id:your_key_secret"Response shape
{
"id": "con_BoZHt0bxx5Qg2qN0",
"phoneNumber": "+18005551234",
"firstName": "Alice",
"lastName": "Doe",
"email": "[email protected]",
"birthDate": "1985-04-12",
"address": {
"street": "1 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
},
"tz": "America/New_York",
"isFavorite": false,
"customFields": {
"Favorite Color": "Red",
"Age": 42,
"Interests": ["Sports", "Music"]
},
"groups": [
{
"id": "grp_DsAdb4EBYBh3z25sBA1q",
"name": "VIP Customers",
"optInAt": "2026-04-01T00:00:00.000Z",
"tags": [
{
"id": "tag_3pXAGm5x9YzKw2VPHCnB",
"name": "Gold",
"createdAt": "2025-08-01T00:00:00.000Z",
"updatedAt": "2025-08-02T00:00:00.000Z"
}
]
}
],
"pendingGroups": [],
"createdAt": "2026-01-15T10:30:00.000Z"
}| Field | Description |
|---|---|
id | Stable internal contact ID, prefixed con_. Use the phone number for addressing — id is just a stable cross-reference. |
phoneNumber | E.164 phone number — the canonical identifier. |
tz | Server-derived IANA timezone. Read-only. See Timezone derivation. |
customFields | Object keyed by label (see Custom fields). Only fields visible on the schema appear; values for deleted/disabled fields are stripped. |
groups | Active memberships. The contact is opted in and messageable for each group listed here. |
pendingGroups | Memberships awaiting opt-in confirmation. Created by Group Membership POST with optInMode: CONFIRM. Move to groups once the contact replies YES. |
createdAt | ISO 8601 timestamp of when the contact was created. |
Profile fields (firstName, lastName, email, birthDate, gender, address) are omitted from the response when unset.
PATCH — Update a contact
Updates one or more profile fields. Uses JSON Merge Patch (RFC 7396) semantics:
- Fields absent from the body are left unchanged.
- Fields with a value are set.
- Fields with
null(or empty string for nullable string fields) are cleared.
curl -X PATCH https://platform.textingline.com/api/v1/contacts/by-phone/+18005551234 \
-u "your_key_id:your_key_secret" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Alicia",
"address": {
"postalCode": "94102",
"country": "US"
},
"customFields": {
"Favorite Color": "Blue"
}
}'What can't be changed via PATCH
phoneNumber— the URL path is the canonical identifier. The body must not includephoneNumber.tz— server-derived. Re-evaluated automatically wheneveraddress.postalCodechanges (since the derivation depends on it).- Group memberships and pending memberships — use the Group Membership endpoints instead. The contact response includes
groups[]andpendingGroups[]for visibility, but PATCH on this endpoint never touches them.
customFields per-label merge
The customFields map merges per-label — only labels present in the body are touched:
{ "customFields": { "Favorite Color": "Blue" } }The above changes only Favorite Color and leaves every other custom field unchanged. To clear a single field, send null:
{ "customFields": { "Favorite Color": null } }To clear every custom field at once, send customFields: null:
{ "customFields": null }A successful response returns 200 OK with the same shape as GET.
Address shape
The address object on the request and response uses a fixed sub-shape:
| Field | Type | Description |
|---|---|---|
street | string | Street line. |
city | string | City. |
state | string | State or region (free-form). |
postalCode | string | Postal code. |
country | string | ISO 3166-1 alpha-2 country code (e.g. US, CA, GB). Case-insensitive on writes, normalized to uppercase. Invalid codes return 400. |
On PATCH, send the address sub-fields you want to change — omitted sub-fields are kept. To clear the entire address, send "address": null.
Custom fields
customFields is a JSON object keyed by label — the human-readable name you gave the field in your dashboard (e.g. "Favorite Color", "Birthday"). Match is case-insensitive and trims whitespace.
{
"customFields": {
"Favorite Color": "Red",
"Age": 42,
"Birthday": "1985-04-12",
"Interests": ["Sports", "Music"]
}
}Value shape depends on the field's type:
| Type | Value |
|---|---|
TEXT | string |
NUMBER | number |
BOOLEAN | boolean |
DATE | YYYY-MM-DD string |
SINGLE_SELECT | string matching one of the field's options |
MULTI_SELECT | array of strings, each matching an option |
Discover the available labels, types, and options via Custom Fields.
Validation errors return 33033 InvalidCustomField with details.label and details.reason:
{
"code": 33033,
"message": "Invalid custom field \"Faverite Color\": unknown_label",
"status": 400,
"moreInfo": "https://docs.smsfactory.com/docs/error-codes",
"details": {
"label": "Faverite Color",
"reason": "unknown_label"
}
}details.reason is one of: unknown_label, duplicate_label_input, type_mismatch, unknown_option, duplicate_option, invalid_date_format, invalid_calendar_date, length_exceeded. See Error Codes for descriptions of each.
Timezone derivation
tz is derived server-side every time the contact's phone or address changes, in this order:
address.postalCode— if present, US, and resolvable to a supported zone.phoneNumberNANP area code — fallback when no zip is available or when the zip doesn't resolve.- Unset — for non-US contacts or numbers we can't resolve.
The product supports 6 US IANA zones: America/New_York, America/Chicago, America/Denver, America/Los_Angeles, America/Anchorage, Pacific/Honolulu. Non-US contacts intentionally have no tz field in the response — sending to them is allowed; timezone-aware features (e.g. flow scheduling) treat them as having no preferred local time.
PATCH a contact's address.postalCode and tz is recomputed automatically on the next read.
Rate limit
| Endpoint | Limit | Window |
|---|---|---|
POST / PATCH /api/v1/contacts/by-phone/{phoneNumber} (write) | 150 requests | 60 seconds |
GET /api/v1/contacts/by-phone/{phoneNumber} (read) | 150 requests | 60 seconds |
Writes share a single contactApiWrite bucket with the Group Membership write endpoints. Reads share a single contactApiRead bucket with Groups LIST/GET and Custom Fields. See Rate Limits for headers and bucket details.
Common errors
| Code | HTTP | Description |
|---|---|---|
33003 | 400 | Invalid phone number — phoneNumber is not a valid E.164 format. |
33030 | 404 | Contact not found. |
33033 | 400 | Invalid custom field — see Custom fields above for details.reason values. |
33037 | 409 | A contact with this phone number already exists. Use GET + PATCH to merge. |
| — | 400 | Body validation error (e.g. invalid address.country — must be ISO 3166-1 alpha-2). Standard envelope. |
See Error Codes for the full reference.