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

> ## Agent Instructions
> Use the public API base URL: https://api.teamfollowup.ai/api.
> Authenticate public API requests with Authorization: Bearer YOUR_API_KEY.
> Use product-owned terms: agents, campaigns, projects (GoHighLevel sub-accounts), contacts, calls, outcomes, skills, cadences, phone numbers.
> Scope requests by locationId rather than projectName.
> Read the guide linked from each API reference group before recommending an endpoint.

# Create a native contact

> Adds one person to this sub-account.

`phone` is the only required field, because a contact that cannot be dialled is not a contact to this product. It is normalised to E.164: a ten-digit number is read as North American, and anything else must carry `+` or `00`. A number that cannot be made into one is refused rather than guessed at, so a typo never rings a stranger.

The number is unique within the sub-account; a second create for the same number is refused. To change a contact you already have, `PATCH` it.

`customFields` takes the keys from **List custom fields**, and a value is checked against its field's type. A key that does not exist is an error rather than a silent drop.

A new contact starts any campaign whose trigger is "New contact".

**The do-not-call history outlives the contact.** If this number asked not to be called before, and the contact was deleted since, the new row comes back carrying that decision, with the date, reason and door it was made through, and `suppressedFromHistory: true` beside it. It will not be dialled. That is deliberate: a paid-for lead that nothing will ever call must not be handed back as a bare success.

Cookie-auth callers must send the `x-csrf-token` header.

Required API key scope: `contacts:write`.



## OpenAPI

````yaml /openapi.yaml post /api/native-contacts
openapi: 3.1.0
info:
  title: TFUAI Server API
  version: 1.0.0
  description: >-
    API reference for the TFUAI SaaS server.


    All published endpoints are mounted under `/api`. Responses use a standard
    envelope: `{ "success": true, ... }` on success and `{ "success": false,
    "error": "..." }` on failure.


    Product API modules are being launched module by module. This spec currently
    exposes only contracted public modules.


    Authenticate public API requests with an API key in the `Authorization`
    header: `Authorization: Bearer YOUR_API_KEY`.
servers:
  - url: https://api.teamfollowup.ai
    description: Production API origin. Public API paths are under /api.
security: []
tags:
  - name: Agents
    description: Manage voice agents, campaigns, workflows, and outcomes.
  - name: Analytics
    description: >-
      Minimal, non-billing performance summary: volume, conversion, and pickup
      metrics.
  - name: Billing
    description: >-
      Billing for your own agency: balance, spend, transaction history, invoices
      and sub-account wallets; auto-recharge, rebilling, Credit Guard and Stripe
      customers; and, with the billing:charge scope, purchases and charges.
  - name: Calls
    description: Review calls, test runs, and execution history.
  - name: Campaign Workflows
    description: >-
      What runs after a call lands: for each outcome (booked, opted out, asked
      for a callback…), an ordered chain of actions — tag the contact, move a
      pipeline stage, send an SMS, book an appointment. Stored as a graph:
      `nodes` are the actions, `edges` say what follows what, and
      `dispositionEntries` maps each outcome to the node its chain starts at.


      **Pick the narrowest endpoint that does the job.**


      | To | Call | |

      | --- | --- | --- |

      | See what fires per outcome | `GET .../workflow/digest` | Reads as plain
      text, no graph walking |

      | Add steps to an outcome | `POST .../outcomes/{outcome}/nodes` | Server
      derives node ids, edge ids, handles, layout |

      | Change a message, tag or setting | `PATCH .../workflow` | Merges;
      carries no graph, so it cannot damage one |

      | Remove one step | `DELETE .../nodes/{nodeId}` | Also re-links the chain
      around it |

      | Author or replace the whole graph | `PUT .../workflow` | Replaces
      everything you send |


      `PUT` is the only one that can re-wire, position nodes, build an `if`,
      save an unconfigured draft, or copy a whole workflow — and the only one
      that can overwrite a change someone else made after you read it. Use it
      when you are genuinely authoring the graph, and send `expectedVersion`
      when you do. For everything else the narrower endpoints are both easier
      and safer.
  - name: Contacts
    description: Contact list, filters, contact call history, and do-not-call actions.
  - name: GHL
    description: CRM variables and contact helpers used by agent and campaign setup flows.
  - name: Native Calendar
    description: >-
      The diary of a sub-account with no CRM: its bookable calendars, the free
      slots they offer, and the appointments on them. Availability is computed
      per request against live bookings and is never cached, because a cached
      slot is one somebody else has already taken.
  - name: Native Contacts
    description: >-
      The people a sub-account with no CRM calls, and the fields it keeps on
      them. This is the system of record for those contacts: there is nowhere
      else they exist. A sub-account with a CRM keeps its contacts there and
      every operation here answers 400 for it.
  - name: Phone Numbers
    description: >-
      Caller ID pool, number search, assignment, movement, and release
      endpoints.
  - name: Power Dialer
    description: >-
      Power Dialer queue, lead cadence, callback, parked lead, and campaign
      schedule endpoints.
  - name: Projects
    description: >-
      Project management endpoints: list, update, disconnect, configure project
      campaigns, and validate setup.
  - name: Skills and Tools
    description: Manage reusable agent behaviors and in-call tool descriptions.
  - name: Split Testing
    description: >-
      Split live campaign traffic across agent variants and shift the weights
      between them.
  - name: Voices
    description: Voice catalogue endpoints for choosing the voice used by an agent.
