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

# Submit signals

> Submit a batch of inputs for background scoring.

Submit one or more people and the signals you want run. You get a `202` with a `request_id`
immediately; results arrive by [polling](https://docs.truthfactor.ai/api-reference/poll-results.md) or on an optional signed
[webhook](https://docs.truthfactor.ai/essentials/webhooks.md). See [Signals](https://docs.truthfactor.ai/essentials/signals.md) for the catalogue and
required inputs.


## OpenAPI

````yaml POST /v1/signals
openapi: 3.1.0
info:
  title: TruthFactor Signal API
  version: 0.1.0
  description: >-
    Identity corroboration. Tell us what you know about a person — a phone
    number, a name, an

    email, a professional profile — and we tell you which of it holds up.


    ## Authentication


    Every request carries a bearer key. An admin of an active account mints keys
    from the

    dashboard; the secret is shown once at creation and never again. Approval
    gates the account,

    not the key.


    ```http

    Authorization: Bearer tf_live_…

    ```


    A missing or unknown key returns `401` with `{"error":"invalid api key"}`.


    ## Quickstart


    Submitting is asynchronous: you get a `request_id` straight away and poll
    it. A batch of one

    is fine.


    **1. Submit**


    ```bash

    curl -sS -X POST https://api.truthfactor.ai/api/v1/signals \
      -H "Authorization: Bearer $TF_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"inputs":[{"phone":"+447911123456","correlation_id":"lead-4417"}],"signals":["phone_connectivity"]}'
    ```


    Returns `202`:


    ```json

    {
      "request_id": "rq_8efcd577456f4bd1",
      "requested_datetime": "2026-09-26T14:03:11Z",
      "status": "pending",
      "accepted": 1
    }

    ```


    **2. Poll**


    ```bash

    curl -sS https://api.truthfactor.ai/api/v1/signals/rq_8efcd577456f4bd1 \
      -H "Authorization: Bearer $TF_API_KEY"
    ```


    `status` stays `pending` until every item is terminal, then `complete`.
    While it is still

    running the batch has no completion time yet, and `completed_datetime`
    carries `null` — the

    key is always present:


    ```json

    {
      "request_id": "rq_8efcd577456f4bd1",
      "requested_datetime": "2026-09-26T14:03:11Z",
      "completed_datetime": null,
      "status": "pending",
      "data": []
    }

    ```


    Once it is done:


    ```json

    {
      "request_id": "rq_8efcd577456f4bd1",
      "requested_datetime": "2026-09-26T14:03:11Z",
      "completed_datetime": "2026-09-26T14:03:41Z",
      "status": "complete",
      "data": [
        {
          "correlation_id": "lead-4417",
          "input": { "phone": "+447911123456", "correlation_id": "lead-4417" },
          "status": "scored",
          "score": 92,
          "band": "clear",
          "reason": "phone_connectivity",
          "signals": { "phone_connectivity": { "trusted": true, "confidence": 92 } }
        }
      ]
    }

    ```


    `correlation_id` is yours. Pass whatever identifies the row on your side and
    it comes back

    untouched, so you never have to match on array position.


    ## Reading a verdict


    This is the part worth getting right.


    | Value | Means |

    | --- | --- |

    | `trusted: true` | Confirmed. The number connects, or the name or profile
    matches. |

    | `trusted: false` | Confirmed otherwise. A positive finding, not an absence
    of one. |

    | `trusted: null` | Unconfirmable — no source could answer. Never read this
    as `false`. |

    | `confidence` | How strongly the sources agreed, 0–100. Always present: it
    carries `null` exactly when `trusted` is `null`. |


    `false` means we checked and the answer was no. `null` means nobody could
    tell you. Treating

    the second as the first is how a good lead gets discarded.


    ## Signals


    Name the signals you want, or omit the field and every signal whose required
    inputs you

    supplied will run, subject to your account's plan.


    ### Available signals


    | Signal | Needs | Tells you |

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

    | `phone_name_match` | phone, first_name, last_name | Tells you if the owner
    of the phone belongs to the given name. |

    | `phone_connectivity` | phone | Whether the phone number reaches a live
    subscriber. Resolves outside the US and Canada. In the US and Canada it
    returns `trusted: null` by design — `premium_phone_connectivity` is the
    signal that answers there. A `trusted: null` is never a `false`. |

    | `phone_profile_match` | phone, profile_url | Tells you if a phone belongs
    to the professional network profile. |

    | `premium_phone_name_match` | phone, first_name, last_name |
    `phone_name_match` on the premium tier: the same question, resolved against
    more than the standard signal reaches, so fewer rows come back unresolved.
    Needs a premium grant on your account. |

    | `premium_phone_connectivity` | phone | `phone_connectivity` on the premium
    tier: the same question, with coverage in the US and Canada where the
    standard signal returns no result. Needs a premium grant on your account. |

    | `premium_phone_profile_match` | phone, profile_url | `phone_profile_match`
    on the premium tier: the same question, resolved against more than the
    standard signal reaches, so fewer rows come back unresolved. Needs a premium
    grant on your account. |


    ### Coming soon


    Not callable yet. They are listed so you can see where the API is going; a
    request naming

    one is rejected rather than silently ignored.


    | Signal | Needs | Tells you |

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

    | `email_name_match` | email, first_name, last_name | Tells you if the owner
    of the email belongs to the given name. |

    | `email_profile_match` | email, profile_url | Tells you if an email belongs
    to the professional network profile. |

    | `phone_type` | phone | Whether the number is a mobile, a landline or VoIP.
    |

    | `phone_ported` | phone | Tells you the last time the phone was ported (a
    recent port is a fraud co-signal). |

    | `phone_activity` | phone | How established the number is over time. A
    long, consistent history corroborates a real person; a number that appears
    from nowhere does not. |

    | `profile_account_age_risk` | profile_url | Risk that the professional
    profile was recently created (a very new account is a strong fraud signal).
    |


    ## When an item is rejected


    A rejected item is a normal outcome for that row, not a failed request. The
    batch still

    returns `200` and the other items still carry verdicts. The reason appears
    on the item:


    - `no_runnable_signal`

    - `invalid_phone`


    ## When an item is unresolved


    An item comes back `unresolved` when it ran but every signal you asked for
    returned

    `trusted: null` — nobody could answer, so there is nothing to score. It
    carries no `score`,

    `band` or item-level `reason`. Unlike a rejected item, it RETURNS its full
    `signals` map, and

    each null verdict carries an `unresolved_reason` telling you why:


    | `unresolved_reason` | Means |

    | --- | --- |

    | `inconclusive` | A source ran or answered but could not conclude on this
    input. |

    | `source_unavailable` | A source could not be reached — a transient outage.
    Worth a retry. |

    | `no_coverage` | No source covers this input, so none was consulted. |


    ## Results by webhook


    Instead of polling, pass `webhook_url` on submit and we POST the completed
    payload to it,

    signed. Results stay fetchable with `GET`, so a failed delivery never loses
    them.


    ## Webhook signature


    Every delivery carries `X-TruthFactor-Signature`: HMAC-SHA256 over the exact

    bytes of the request body, lowercase hex, prefixed `sha256=`.

    Verify before you parse — re-serialising the JSON changes the bytes, and the
    signature will no

    longer match. Compare the two values in constant time.


    Run this and you get the vector below, so it doubles as a test of your
    implementation:


    ```bash

    SIGNING_SECRET='whsec_example_do_not_use_in_production'

    RAW_BODY='{"request_id":"rq_b42e","requested_datetime":"2026-09-26T18:20:05Z","completed_datetime":"2026-09-26T18:20:38Z","status":"complete"}'


    echo "sha256=$(printf '%s' "$RAW_BODY" \
      | openssl dgst -sha256 -hmac "$SIGNING_SECRET" -r | cut -d' ' -f1)"
    # sha256=12362f6de2f6f2b7577e1a509d98e4cb9fd70762c4f8c769bcb07d0efd7bfbad

    ```
servers:
  - url: https://api.truthfactor.ai/api
    description: Production (routes live under /api/v1).
  - url: https://api.staging.truthfactor.ai/api
    description: Staging.
security:
  - bearerAuth: []
paths:
  /v1/signals:
    post:
      summary: Submit a batch for background scoring
      description: >-
        Validates, persists and enqueues per-input work, returning 202 with a
        request_id. Results arrive by webhook (if webhook_url is given) and/or
        by polling.
      operationId: submitSignals
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
          description: >-
            Optional. A repeated Idempotency-Key (or an identical batch within a
            window) returns the same request_id without re-charging.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignalsRequest'
            examples:
              poll_clear:
                summary: Poll — a consistent identity
                value:
                  inputs:
                    - phone: '+447911123456'
                      first_name: Dana
                      last_name: Okafor
                      profile_url: linkedin.com/in/dana-okafor
                      email: dana.okafor@example.com
                      correlation_id: signup-3391
                  signals:
                    - phone_name_match
                    - phone_connectivity
                    - phone_profile_match
                    - email_name_match
              webhook_name_mismatch:
                summary: Webhook — a name mismatch
                value:
                  inputs:
                    - phone: '+447911123457'
                      first_name: Jordan
                      last_name: Reyes
                      profile_url: linkedin.com/in/jordan-reyes
                      correlation_id: order-5567
                  signals:
                    - phone_name_match
                    - phone_connectivity
                    - phone_profile_match
                  webhook_url: https://acme.example.com/hooks/truthfactor
      responses:
        '202':
          description: Accepted — background scoring enqueued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
        '401':
          description: invalid api key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: out_of_credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            invalid_request — malformed body, unknown/ungoverned signal, empty
            inputs, or over the size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            rate_limited — the account's request rate, or its ceiling of items
            queued in non-terminal batches. Carries Retry-After.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            Retry-After:
              description: >-
                Whole seconds to wait before retrying. Always present on a 429
                and always at least 1. The request-rate refusal computes it from
                the account's bucket; the in-flight ceiling's is deliberately
                coarse, since when queued work drains depends on the sources.
              schema:
                type: integer
      security:
        - bearerAuth: []
components:
  schemas:
    SignalsRequest:
      type: object
      properties:
        inputs:
          type: array
          items:
            $ref: '#/components/schemas/SignalInput'
          minItems: 1
        signals:
          type: array
          items:
            $ref: '#/components/schemas/Signal'
          minItems: 1
          description: >-
            Which signals to run for every input. Omitted → all signals whose
            required inputs are present, subject to phase and per-account
            governance.
        webhook_url:
          type: string
          format: uri
          description: Where the completed payload is POSTed, signed. Optional.
        skip_cache:
          type: boolean
          description: >-
            Bypass the result cache and resolve every signal fresh — a live
            probe/dial is placed rather than a cached verdict returned — and
            never idempotency-replay this submission (a new request_id is always
            minted). Default false. Optional.
      required:
        - inputs
      additionalProperties: false
      description: The POST /v1/signals body.
    AcceptedResponse:
      type: object
      properties:
        request_id:
          type: string
          minLength: 1
        requested_datetime:
          type: string
          format: date-time
        status:
          type: string
          const: pending
        accepted:
          type: integer
          minimum: 0
      required:
        - request_id
        - requested_datetime
        - status
        - accepted
      additionalProperties: false
      description: The 202 acknowledgement of POST /v1/signals.
    Error:
      type: object
      required:
        - error
      description: An error body. error is a machine-readable code.
      properties:
        error:
          type: string
    SignalInput:
      type: object
      properties:
        phone:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        profile_url:
          type: string
        correlation_id:
          type: string
          description: >-
            The caller's join key, echoed back verbatim on the matching result
            item.
      additionalProperties: false
      description: One submitted person. Every field is optional — a single field is valid.
    Signal:
      type: string
      enum:
        - phone_name_match
        - phone_connectivity
        - phone_profile_match
        - email_name_match
        - email_profile_match
        - phone_type
        - phone_ported
        - phone_activity
        - profile_account_age_risk
        - premium_phone_name_match
        - premium_phone_connectivity
        - premium_phone_profile_match
      description: The name of a signal.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An account-scoped API key, `tf_live_…`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.