curl -X POST "https://api.tracklysms.com/api/v2/contacts" \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+14155559876",
"list_number": "+18005551234",
"use_journeys": true,
"custom_fields": {
"first_name": "Jane",
"source": "landing_page_v2"
},
"ip_address": "203.0.113.42",
"url": "https://example.com/signup"
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/contacts",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json"
},
json={
"phone_number": "+14155559876",
"list_number": "+18005551234",
"use_journeys": True,
"custom_fields": {
"first_name": "Jane",
"source": "landing_page_v2"
},
"ip_address": "203.0.113.42",
"url": "https://example.com/signup"
}
)
data = response.json()
print(f"New contact: {data['is_new_contact']}")
print(f"Journeys enrolled: {data['journeys_enrolled']}")
const response = await fetch("https://api.tracklysms.com/api/v2/contacts", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
phone_number: "+14155559876",
list_number: "+18005551234",
use_journeys: true,
custom_fields: {
first_name: "Jane",
source: "landing_page_v2",
},
ip_address: "203.0.113.42",
url: "https://example.com/signup",
}),
});
const data = await response.json();
console.log("New contact:", data.is_new_contact);
console.log("Journeys enrolled:", data.journeys_enrolled);
{
"success": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": true,
"is_new_list_contact": true,
"journeys_enrolled": 1
}
{
"success": true,
"resubscribed": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": false,
"is_new_list_contact": false,
"journeys_enrolled": 0,
"carrier_status": "cleared"
}
{
"success": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": false,
"is_new_list_contact": false,
"journeys_enrolled": 0,
"skipped": true,
"skip_reason": "previously_unsubscribed"
}
{
"error": "phone_number is required",
"code": "missing_phone_number"
}
{
"error": "Invalid E.164 phone number format",
"code": "invalid_phone"
}
{
"error": "Sending list not found for this list_number",
"code": "list_not_found"
}
{
"error": "An active payment method is required when use_validation=true",
"code": "no_payment_method"
}
Contacts (v2)
Create Contact
Add a single contact to a sending list, with optional journey enrollment and custom field tracking.
POST
/
v2
/
contacts
curl -X POST "https://api.tracklysms.com/api/v2/contacts" \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+14155559876",
"list_number": "+18005551234",
"use_journeys": true,
"custom_fields": {
"first_name": "Jane",
"source": "landing_page_v2"
},
"ip_address": "203.0.113.42",
"url": "https://example.com/signup"
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/contacts",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json"
},
json={
"phone_number": "+14155559876",
"list_number": "+18005551234",
"use_journeys": True,
"custom_fields": {
"first_name": "Jane",
"source": "landing_page_v2"
},
"ip_address": "203.0.113.42",
"url": "https://example.com/signup"
}
)
data = response.json()
print(f"New contact: {data['is_new_contact']}")
print(f"Journeys enrolled: {data['journeys_enrolled']}")
const response = await fetch("https://api.tracklysms.com/api/v2/contacts", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
phone_number: "+14155559876",
list_number: "+18005551234",
use_journeys: true,
custom_fields: {
first_name: "Jane",
source: "landing_page_v2",
},
ip_address: "203.0.113.42",
url: "https://example.com/signup",
}),
});
const data = await response.json();
console.log("New contact:", data.is_new_contact);
console.log("Journeys enrolled:", data.journeys_enrolled);
{
"success": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": true,
"is_new_list_contact": true,
"journeys_enrolled": 1
}
{
"success": true,
"resubscribed": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": false,
"is_new_list_contact": false,
"journeys_enrolled": 0,
"carrier_status": "cleared"
}
{
"success": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": false,
"is_new_list_contact": false,
"journeys_enrolled": 0,
"skipped": true,
"skip_reason": "previously_unsubscribed"
}
{
"error": "phone_number is required",
"code": "missing_phone_number"
}
{
"error": "Invalid E.164 phone number format",
"code": "invalid_phone"
}
{
"error": "Sending list not found for this list_number",
"code": "list_not_found"
}
{
"error": "An active payment method is required when use_validation=true",
"code": "no_payment_method"
}
Creates or updates a contact on the specified sending list. If the contact already exists on the list, their custom fields are merged (not replaced) and existing data is preserved. New contacts have their timezone resolved from the phone number’s area code. Carrier and line-type data are only populated when
use_validation is set to true (a paid carrier lookup — see below).
Body Parameters
string
required
Contact phone number in E.164 format (e.g.,
+14155559876). Set use_validation to true to run a carrier lookup on the number (billed per lookup); by default the contact is stored without a carrier lookup.string
required
Sending list phone number in E.164 format (e.g.,
+18005551234). Must be a list owned by your account.boolean
default:"false"
When
true, the contact will be enrolled in any matching Welcome Journeys configured for the target list.boolean
default:"true"
When
true, skips journey enrollment if the contact is already on this list.string
Signup date in ISO 8601 UTC format (e.g.,
2025-03-15T14:30:00Z). Defaults to the current timestamp if omitted.string
IP address of the contact at the time of signup. Used for consent tracking and compliance records.
string
URL of the page where the contact signed up. Used for consent tracking and compliance records.
string
How consent to message this contact was obtained. One of
web_form, sms_keyword, mobile_app, verbal, written, api_with_proof, double_opt_in. Optional — stored on the contact for TCPA compliance when a valid method is provided, otherwise null. Invalid or verbal values are ignored (stored as null), not rejected. To require a consent method when reactivating an opted-out contact, use the Resubscribe endpoint.string
URL or short description of where consent was captured. Defaults to the
url value when omitted.string
The exact disclosure text shown to the contact at the time of consent.
string
Browser/device user-agent string captured at consent, stored on the compliance record.
object
Key-value pairs of custom data to attach to the contact on this list. Values can be strings, numbers, or booleans. Custom fields are merged with any existing fields — existing keys not included in the request are preserved.
boolean
default:"false"
When
true, replaces the contact’s custom fields entirely instead of merging. When false (default), provided custom fields are merged into existing fields.string
Ad network source for attribution tracking (e.g.,
adwords, facebook).string
Ad network campaign or account ID for attribution tracking.
boolean
default:"false"
When
true, the contact’s phone number is run through phone validation (LRN + DNC) and the result is attached to the response and persisted to the contact’s profile. The contact is created either way — invalid or unreachable numbers are accepted and stored, then can be segmented out of campaigns using the validation_status audience filter. The validation field in the response shows the per-disposition outcome.Billing. Requires an active payment method on file — requests without one return HTTP 402. Each non-cached lookup is billed at $0.003 (same as the /v1/phone/validate endpoint) and rolled up into your monthly invoice. Once a number has been validated for your account, repeat opt-ins of the same number are free indefinitely (validation.cached: true, validation.cost: 0). Pass force=true to bypass the cache and re-validate at $0.003 (rare; LRN data only changes on carrier porting).boolean
default:"false"
When
true, the contact is held pending confirmation (not sendable, not enrolled in journeys) and sent the confirmation message from optin_configuration_id. They activate only after replying Y/YES/CONFIRM. A pending contact is returned with pending: true. Requires optin_configuration_id to reference an active double-opt-in configuration on the same list — otherwise the request is rejected. For double-opt-in contacts, journey enrollment happens at confirmation and follows the opt-in configuration’s journey settings, not this request’s use_journeys.string
ID of the double-opt-in opt-in configuration used to drive confirmation. Required when
double_optin is true, and it must be an active double-opt-in config belonging to the same list. Passing it without double_optin=true is rejected with optin_configuration_requires_double_optin.Response Fields
boolean
Whether the operation completed successfully.
string
Unique identifier for this contact (the phone number’s global ID).
string
Unique identifier for this contact’s membership of the list.
boolean
true if this phone number was new to the system. false if it already existed.boolean
true if a new list membership was created. false if the contact was already on this list.integer
Number of Welcome Journeys the contact was enrolled in. Always
0 if use_journeys is false, and always 0 for a double_optin contact (enrollment is deferred to confirmation).boolean
Present and
true when the contact is held pending confirmation — either because this request created the membership with double_optin=true, or because the contact’s existing membership on this list was already pending confirmation (in which case pending is returned regardless of this request’s double_optin value). Also true when a previously-unsubscribed contact submits double_optin=true and the list’s resubscribe policy allows it: the confirmation ask is (re)sent and the resubscribe completes only once the contact replies Y/YES/CONFIRM. The contact is not sendable until then.boolean
Present and
true when the contact was previously unsubscribed, the list’s resubscribe policy allows it, and double_optin was not set — the opt-in reactivated the existing membership immediately instead of creating a new one. When present, carrier_status is also returned. A resub submitted with double_optin=true returns pending: true instead; it reactivates only on confirmation.string
Returned alongside
resubscribed: true. cleared when the membership was reactivated cleanly, or pending_start_keyword when the prior opt-out was a STOP keyword and the contact must text START to clear the carrier-level block before messages can be delivered.boolean
Present and
true when the opt-in did not activate the membership — either the contact was previously unsubscribed and the list does not allow resubscriptions, or a double-opt-in hold reached its confirmation-ask limit. No reactivation occurs; skip_reason explains why.string
Returned alongside
skipped: true. One of previously_unsubscribed (the list does not allow resubscriptions) or max_confirmation_attempts (a double-opt-in hold reached its confirmation-ask limit before the contact confirmed).array
Array of warning strings about missed configuration or invalid signup data. For example, if there are active Welcome Journeys matching this list but
use_journeys was not set to true. When ip_address or url are provided but invalid, they are silently dropped (not stored) and a warning is included: signup_ip_rejected_non_string, signup_ip_rejected_invalid_format, signup_url_rejected_non_string, signup_url_rejected_empty, signup_url_rejected_overlong, or signup_url_rejected_bad_scheme.object
Present only when
use_validation was true. Contains the full phone validation payload — same schema as the /v1/phone/validate response: phone, valid, line_type, carrier, carrier_raw, ported, country, state, city, on_dnc, disposition, cost, cached. The contact is created regardless of disposition — use the validation_status audience filter to segment out invalid or unreachable numbers from campaigns.Examples
curl -X POST "https://api.tracklysms.com/api/v2/contacts" \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+14155559876",
"list_number": "+18005551234",
"use_journeys": true,
"custom_fields": {
"first_name": "Jane",
"source": "landing_page_v2"
},
"ip_address": "203.0.113.42",
"url": "https://example.com/signup"
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/contacts",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json"
},
json={
"phone_number": "+14155559876",
"list_number": "+18005551234",
"use_journeys": True,
"custom_fields": {
"first_name": "Jane",
"source": "landing_page_v2"
},
"ip_address": "203.0.113.42",
"url": "https://example.com/signup"
}
)
data = response.json()
print(f"New contact: {data['is_new_contact']}")
print(f"Journeys enrolled: {data['journeys_enrolled']}")
const response = await fetch("https://api.tracklysms.com/api/v2/contacts", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
phone_number: "+14155559876",
list_number: "+18005551234",
use_journeys: true,
custom_fields: {
first_name: "Jane",
source: "landing_page_v2",
},
ip_address: "203.0.113.42",
url: "https://example.com/signup",
}),
});
const data = await response.json();
console.log("New contact:", data.is_new_contact);
console.log("Journeys enrolled:", data.journeys_enrolled);
{
"success": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": true,
"is_new_list_contact": true,
"journeys_enrolled": 1
}
{
"success": true,
"resubscribed": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": false,
"is_new_list_contact": false,
"journeys_enrolled": 0,
"carrier_status": "cleared"
}
{
"success": true,
"contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
"list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
"is_new_contact": false,
"is_new_list_contact": false,
"journeys_enrolled": 0,
"skipped": true,
"skip_reason": "previously_unsubscribed"
}
{
"error": "phone_number is required",
"code": "missing_phone_number"
}
{
"error": "Invalid E.164 phone number format",
"code": "invalid_phone"
}
{
"error": "Sending list not found for this list_number",
"code": "list_not_found"
}
{
"error": "An active payment method is required when use_validation=true",
"code": "no_payment_method"
}
Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | missing_phone_number | The phone_number field was not provided in the request body. |
| 400 | missing_list_number | The list_number field was not provided in the request body. |
| 400 | list_not_found | No sending list with the given list_number exists for this account. |
| 400 | invalid_phone | The phone_number is not a valid E.164 phone number. |
| 400 | invalid_list_number | The list_number is not a valid E.164 phone number. |
| 400 | double_optin_requires_config | double_optin=true was sent without optin_configuration_id. |
| 400 | invalid_optin_configuration | optin_configuration_id is not an active double-opt-in config on this list. |
| 400 | optin_configuration_requires_double_optin | optin_configuration_id was sent without double_optin=true. |
| 400 | double_optin_take_failed | The double-opt-in hold could not be created; the pending membership was rolled back. Retry the request. |
| 400 | save_error | The contact subscription could not be saved. Retry the request. |
| 400 | cooling_off | Only when the list allows resubscription and the contact recently opted out via STOP: the cooling-off window has not elapsed. Also returned when double_optin=true. |
| 400 | compliance_suppressed | The contact is suppressed by a compliance decision (e.g. a GDPR erasure) and cannot be resubscribed by any opt-in. |
| 400 | provider_sync_failed | Only on the resubscribe-via-create path (list allows resubscription): the carrier consent sync failed, so the re-subscription is aborted with no local state change — the contact remains inactive. |
| 400 | payload_too_large | The request payload exceeds the 4KB limit. |
| 401 | invalid_credentials | Missing or invalid X-Api-Key header. |
| 403 | account_suspended | Your account is suspended. Resolve outstanding billing or contact support. |
| 402 | no_billing_config / no_payment_method / payment_failed / suspended | use_validation=true requires an active payment method on file. |
| 503 | provider_error | use_validation=true could not reach the validation provider. Retry later. |
| 503 | profile_create_failed | The contact profile could not be created; retry the request. |
| 500 | internal_error | An unexpected server error occurred. |
VOIP numbers are created like any other contact. When
use_validation is true, a VOIP number is marked unreachable in the validation result rather than being rejected — segment it out at send time with the validation_status audience filter.Next Steps
Importing Contacts
Import contacts from CSV or API
Create Audience
Segment your contacts into audiences