paths:
  /api/native-contacts:
    post:
      tags:
        - Native Contacts
      summary: Create a native contact
      description: >-
        Adds one person to this sub-account.


        `phone` is the only required field, because a contact that cannot be
        dialled is not a contact to this product. It is normalised to E.164: a
        ten-digit number is read as North American, and anything else must carry
        `+` or `00`. A number that cannot be made into one is refused rather
        than guessed at, so a typo never rings a stranger.


        The number is unique within the sub-account; a second create for the
        same number is refused. To change a contact you already have, `PATCH`
        it.


        `customFields` takes the keys from **List custom fields**, and a value
        is checked against its field's type. A key that does not exist is an
        error rather than a silent drop.


        A new contact starts any campaign whose trigger is "New contact".


        **The do-not-call history outlives the contact.** If this number asked
        not to be called before, and the contact was deleted since, the new row
        comes back carrying that decision, with the date, reason and door it was
        made through, and `suppressedFromHistory: true` beside it. It will not
        be dialled. That is deliberate: a paid-for lead that nothing will ever
        call must not be handed back as a bare success.


        Cookie-auth callers must send the `x-csrf-token` header.


        Required API key scope: `contacts:write`.
      operationId: create-native-contact
      parameters:
        - name: locationId
          in: query
          required: true
          schema:
            type: string
          description: >-
            The sub-account. It is the only thing that identifies the tenant on
            these routes, and it is re-checked against your access on every
            call. A sub-account that keeps its contacts in a CRM answers 400.
          example: nat_fd9e2a8dd4be4396b604
      requestBody:
        required: true
        description: The person.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NativeContactCreateRequest'
            examples:
              default:
                value:
                  firstName: Ana
                  lastName: Diaz
                  phone: '+13125550142'
                  email: ana@example.com
                  timezone: America/Chicago
                  customFields:
                    service_needed: Implants
      responses:
        '200':
          description: >-
            The contact as stored. `suppressedFromHistory` appears when this
            number was already on do-not-call.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactResponse'
              examples:
                default:
                  value:
                    success: true
                    data:
                      _id: nct_0123456789abcdef0123
                      locationId: nat_fd9e2a8dd4be4396b604
                      firstName: Ana
                      lastName: Diaz
                      email: ana@example.com
                      phone: '+13125550142'
                      timezone: America/Chicago
                      address1: 400 W Erie St
                      city: Chicago
                      state: IL
                      postalCode: '60654'
                      customFields:
                        service_needed: Implants
                        budget: '4000'
                      doNotCall: false
                      source: api
                      callCount: 2
                      lastOutcome: TRANSFER
                      createdAt: '2026-09-18T14:02:11.000Z'
                      updatedAt: '2026-09-20T09:14:52.000Z'
        '400':
          description: >-
            The request is malformed, or this sub-account keeps its contacts in
            a CRM.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: A phone number is required.
        '401':
          description: Missing or invalid authentication.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: Unauthorized
        '403':
          description: Authenticated, but without access to this sub-account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: You do not have access to this sub-account.
        '429':
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: TooManyRequests
        '500':
          description: Unexpected server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: Contacts request failed.
        '503':
          description: >-
            The do-not-call history could not be read, so nothing was created.
            Try again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NativeContactsErrorResponse'
              examples:
                default:
                  value:
                    success: false
                    error: >-
                      This number could not be checked against the do-not-call
                      history, so the contact was not created. Try again.
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |-
            curl --request POST \
              --url https://api.teamfollowup.ai/api/native-contacts \
              --header 'Authorization: Bearer YOUR_API_KEY' \
              --header 'Content-Type: application/json' \
              --data '{
              "firstName": "Ana",
              "lastName": "Diaz",
              "phone": "+13125550142",
              "email": "ana@example.com",
              "timezone": "America/Chicago",
              "customFields": {
                "service_needed": "Implants"
              }
            }'
