KYC

Know Your Customer (KYC) verifies that an individual account holder is who they say they are. It's a regulatory and fraud-prevention primitive: collect a legal identity, match it against authoritative sources, confirm with a document and a selfie, and produce a binary outcome you can act on. Frame models KYC as a capability on an account - you request it, Frame runs the checks, and any downstream payment capability that depends on a verified identity activates automatically.

KYC is for individual accounts only. Businesses follow KYB (Know Your Business), which Frame runs against the legal entity while onboarding each owner or controller as a separate individual account in parallel.

When you need KYC

Request the kyc capability when your flow requires a verified identity. The two common cases:

  • Paying funds out to an individual. card_receive and bank_account_receive both depend on completed KYC. Frame won't push money to an unverified person.
  • Running age-restricted flows. age_verification and other compliance capabilities build on top of KYC.

If you request card_receive, bank_account_receive, or creator_shield on an account, Frame includes kyc automatically - you don't have to ask for it separately. Request kyc directly when you need identity verification independent of any payment capability (e.g., for compliance threshold enforcement on your platform).

Buyers paying you through hosted checkout don't need KYC. The card_send capability activates on a payment method alone; verifying every buyer's identity would be excessive friction for the case where you only need to charge their card.

Required information

To complete KYC for an individual account, Frame needs:

FieldWhere it lives on the Account
Legal first nameprofile.individual.name.first_name
Legal last nameprofile.individual.name.last_name
Phone numberprofile.individual.phone
Date of birthprofile.individual.birthdate
Residential addressprofile.individual.address
Last 4 of SSNCollected during the onboarding session and stored encrypted; only ssn_last_four is returned via the API

Anything you have at account-creation time, prefill - Frame won't re-collect it during the hosted flow. Fields you don't have are surfaced in the capability's currently_due array; the onboarding session then collects them through Frame-hosted UI.

The flow

KYC is gated by a hosted onboarding session - Frame owns the UX, you own the redirect points. The shape:

  1. Create the account with kyc in the capabilities array and any identity fields you already have.
  2. Create an onboarding session for the account. Frame returns a URL.
  3. Redirect the account holder to that URL. The hosted flow walks them through:
    • Consenting to the identity verification process.
    • Confirming any additional fields Frame couldn't prefill from your input (last 4 of SSN, address corrections, etc.).
  4. Handle the return. Frame redirects the account holder to your return_url when they finish. The redirect fires when the user clicks through - not when the verification has resolved. Document verification runs asynchronously and can take seconds to minutes.
  5. Read the outcome. Subscribe to the account.identity_verification.* webhooks - approved, failed, review_opened - or retrieve the run from /v1/accounts/:account_id/identity_verifications. capability.activated still tells you the capability turned on, but the verification resource is where the outcome and its reason live. Don't act on the redirect alone.

KYC resolves from data alone. Frame matches the identity fields against authoritative sources and returns a decision - no document, no selfie. Document capture is a separate step that runs only when the data checks cannot reach a decision.

Walk-through with code: Run KYC.

When KYC asks for a document

Some identities do not resolve from data. A thin credit file, a recent address change, or a name that authoritative sources hold differently all produce an inconclusive result rather than a pass or a fail. Frame does not decline these account holders. It asks them for a government ID instead.

When that happens, Frame requests the idv capability on the account and fires a capability.requested webhook. The kyc capability stays pending - it has not failed, it is waiting on the document.

An onboarding session picks the step up on its own. A session derives its steps from the account's outstanding requirements on every request, so an account holder still in the flow when the step-up fires sees an ID verification step appear. You don't create it, name it, or configure it.

That covers the common case, because KYC usually resolves while the holder is still in the session. Handle the other case explicitly:

  1. Handle the capability.requested webhook. The payload names the account and the idv capability. Treat it as "this account holder has more to do," not as an error.
  2. Check whether their session is still live. Sessions carry an expiry. If the holder is still in an unexpired session, they are already looking at the new step and you need to do nothing.
  3. Create a new onboarding session only if the old one has expired - or if the holder left before the decision landed. Redirect them back to the new URL.
  4. Confirm activation the same way you do for KYC: wait for capability.activated on kyc. A passing document resolves the original KYC check, and the downstream payment capabilities activate behind it.

KYC resolves asynchronously, so the decision can land after the account holder has closed the tab or after the session has expired. That is what the webhook is for - it's your signal to bring them back, whether or not they were still on screen.

