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

MethodPathPurpose
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.

FieldTypeDescription
firstNamestringFirst name. Up to 40 characters.
lastNamestringLast name. Up to 40 characters.
emailstringRFC 5322 email address.
birthDatestring (YYYY-MM-DD)ISO date.
genderstringOne of MALE, FEMALE, OTHER.
addressobjectMailing address. See Address shape below.
isFavoritebooleanOperator flag — purely an annotation, has no effect on messaging.
customFieldsobjectCustom-field values keyed by label. See Custom fields. Discover labels via Custom Fields.

Read-only fields

  • tz is not accepted in the request body. The server derives it from the contact's address.postalCode (when present, US) or from the area code of the phone number, returning one of the 6 supported US zones. See Timezone derivation.
  • phoneNumber is taken from the URL only — don't put it in the body. Including it returns 400.

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"
}
FieldDescription
idStable internal contact ID, prefixed con_. Use the phone number for addressing — id is just a stable cross-reference.
phoneNumberE.164 phone number — the canonical identifier.
tzServer-derived IANA timezone. Read-only. See Timezone derivation.
customFieldsObject keyed by label (see Custom fields). Only fields visible on the schema appear; values for deleted/disabled fields are stripped.
groupsActive memberships. The contact is opted in and messageable for each group listed here.
pendingGroupsMemberships awaiting opt-in confirmation. Created by Group Membership POST with optInMode: CONFIRM. Move to groups once the contact replies YES.
createdAtISO 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 include phoneNumber.
  • tz — server-derived. Re-evaluated automatically whenever address.postalCode changes (since the derivation depends on it).
  • Group memberships and pending memberships — use the Group Membership endpoints instead. The contact response includes groups[] and pendingGroups[] 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:

FieldTypeDescription
streetstringStreet line.
citystringCity.
statestringState or region (free-form).
postalCodestringPostal code.
countrystringISO 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:

TypeValue
TEXTstring
NUMBERnumber
BOOLEANboolean
DATEYYYY-MM-DD string
SINGLE_SELECTstring matching one of the field's options
MULTI_SELECTarray 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:

  1. address.postalCode — if present, US, and resolvable to a supported zone.
  2. phoneNumber NANP area code — fallback when no zip is available or when the zip doesn't resolve.
  3. 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

EndpointLimitWindow
POST / PATCH /api/v1/contacts/by-phone/{phoneNumber} (write)150 requests60 seconds
GET /api/v1/contacts/by-phone/{phoneNumber} (read)150 requests60 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

CodeHTTPDescription
33003400Invalid phone number — phoneNumber is not a valid E.164 format.
33030404Contact not found.
33033400Invalid custom field — see Custom fields above for details.reason values.
33037409A contact with this phone number already exists. Use GET + PATCH to merge.
400Body validation error (e.g. invalid address.country — must be ISO 3166-1 alpha-2). Standard envelope.

See Error Codes for the full reference.