Attesto Trust API (v1)

Screen job applications for signs of fraud.

The Trust API tells you whether a job application shows signs of fraud. You send the candidate's application details and a link to their resume. Attesto checks them against independent evidence, such as the candidate's online footprint, phone line and resume. It returns an overall verdict, a verdict for each signal it checked, and a plain-language explanation of each.

Verdicts are advisory. Use them to decide which applications a person should look at first, or more closely. Never reject a candidate automatically because of a verdict.

Who it's for

The Trust API is built for software that handles job applications for many employers and wants fraud screening inside its own product:

  • applicant tracking systems and HR platforms;
  • job boards and marketplaces;
  • background check providers, as an early check before their usual process;
  • AI hiring agents, which can read the verdict and reasoning but should hand any rejection to a person.

If you are a single employer and Attesto already supports your applicant tracking system, you don't need the API. Attesto's ATS integration works without code.

How it fits your workflow

  1. A candidate applies. Your system starts a check with POST /v1/fraud-checks, sending the application details and a link to the resume. Attesto replies straight away with the check's id.
  2. The check runs in the background. Most checks finish within a few minutes, and every check finishes within 60 minutes.
  3. You get the result. Attesto sends it to your webhook URL. You can also fetch it at any time with GET /v1/fraud-checks/{id}.
  4. A person decides. Show the recruiter the verdict and its reasoning, and act on it like this:
  • clear: the evidence supports that the application is genuine. Continue as normal.
  • suspicious: the evidence shows red flags. Have a person review the application, starting with each signal's reasoning. Don't reject it automatically.
  • unknown: there isn't enough evidence to say either way. Treat it as no finding, not as a negative one.
  • fraudulent: Attesto staff previously confirmed fraud involving this candidate's email address. Have a person confirm before taking any action, because someone else may have used that email address.
  • verified_clear: reserved for a later version, and not returned in v1. Treat it like clear.

Quickstart

This runs one check in the sandbox. The sandbox returns made-up results, calls no outside services and is free.

1. Get a sandbox key. Ask Attesto for one. Sandbox keys start with att_test_.

2. Start a check.

curl https://api.sandbox.attesto.com/v1/fraud-checks \
  -H "Authorization: Bearer $ATTESTO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "candidate_name": "John Doe",
    "email": "john.doe@example.com",
    "resume_url": "https://example.com/resumes/candidate-123.pdf",
    "application_id": "client-app-123"
  }'

The reply is 202 Accepted, with the check's id:

{
  "id": "fdc_9a4c1e7f3b2d8065",
  "status": "processing",
  "application_id": "client-app-123",
  "created_at": "2026-04-27T14:22:58Z"
}

3. Get the result. Set up a webhook (see Webhooks), or poll:

curl https://api.sandbox.attesto.com/v1/fraud-checks/fdc_9a4c1e7f3b2d8065 \
  -H "Authorization: Bearer $ATTESTO_API_KEY"

Once status is completed, result holds the verdict. A real result lists all five signals; this one is shortened to two:

{
  "id": "fdc_9a4c1e7f3b2d8065",
  "status": "completed",
  "application_id": "client-app-123",
  "created_at": "2026-04-27T14:22:58Z",
  "result": {
    "verdict": "clear",
    "reasoning": "The LinkedIn profile matches the resume, and the phone number is registered to the candidate.",
    "signals": [
      {
        "name": "online_footprint",
        "verdict": "clear",
        "sub_verdict": "linkedin-match",
        "reasoning": "LinkedIn profile found, and it matches the resume.",
        "sources": ["https://www.linkedin.com/in/johndoe"],
        "contributes_to_verdict": true
      },
      {
        "name": "phone",
        "verdict": "clear",
        "sub_verdict": "phone-name-match",
        "reasoning": "The number is a mobile line registered to the candidate's name.",
        "contributes_to_verdict": true
      }
    ],
    "metadata": {
      "tier": "tier_2",
      "version": "1.0.0",
      "processing_time_ms": 4210,
      "completed_at": "2026-04-27T14:23:03Z",
      "source": "automated"
    }
  }
}

The endpoint reference has more examples, including a request with every field and a result for each verdict that v1 returns.

Sandbox test emails

In the sandbox, the candidate's email decides the result:

  • john.doe@example.com or clear@example.com: completed with clear.
  • suspicious@example.com: completed with suspicious, with signals showing all three signal verdicts.
  • unknown@example.com: completed with unknown, from evenly split evidence.
  • gap@example.com: completed with unknown, because one signal couldn't be completed. error is set on online_footprint.
  • fraudulent@example.com: completed with fraudulent, from an earlier staff review.
  • verified-clear@example.com: completed with verified_clear, so you can test handling it. Production doesn't return it in v1.
  • failed@example.com: failed with upstream_error.
  • slow@example.com: stays processing for 2 minutes, to test polling and webhooks.
  • rate-limited@example.com: 429 with Retry-After.
  • Any other email: 400, so a sandbox key left in production code fails loudly.