Budget for this branch before you go live. An integration that treats every non-active KYC as a failure strands the account holders who only needed to show an ID.

Outcomes

A KYC run produces an identity verification - its own resource, retrievable at /v1/accounts/:account_id/identity_verifications and pushed to you as the account.identity_verification.* webhook family. That resource is the outcome. The kyc capability tells you what the account can do; the verification tells you what happened and why.

A failed run is not a denial

This is the distinction to build on, and the one most integrations get wrong.

status: "failed" means the provider returned a non-passing result. It does not mean this person cannot be verified. Most failed runs are recoverable - the data was wrong, a document is needed, a human is looking, the provider hiccuped.

A denial is failure_type: "verification_rejected", equivalently category: "terminal". That is the only outcome that says the answer cannot change for this person, however they retry. Decline service on that. Do not decline on status: "failed" alone - you will turn away account holders who needed to correct a typo or show an ID.

status == "failed"            → something did not pass. Read failure_type.
category == "terminal"        → denied. This does not reopen.
retriable == true             → try again; corrected data or a retry can succeed.
category == "step_up"         → not a denial. Frame wants a document.
category == "review"          → not a conclusion. Wait for the verdict webhook.

Run statuses

statusMeansWebhook
pendingThe provider is still working.none
approvedIdentity verified. kyc moves to active.account.identity_verification.approved
failedThe provider returned a non-passing result. Read failure_type.account.identity_verification.failed
erroredThe check did not complete. Nothing was concluded about the account holder.none - see below
incompleteThe provider could not finish with what it had.account.identity_verification.failed

account.identity_verification.review_opened fires separately when a run routes to a human. It is not a conclusion - the run has not passed or failed yet, and a verdict webhook follows when the review closes.

An errored run emits no verdict webhook at all. It reached no judgement, so Frame does not push one. You will see it by retrieving the run, not by listening. If your integration is webhook-only, an errored run looks like silence - poll the verification when a run has been outstanding longer than you expect.

Verdict webhooks are conclusion-gated: .approved and .failed fire only once the run is terminal and has no open review. A failure that routes to review emits .review_opened first and its verdict later, so you never receive a verdict you have to walk back.

A failed verification does not disable the kyc capability. disabled means a deliberate switch-off - by you, or by Frame acting on a policy decision - and nothing else. An integration that watches capability.disabled to learn a check failed will miss every failure. Watch account.identity_verification.failed instead.

The capability's own states still mean what they always did - see the capability lifecycle - and for KYC the common path is pendingactive. A pending capability covers several situations; see Reading a pending KYC.

errors[] on the capability

The capability's errors[] array carries the same conclusion, derived at read time. For a kyc capability whose verification obligation is still unmet and whose latest concluded run failed, it holds exactly one entry:

{
  "id": "...",
  "object": "capability_error",
  "code": "identity_mismatch",
  "message": "The information provided does not match the identity record located."
}

code is the failure_type; message is Frame's wording for it. The array is empty when the obligation is satisfied, when the latest run passed, or when a run is still open - an empty errors[] is not "no failure recorded", it is "nothing unmet right now".

It is a convenience for rendering, not a substitute for the verification resource: it carries no category and no retriable. Branch on the verification; render from errors[].

Outcomes are not predictive - a failed verification for one account holder doesn't say anything about a different one, even with similar fields. KYC is identity verification, not risk scoring.

Reading a pending KYC

A pending KYC capability means one of three things, and they call for three different responses:

What's happeningHow to tellWhat to do
The account holder still owes informationcurrently_due is non-emptyPut them through an onboarding session to collect the listed fields
A check is in flightcurrently_due is empty, no idv capability on the accountNothing. Wait for account.identity_verification.approved or .failed
The identity didn't resolve and Frame wants a documentAn idv capability has appeared on the accountGet the holder back into a session - see When KYC asks for a document

A fourth case used to be invisible here: a run routed to human review looked exactly like a check still in flight. It no longer does. A review fires account.identity_verification.review_opened and the run carries failure_type: "review_pending" with category: "review", so you can tell the two apart from the API. Nothing on your side unblocks a review - a verdict webhook follows when it closes.

How Frame triages a failed check

Not every failed identity check means the same thing, and Frame does not collapse them into one outcome. Every run carries a failure_type - Frame's own provider-neutral vocabulary - plus the category that says how to remediate it and a retriable flag derived from that category.

