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

# Poll results

> Poll a batch's status and results by request_id.

Returns the same payload the webhook delivers, with whatever items have scored so far. `status`
stays `pending` until every item is terminal, then flips to `complete`. You can rely on polling
alone. See [The verdict model](https://docs.truthfactor.ai/essentials/verdicts.md) for how to read each item.


## OpenAPI

````yaml GET /v1/signals/{request_id}
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/{request_id}:
    get:
      summary: Poll batch status and results
      description: >-
        Returns the same payload the webhook delivers, with whatever items have
        scored. A caller may rely on polling alone.
      operationId: getSignals
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
          description: The request_id returned by POST /v1/signals.
      responses:
        '200':
          description: The batch status and results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignalsResponse'
              examples:
                poll_clear:
                  summary: Poll — a consistent identity
                  value:
                    request_id: rq_7a1d
                    requested_datetime: '2026-09-26T14:03:11Z'
                    completed_datetime: '2026-09-26T14:03:41Z'
                    status: complete
                    data:
                      - correlation_id: signup-3391
                        status: scored
                        score: 94
                        band: clear
                        reason: phone_profile_match
                        input:
                          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:
                            trusted: true
                            confidence: 90
                          phone_connectivity:
                            trusted: true
                            confidence: 98
                          phone_profile_match:
                            trusted: true
                            confidence: 76
                          email_name_match:
                            trusted: null
                            confidence: null
                webhook_name_mismatch:
                  summary: Webhook — a name mismatch
                  value:
                    request_id: rq_b42e
                    requested_datetime: '2026-09-26T18:20:05Z'
                    completed_datetime: '2026-09-26T18:20:38Z'
                    status: complete
                    data:
                      - correlation_id: order-5567
                        status: scored
                        score: 36
                        band: high_risk
                        reason: name_mismatch
                        input:
                          phone: '+447911123457'
                          first_name: Jordan
                          last_name: Reyes
                          profile_url: linkedin.com/in/jordan-reyes
                          correlation_id: order-5567
                        signals:
                          phone_name_match:
                            trusted: false
                            confidence: 100
                          phone_connectivity:
                            trusted: true
                            confidence: 94
                          phone_profile_match:
                            trusted: false
                            confidence: 78
        '401':
          description: invalid api key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: no batch with that request_id for this account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - bearerAuth: []
components:
  schemas:
    SignalsResponse:
      type: object
      properties:
        request_id:
          type: string
          minLength: 1
        requested_datetime:
          type: string
          format: date-time
        completed_datetime:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
        status:
          type: string
          enum:
            - pending
            - complete
            - partial
            - failed
        data:
          type: array
          items:
            $ref: '#/components/schemas/ResultItem'
      required:
        - request_id
        - requested_datetime
        - completed_datetime
        - status
        - data
      additionalProperties: false
      description: The GET /v1/signals/{request_id} and webhook body.
    Error:
      type: object
      required:
        - error
      description: An error body. error is a machine-readable code.
      properties:
        error:
          type: string
    ResultItem:
      oneOf:
        - type: object
          properties:
            correlation_id:
              type: string
            input:
              $ref: '#/components/schemas/SignalInput'
            status:
              type: string
              const: scored
            score:
              type: integer
              minimum: 0
              maximum: 100
            band:
              type: string
              enum:
                - clear
                - caution
                - high_risk
            reason:
              type: string
              minLength: 1
              description: >-
                The dominant factor: an override code, else the lowest-scoring
                signal.
            signals:
              $ref: '#/components/schemas/SignalVerdictMap'
          required:
            - input
            - status
            - score
            - band
            - reason
            - signals
          additionalProperties: false
          description: A scored item.
        - type: object
          properties:
            correlation_id:
              type: string
            input:
              $ref: '#/components/schemas/SignalInput'
            status:
              type: string
              const: rejected
            reason:
              type: string
              minLength: 1
              description: >-
                Why the item was rejected: no_runnable_signal when the input
                supports none of the selected signals, invalid_phone when the
                phone will not parse, and so on. Not a score factor — nothing
                was scored.
            signals:
              $ref: '#/components/schemas/SignalVerdictMap'
          required:
            - input
            - status
            - reason
          additionalProperties: false
          description: A rejected item — no_runnable_signal, invalid_phone, ….
        - type: object
          properties:
            correlation_id:
              type: string
            input:
              $ref: '#/components/schemas/SignalInput'
            status:
              type: string
              const: error
            reason:
              type: string
              minLength: 1
              description: >-
                Why the item failed, when that is known. Optional — an errored
                item is promised no field beyond its status.
            signals:
              $ref: '#/components/schemas/SignalVerdictMap'
          required:
            - input
            - status
          additionalProperties: false
          description: An errored item.
        - type: object
          properties:
            correlation_id:
              type: string
            input:
              $ref: '#/components/schemas/SignalInput'
            status:
              type: string
              const: unresolved
            signals:
              $ref: '#/components/schemas/SignalVerdictMap'
          required:
            - input
            - status
            - signals
          additionalProperties: false
          description: >-
            An unresolved item (TFA-130): it ran but every signal returned
            trusted: null, so there is no score. It returns its full signals
            map, each verdict null with an unresolved_reason, and no
            score/band/reason.
      description: >-
        One result item, discriminated on status: scored carries
        score/band/reason/signals; rejected carries reason; unresolved carries
        its signals map (every verdict null, each with an unresolved_reason) and
        no score/band/reason; error may carry neither.
    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.
    SignalVerdictMap:
      type: object
      properties:
        phone_name_match:
          $ref: '#/components/schemas/SignalVerdict'
        phone_connectivity:
          $ref: '#/components/schemas/SignalVerdict'
        phone_profile_match:
          $ref: '#/components/schemas/SignalVerdict'
        email_name_match:
          $ref: '#/components/schemas/SignalVerdict'
        email_profile_match:
          $ref: '#/components/schemas/SignalVerdict'
        phone_type:
          $ref: '#/components/schemas/SignalVerdict'
        phone_ported:
          $ref: '#/components/schemas/SignalVerdict'
        phone_activity:
          $ref: '#/components/schemas/SignalVerdict'
        profile_account_age_risk:
          $ref: '#/components/schemas/SignalVerdict'
        premium_phone_connectivity:
          $ref: '#/components/schemas/SignalVerdict'
        premium_phone_name_match:
          $ref: '#/components/schemas/SignalVerdict'
        premium_phone_profile_match:
          $ref: '#/components/schemas/SignalVerdict'
      additionalProperties: false
      description: >-
        Only the selected signals (or all applicable when omitted). A signal
        that could not run is {trusted: null, confidence: null}, never {trusted:
        false}.
    SignalVerdict:
      oneOf:
        - type: object
          properties:
            trusted:
              type: boolean
            confidence:
              type: integer
              minimum: 0
              maximum: 100
          required:
            - trusted
            - confidence
          additionalProperties: false
          description: A resolved verdict.
        - type: object
          properties:
            trusted:
              type: 'null'
            confidence:
              type: 'null'
            unresolved_reason:
              type: string
              enum:
                - inconclusive
                - source_unavailable
                - no_coverage
          required:
            - trusted
            - confidence
          additionalProperties: false
          description: >-
            Unresolved — no source produced a verdict. On an unresolved item it
            carries an unresolved_reason (inconclusive, source_unavailable or
            no_coverage) explaining the null (TFA-130).
      description: >-
        One signal's verdict. Either a resolved verdict (a boolean trusted with
        an integer confidence) or the unresolved {trusted: null, confidence:
        null}. confidence is null exactly when trusted is null — a mixed
        {trusted: null, confidence: 100} or {trusted: true, confidence: null} is
        not a valid verdict, which is why this is a oneOf rather than two
        independently-nullable fields.
  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.