components:
  schemas:
    NativeContactCreateRequest:
      type: object
      additionalProperties: {}
      required:
        - phone
      properties:
        phone:
          type: string
          description: >-
            Required. E.164, or a ten-digit North American number. Anything else
            must carry `+` or `00`.
          example: '+13125550142'
        firstName:
          type: string
          maxLength: 100
          example: Ana
        lastName:
          type: string
          maxLength: 100
          example: Diaz
        email:
          type: string
          maxLength: 254
          example: ana@example.com
        timezone:
          type: string
          description: >-
            An IANA zone such as `America/Chicago`. An unknown one is refused
            rather than producing an agent that offers 3am.
          example: America/Chicago
        address1:
          type: string
          maxLength: 200
        city:
          type: string
          maxLength: 200
        state:
          type: string
          maxLength: 200
        postalCode:
          type: string
          maxLength: 200
        customFields:
          type: object
          additionalProperties:
            type: string
          description: Keyed by field key. A key with no definition is an error.
        doNotCall:
          type: boolean
          description: Create the contact already suppressed.
        doNotCallReason:
          type: string
          maxLength: 500
    NativeContactResponse:
      type: object
      additionalProperties: {}
      required:
        - success
        - data
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/NativeContact'
    NativeContactsErrorResponse:
      type: object
      additionalProperties: {}
      required:
        - success
        - error
      properties:
        success:
          type: boolean
        error:
          type: string
          example: A phone number is required.
        errors:
          type: array
          description: Every reason the write was refused, when there is more than one.
          items:
            type: string
        failed:
          type: array
          description: >-
            On a refused delete: the bookings that would not cancel, which is
            why the contact is still here.
          items:
            type: object
            additionalProperties: {}
        cancelled:
          type: array
          description: >-
            On a refused delete: the bookings that HAD already been cancelled
            before it stopped. They do not come back.
          items:
            type: object
            additionalProperties: {}
        contactKept:
          type: boolean
          description: True when the contact is still here, whatever else failed.
    NativeContact:
      type: object
      additionalProperties: {}
      required:
        - _id
        - locationId
        - phone
      description: >-
        One person a CRM-less sub-account calls. Deliberately the minimum a dial
        needs: who they are, how to reach them, what zone they are in, whether
        they have asked not to be called, plus whatever fields the sub-account
        defines.
      properties:
        _id:
          type: string
          description: >-
            The contact id. Always starts with `nct_`, which is what keeps it
            from colliding with a CRM's ids.
          example: nct_0123456789abcdef0123
        locationId:
          type: string
          example: nat_fd9e2a8dd4be4396b604
        firstName:
          type: string
          example: Ana
        lastName:
          type: string
          example: Diaz
        email:
          type: string
          example: ana@example.com
        phone:
          type: string
          description: E.164, as stored. Unique within the sub-account.
          example: '+13125550142'
        timezone:
          type: string
          description: >-
            IANA zone. Empty when unknown, and the campaign's calling window
            then decides.
          example: America/Chicago
        address1:
          type: string
          example: 400 W Erie St
        city:
          type: string
          example: Chicago
        state:
          type: string
          example: IL
        postalCode:
          type: string
          example: '60654'
        customFields:
          type: object
          additionalProperties:
            type: string
          description: >-
            Values keyed by field key. Stored as text, because a prompt reads
            them as text either way.
          example:
            service_needed: Implants
        caller_pref:
          type: string
          description: >-
            The contact's memory: what earlier calls learned, and what people
            added by hand, as one block the next call reads.
        doNotCall:
          type: boolean
          example: false
        doNotCallAt:
          type:
            - string
            - 'null'
          description: When the decision was made, not when this row was written.
        doNotCallUpdatedAt:
          type:
            - string
            - 'null'
        doNotCallReason:
          type: string
          example: Asked us not to call again.
        doNotCallSource:
          type: string
          description: 'Which door it came through: `call`, `import`, `api`, `dashboard`.'
          example: call
        optedOut:
          type: boolean
          description: >-
            True when the person opted out outright, rather than being switched
            off by somebody here.
        optedOutAt:
          type:
            - string
            - 'null'
        suppressedFromHistory:
          type: boolean
          description: >-
            Present and true when this contact came back carrying a do-not-call
            decision made before it existed. Nothing will dial it. You did not
            ask for that, which is why you are being told.
        source:
          type: string
          description: 'How the contact arrived: `api`, `import`, `lead-link`, `dashboard`.'
          example: api
        callCount:
          type: integer
          example: 2
        lastOutcome:
          type:
            - string
            - 'null'
          description: The last call's outcome, as the engine recorded it.
          example: TRANSFER
        createdAt:
          type: string
          example: '2026-09-18T14:02:11.000Z'
        updatedAt:
          type: string
          example: '2026-09-20T09:14:52.000Z'
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: 'Send your API key as `Authorization: Bearer YOUR_API_KEY`.'

````