Failure types

failure_typecategoryretriableWhat it meansYour move
identity_mismatchretriable_with_new_datatrueThe identity fields exist but don't match the record Frame located.Correct the data and run the verification again.
identity_not_foundstep_upfalseNo sufficient record behind the supplied identity. More data won't fix it; a document can.Get the account holder back into a session - see When KYC asks for a document.
verification_rejectedterminalfalseThe outcome cannot change for this person, however you retry.Decline service per your policy. This does not reopen.
review_pendingreviewfalseNot a conclusion - a human is looking at it.Wait. A verdict webhook follows when the review closes.
provider_errortransienttrueThe check did not complete. Nothing was concluded about the account holder. Carried by an errored run, so no webhook fires - you see it by retrieving the run.Retry.
signals_unavailabletransienttrueThe run failed before its risk signals arrived - a fetch failure, not a review request.Retry.
unclassifiedunclassifiedfalseA result no classifier recognises. It concludes nothing and is deliberately visible rather than silently bucketed.Contact Frame support with the verification ID.

Only verification_rejected is a denial. Every other row is a failed run that is not a denial - see A failed run is not a denial. Reading status: "failed" as "decline this person" is the most expensive mistake available here.

Branch on category, not on the individual type. The categories are the remediation classes and they are stable; failure types can be added as classifiers learn new results. retriable is derived - it is true for retriable_with_new_data and transient, and false for everything else - so a retry loop can read that one field and be correct.

A terminal outcome wins over a step-up. If a run produces both "couldn't locate this identity" and a terminal signal, the result is verification_rejected - a document cannot rescue an identity that failed terminally.

unclassified is the fall-through, not a fixed list. Treat an unfamiliar result as needing a human rather than assuming it is benign.

What the API does not carry

Frame's verification providers return their own reason codes - vendor-specific strings naming what didn't reconcile. Those never cross the API boundary. The wire speaks failure_type and category only, so your integration is not coupled to a provider Frame may change.

The specific signals are visible in the frameOS dashboard, which is the surface for them. See Reading a verification in the dashboard.

Whatever the underlying signal, fraud, sanctions and synthetic-identity findings are not for your end user. Telling an account holder their identity matched a watchlist is both customer-hostile and a tip-off to a real bad actor. Surface a generic "we couldn't verify your identity" and route them to your support path.

Reading a verification in the dashboard

Reason codes, risk signals, and verification history are not on the API - they're on the account holder's identity detail page in frameOS. When an integrator asks "why did this specific person fail," that page is the answer. It has five panels:

  • Status - the account holder's overall outcome: Verified, Step up, Failed, or Pending. This is a lifetime view: an account holder who ever passed a check reads Verified even if a later check didn't. Below it, Outcome names the triage path of the most recent check - Hard fail, Step up, or Manual review - and appears only while that latest check is non-passing. When the two disagree, Status is telling you about history and Outcome is telling you about now.
  • Profile - the identity as submitted: name, email, phone, birthdate, computed age, last 4 of SSN, address. Read this first on a failed check. A mistyped birth year or a stale address explains more failures than the reason codes do.
  • Risk signals - the findings from the most recent check, each with a plain-language description and a severity of High, Medium, Low, or Info, sorted with High first. Info-severity signals are the checks that passed, so a healthy verification is mostly Info rows.
  • Verifications - one row per check ever run against this person, KYC and government ID alike, newest first, with status and abbreviated reasons.
  • Timeline - the same runs as a chronological feed, grouped by day: when each check ran and what it returned, when a step-up was requested, when a check was flagged for review, and each stage of a document verification (started, completed, approved, declined, expired).

Risk signals and Outcome describe the latest check only. To understand an account holder with several attempts - a common shape, since a step-up produces at least two - read the Timeline for the sequence and the Verifications table for per-run detail.

Frame does not surface risk signals for sandbox verifications, so a sandbox account holder shows reason codes without the accompanying descriptions.

Re-verification

KYC isn't a one-shot. If an account holder's information changes (legal name change, new address) or your platform's compliance policy mandates periodic refresh, update the profile fields. Frame re-evaluates the kyc capability against the new data.

Re-evaluation is not the same as re-running the identity check. Correcting a typo does not automatically send the identity back to the verification provider for a fresh decision - a capability that failed can activate later, but the new decision arrives through the verification provider or through a passing document, not from the profile edit alone. If an account holder failed on data you've since corrected and the capability hasn't moved, contact Frame support rather than editing the profile repeatedly.