Apart from slow@example.com, sandbox checks finish in about 5 seconds. Webhooks are signed and delivered as in production. The sandbox checks that resume_url is a valid link but doesn't download it. Don't send real candidate data to the sandbox.

Core concepts

Checks

A check is one fraud screening of one application. Its id starts with fdc_, and its status is one of:

  • processing: still running.
  • completed: finished, and result holds the verdict.
  • failed: no verdict could be produced, and error says why.

Checks are asynchronous: starting one never waits for the result. An application can have several checks.

Signals

A check looks at up to five signals. Each one returns its own verdict (clear, suspicious or unknown), a sub_verdict naming its main finding (listed under Sub-verdict reference), and a reasoning.

  • online_footprint looks at the candidate's LinkedIn profile and web search results. It needs candidate_name, and a linkedin_url makes it more reliable.
  • phone looks at who the phone line is registered to, the type of line, and the email address. It needs phone, or a phone number on the resume.
  • github looks at the candidate's GitHub profile. It needs github_url, or a GitHub link on the resume.
  • resume_content looks at the resume itself: employers, schools, skills, locations and dates. It needs resume_url.
  • application_anomalies checks whether the application's name, email and location agree with the resume. It needs resume_url.

Which signals run depends on your plan. Every signal is listed in signals, whether or not it ran. A signal that didn't run, or had nothing to check, returns unknown and says why in its reasoning.

contributes_to_verdict shows which signals counted toward the overall verdict. A signal that couldn't be completed, for example because a source was unavailable, has error set. A check with such a gap never returns clear.

Earlier staff reviews

Attesto staff sometimes confirm, after a manual review, that an application was fraudulent. When a new check uses an email address from such a case, it returns fraudulent straight away. metadata.source is prior_manual_review, metadata.reviewed_at gives the date of the review, and signals is empty.

Authentication

Send your API key as a bearer token on every request:

Authorization: Bearer att_live_...
  • Sandbox: https://api.sandbox.attesto.com, with keys that start with att_test_.
  • Production: https://api.attesto.com, with keys that start with att_live_.

Attesto issues your keys and shows each one only once. Keep keys on your servers, never in a browser or mobile app. A key used in the wrong environment returns 401. When you rotate keys, the old and new keys work side by side until you ask Attesto to revoke the old one, which takes effect within 60 seconds.

Webhooks

Attesto sends one event to your webhook URL when a check finishes:

  • fraud_check.completed: the check has a verdict.
  • fraud_check.failed: the check couldn't produce one.

The event's data is the same check object that GET /v1/fraud-checks/{id} returns. The payloads are listed under Webhook events in the reference. Webhooks follow the Standard Webhooks specification, so you can verify them with its open-source libraries.

Set up your endpoint

Send Attesto a public HTTPS URL that accepts POST requests, one for each environment, such as https://example.com/attesto-webhooks. Attesto registers it and gives you a signing secret that starts with whsec_. Redirects aren't followed, and URLs on private networks are rejected.

Handle an event

  1. Verify the signature with a Standard Webhooks library, using your signing secret, the raw request body, and the webhook-id, webhook-timestamp and webhook-signature headers. Reject any request that fails.
  2. Respond with a 2xx within 10 seconds, then process the event in the background.
  3. Expect duplicates and any order. Skip any webhook-id you have already processed, and ignore event types you don't handle.
# pip install standardwebhooks
from standardwebhooks.webhooks import Webhook

webhook = Webhook(ATTESTO_WEBHOOK_SECRET)  # your whsec_ secret

def handle(raw_body: bytes, headers: dict) -> None:
    event = webhook.verify(raw_body, headers)  # raises if the signature is invalid
    if event["type"] == "fraud_check.completed":
        save_result(event["data"])
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.ATTESTO_WEBHOOK_SECRET); // your whsec_ secret

function handle(rawBody, headers) {
  const event = webhook.verify(rawBody, headers); // throws if the signature is invalid
  if (event.type === "fraud_check.completed") saveResult(event.data);
}

Retries

If your endpoint doesn't return a 2xx within 10 seconds, Attesto retries the delivery. That includes any 4xx. There are up to 8 attempts over about 21 hours: straight away, then after 30 seconds, 2 minutes, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. After the last attempt, Attesto stops and tells your technical contact. You can still fetch the check with GET.

webhook-id stays the same on every attempt, while webhook-timestamp changes. The event's timestamp is when the check finished, so it doesn't change either.

Rotate your signing secret

Ask Attesto for a new secret. For 24 hours, webhook-signature carries signatures from both the old and the new secret, so you can switch without missing events. Standard Webhooks libraries accept a header with several signatures.

Errors

Every error from the API has the same shape:

{
  "error": {
    "code": "invalid_request",
    "message": "candidate_name is required",
    "details": { "field": "candidate_name" }
  }
}
  • 400 invalid_request: a field is missing or invalid, and details.field names it. This includes a body over 1 MB, a resume_url that isn't public HTTPS, and a reused Idempotency-Key with a different body. Fix the request, and don't retry it unchanged.
  • 401 unauthorized: the API key is missing, revoked, unknown, or for the other environment. Check your key.
  • 403 forbidden: the key is valid, but your account can't use this feature. Contact Attesto.
  • 404 not_found: your account has no check with this id, including checks removed after the retention period.
  • 429 rate_limited: you reached a limit, named in details.limit. Retry after the number of seconds in Retry-After.
  • 500 internal_error: something went wrong at Attesto. Retry, and for POST use the same Idempotency-Key.

Responses from Attesto's network edge, such as 413 or 502 to 504, may not be JSON. Treat them like 500.

Every response has an X-Request-ID header. Include it when you contact Attesto about a request.

When a check fails

A check that can't produce a verdict is not an HTTP error. It ends with status: failed, and its error says why:

  • upstream_error: an outside service the check depends on failed. details.provider names the signal affected.
  • invalid_request: the resume couldn't be downloaded or read. details.field is resume_url.
  • internal_error: something went wrong at Attesto.

To try again, start a new check with a new Idempotency-Key.

Limits

Rate limits

Limits are set for each account. When you reach one, the API returns 429 with a Retry-After header, and details.limit says which limit applies:

  • rate: too many requests in a short time.
  • backlog: too many of your checks are waiting to run.
  • spend: your account has reached its usage cap.

Ask Attesto for your account's limits.

Size limits

WhatLimit
Request body1 MB
ats_questions100 items, each answer up to 5,000 characters
Resume file20 MB

Idempotency

Send an Idempotency-Key header, a UUID you generate, when you start a check. If the request fails on the network, retry with the same key, and you won't start a second check.

  • A repeated request with the same key returns the original check, with 202 and its current state, instead of starting a new one.
  • Keys belong to your account and last as long as the check does.
  • Reusing a key with a different body returns 400. The comparison ignores resume_url, because signed links change between retries.

Use a new key for each new check, including when you retry a check that failed.

Data retention

Attesto keeps each check for 90 days, unless your agreement sets a different period. After that, GET returns 404. To have a candidate's data deleted sooner, contact Attesto.

Versioning

The major version is in the path, as in /v1/. Within a version, Attesto may add fields, signals, sub_verdict values and event types without notice, so your code should ignore any it doesn't recognize. Breaking changes come with a new major version and 12 months' notice.

metadata.version identifies the version of Attesto's checks that produced a verdict. It changes when Attesto changes how checks work. Store it with the verdict for your records.

Sub-verdict reference

sub_verdict names the main finding within a signal. Use it to explain or group results, and use verdict to decide what to do: the same sub_verdict can come with different verdicts, depending on how serious the finding is. It can be an empty string. New values may be added, so treat one you don't recognize as informational.

online_footprint

  • linkedin-match: a LinkedIn profile was found, and it matches the resume.
  • google-match: web search results confirm the candidate's identity.
  • experience-mismatch: the profile or search results show a different profession, or conflict with the resume.
  • low-follower-count: the LinkedIn profile has very few connections, and nothing else supports it.
  • no-matching-search-results: web search results don't back up the candidate's details or LinkedIn profile.
  • not-enough-info: too little public information to confirm or question the candidate's identity.
  • insufficient-signals: some information was found, but it is mixed or too weak to decide.

phone

  • phone-name-match: the line is registered to the candidate.
  • phone-family-match: the line is registered to someone with the candidate's family name.
  • phone-name-mismatch: the line is registered to someone else, along with other warning signs.
  • voip-phone-number: the number is an internet (VoIP) line, along with other warning signs.
  • phone-throwaway-email: the email address uses a disposable email service, along with other warning signs.
  • phone-insufficient-data: not enough information to confirm or question the phone number.

github

  • github-match: the GitHub profile's name matches the resume.
  • github-mismatch: a GitHub profile was found, but its name is clearly different from the resume's.
  • insufficient-github-data: no GitHub profile was given or found, or it had too little information.

resume_content

  • resume-content-clear: no red flags were found in the resume.
  • experience-company-mismatch: a role belongs to a clearly different industry from the company it's listed under.
  • skills-experience-mismatch: the listed skills don't fit the work history, as if they came from a different person.
  • missing-employer-or-institution: most jobs or schools are listed without an organization name.
  • invalid-organization: an employer or school doesn't appear to be a real organization.
  • location-mismatch: a job is listed in a place where that company isn't known to operate.
  • multiple-red-flags: several serious red flags were found.

application_anomalies

  • no-anomalies: the application agrees with the resume.
  • email-mismatch: the application's email address doesn't appear on the resume.
  • name-mismatch: the application's name differs from the resume's, beyond nicknames, middle names, name order or typos.
  • location-mismatch: the application's location is in a different region or country from the resume's.
  • linkedin-not-provided: the application form asked for a LinkedIn profile, and the candidate left it blank. Only when this check is on for your account.
  • multiple-anomalies: two or more of the findings above.
Servers
Sandbox, for att_test_ keys
https://api.sandbox.attesto.com
Production, for att_live_ keys
https://api.attesto.com