> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tracklysms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Contact

> Add a single contact to a sending list, with optional journey enrollment and custom field tracking.

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

<ParamField body="phone_number" type="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.
</ParamField>

<ParamField body="list_number" type="string" required>
  Sending list phone number in E.164 format (e.g., `+18005551234`). Must be a list owned by your account.
</ParamField>

<ParamField body="use_journeys" type="boolean" default="false">
  When `true`, the contact will be enrolled in any matching Welcome Journeys configured for the target list.
</ParamField>

<ParamField body="skip_journey_if_exists" type="boolean" default="true">
  When `true`, skips journey enrollment if the ListContact record already exists (i.e., the contact was previously added to this list). Only relevant when `use_journeys` is `true`.
</ParamField>

<ParamField body="signup_date" type="string">
  Signup date in ISO 8601 UTC format (e.g., `2025-03-15T14:30:00Z`). Defaults to the current timestamp if omitted.
</ParamField>

<ParamField body="ip_address" type="string">
  IP address of the contact at the time of signup. Used for consent tracking and compliance records.
</ParamField>

<ParamField body="url" type="string">
  URL of the page where the contact signed up. Used for consent tracking and compliance records.
</ParamField>

<ParamField body="consent_method" type="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](/api-reference/v2/contacts/resubscribe) endpoint.
</ParamField>

<ParamField body="consent_source" type="string">
  URL or short description of where consent was captured. Defaults to the `url` value when omitted.
</ParamField>

<ParamField body="consent_language" type="string">
  The exact disclosure text shown to the contact at the time of consent.
</ParamField>

<ParamField body="user_agent" type="string">
  Browser/device user-agent string captured at consent, stored on the compliance record.
</ParamField>

<ParamField body="custom_fields" type="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.
</ParamField>

<ParamField body="override_custom_fields" type="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.
</ParamField>

<ParamField body="ad_network_source" type="string">
  Ad network source for attribution tracking (e.g., `adwords`, `facebook`).
</ParamField>

<ParamField body="ad_network_id" type="string">
  Ad network campaign or account ID for attribution tracking.
</ParamField>

<ParamField body="use_validation" type="boolean" default="false">
  When `true`, the contact's phone number is run through [phone validation](/api-reference/phone/validate) (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).
</ParamField>

<ParamField body="double_optin" type="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`.
</ParamField>

<ParamField body="optin_configuration_id" type="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`.
</ParamField>

## Response Fields

<ResponseField name="success" type="boolean">
  Whether the operation completed successfully.
</ResponseField>

<ResponseField name="contact_id" type="string">
  Unique identifier of the Contact record.
</ResponseField>

<ResponseField name="list_contact_id" type="string">
  Unique identifier of the ListContact record (contact-on-list membership).
</ResponseField>

<ResponseField name="is_new_contact" type="boolean">
  `true` if a new Contact record was created. `false` if the phone number already existed in the system.
</ResponseField>

<ResponseField name="is_new_list_contact" type="boolean">
  `true` if a new ListContact record was created. `false` if the contact was already on this list.
</ResponseField>

<ResponseField name="journeys_enrolled" type="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).
</ResponseField>

<ResponseField name="pending" type="boolean">
  Present and `true` only when `double_optin=true` and the contact is held pending confirmation. The contact is not sendable until they reply `Y`/`YES`/`CONFIRM`.
</ResponseField>

<ResponseField name="resubscribed" type="boolean">
  Present and `true` when the contact was previously unsubscribed and the list has `allow_resubscriptions` enabled — the opt-in reactivated the existing membership through the resubscribe flow instead of creating a new one. When present, `carrier_status` is also returned.
</ResponseField>

<ResponseField name="carrier_status" type="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.
</ResponseField>

<ResponseField name="skipped" type="boolean">
  Present and `true` when the contact was previously unsubscribed and the list does **not** allow resubscriptions. No reactivation occurs; `skip_reason` explains why.
</ResponseField>

<ResponseField name="skip_reason" type="string">
  Returned alongside `skipped: true`. Currently always `previously_unsubscribed`.
</ResponseField>

<ResponseField name="warnings" type="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`.
</ResponseField>

<ResponseField name="validation" type="object">
  Present **only when `use_validation` was `true`**. Contains the full phone validation payload — same schema as the [`/v1/phone/validate`](/api-reference/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.
</ResponseField>

## Examples

<RequestExample>
  ```bash cURL theme={null}
  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"
    }'
  ```

  ```python Python theme={null}
  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']}")
  ```

  ```javascript Node.js theme={null}
  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);
  ```
</RequestExample>

<ResponseExample>
  ```json 201 - Created theme={null}
  {
    "success": true,
    "contact_id": "665a1b2c3d4e5f6a7b8c9d0e",
    "list_contact_id": "665a1b2c3d4e5f6a7b8c9d0f",
    "is_new_contact": true,
    "is_new_list_contact": true,
    "journeys_enrolled": 1
  }
  ```

  ```json 201 - Resubscribed (previously unsubscribed, list allows resubscription) theme={null}
  {
    "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"
  }
  ```

  ```json 201 - Skipped (previously unsubscribed, list disallows resubscription) theme={null}
  {
    "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"
  }
  ```

  ```json 400 - Missing Required Field theme={null}
  {
    "error": "phone_number is required",
    "code": "missing_phone_number"
  }
  ```

  ```json 400 - Invalid Phone Number theme={null}
  {
    "error": "Invalid E.164 phone number format",
    "code": "invalid_phone"
  }
  ```

  ```json 400 - List Not Found theme={null}
  {
    "error": "Sending list not found for this list_number",
    "code": "list_not_found"
  }
  ```

  ```json 402 - Billing Not Enabled (use_validation=true) theme={null}
  {
    "error": "An active payment method is required when use_validation=true",
    "code": "no_payment_method"
  }
  ```
</ResponseExample>

## 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_not_supported_on_resubscribe`                                | `double_optin=true` was sent for a previously unsubscribed contact. Resubscribe without `double_optin`, or re-opt-in through the confirmation flow. |
| 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.                       |
| 400         | `provider_sync_failed`                                                     | Only on the resubscribe-via-create path (list allows resubscription): re-subscription saved locally but the carrier consent sync failed.            |
| 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.                                                                          |
| 429         | `rate_limited`                                                             | Request throttled; retry with exponential backoff after the window resets.                                                                          |
| 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.                                                                                                                |

<Note>
  VOIP numbers may be rejected if your account has the `reject_voip_numbers` setting enabled. Contact support to adjust this setting.
</Note>

## Next Steps

<CardGroup cols={2}>
  <Card title="Importing Contacts" icon="upload" href="/guides/contacts/importing">
    Import contacts from CSV or API
  </Card>

  <Card title="Create Audience" icon="users" href="/api-reference/v2/audiences/create-audience">
    Segment your contacts into audiences
  </Card>
</CardGroup>