The previous KYC result isn't "lost" - Frame tracks every verification run on the account, visible in the dashboard's verification timeline. The capability surface reflects the current state.

Scope - what KYC checks

KYC verifies the legal identity claim:

  • The name, SSN last 4, address, and date of birth reconcile against authoritative sources.
  • The identity belongs to a real person, and that person is not deceased.
  • The identity shows no signal of synthetic or stolen construction.

When those checks resolve the identity, KYC is done. When they cannot, the idv capability adds a document layer on top:

  • The document is authentic (not forged, not visibly tampered).
  • The selfie is a live person, not a static image.
  • The selfie face matches the document photo within Frame's matching threshold.

KYC does not check:

  • Whether the person is on a sanctions list (that's separate compliance work, run alongside).
  • Whether the address is currently valid (that's address_verification).
  • Whether the phone is reachable (that's phone_verification).
  • Whether the person is of legal age for a regulated activity (that's age_verification, which builds on KYC).

Treat KYC as the foundation: it verifies identity, and the other compliance capabilities verify specific attributes of that identity.

Platform scope

Every KYC integration needs a server-side onboarding session you create from your backend. What renders the account holder's side of it depends on your platform.

On the web, that's frame-js. The hosted verification UI loads on os.framepayments.com and walks the account holder through document capture in the browser or mobile browser.

For native apps, Frame ships iOS, Android, and React Native SDKs - see Mobile SDKs. The onboarding session is created the same way; the SDK handles presenting the flow on-device. Check each SDK's own documentation for its current onboarding surface.

Gotchas

Symptom: the account holder completed the session and was redirected back, but the kyc capability is still pending. Why: Frame's redirect fires when the user clicks through, not when document analysis resolves. Verification can take seconds to minutes. Fix: listen for the account.identity_verification.* webhooks; treat the redirect as "they finished entering data," not "verification passed."

Symptom: you requested kyc plus card_receive on a new account and only kyc shows up pending in the response. Why: Frame stages dependent capabilities - card_receive won't be evaluated until kyc activates. The capability still exists on the account, just not surfaced until its prerequisites resolve. Fix: once kyc transitions to active, retrieve the account again and you'll see card_receive listed and being evaluated.

Symptom: the kyc capability sits pending indefinitely, and the account has grown an idv capability you never requested. Why: the identity didn't resolve from data, so Frame requested a government ID. The account holder has an outstanding step and nothing is going to move until they complete it. Fix: handle the capability.requested webhook, create a fresh onboarding session, and redirect the account holder back into it. See When KYC asks for a document.

Symptom: you sent the account holder back to their original onboarding session URL for the step-up and the link no longer works. Why: onboarding sessions expire, and KYC can resolve after the holder has left. The step itself is fine - a live session would have shown it - but the session is gone. Fix: create a new onboarding session for the account and redirect them to that URL.

Symptom: you're watching capability.disabled to learn that KYC failed, and it never fires. Why: a failed verification doesn't disable the capability. disabled means a deliberate switch-off. Fix: subscribe to account.identity_verification.failed and read failure_type off the payload - the verification resource is the outcome surface, and it carries the reason the capability event never did.

Symptom: the kyc capability has been pending for hours, currently_due is empty, and there's no idv capability. Why: the check failed on a signal Frame routes to human review. Review is not a distinct capability status, so it presents exactly like a check still resolving. Fix: contact Frame support with the account ID. Re-requesting the capability or creating another onboarding session does nothing.

Symptom: the dashboard shows an account holder as Verified but their kyc capability is disabled. Why: the dashboard Status is a lifetime view - one passing check ever makes it Verified - while the capability reflects the current decision. Fix: read the Outcome row underneath Status, which describes the latest check, and the verification timeline for the sequence. The capability is the authoritative answer for whether you can pay this person.

Symptom: a KYC verification passes for one account holder and fails for a similar-looking one. Why: document quality, lighting, and the matching threshold are all stochastic. KYC is identity-level, not risk-level - small data differences yield different outcomes. Fix: surface a remediation path (re-submit with clearer photos, manual review) rather than treating the failure as final.

Reference

For the full API surface, see POST/v1/accounts and POST/v1/onboarding_sessions.

Frame Assistant

Ask anything about Frame's APIs and products