Onboarding & compliance lifecycle
Understand submissions, information requests, endorsements, and account KYC statuses, then use webhooks to show users the tasks they need to complete.
After an account is created, Due sometimes needs more information from your user: to finish KYC, to open a new rail, to let a transfer through, or to answer a compliance question. This guide explains the objects involved one by one, lists every status, and then shows how to build on webhooks so you always know whether your user has something to do.
The building blocks
Submission: the form
A submission is a form your user fills in. It lists what Due needs (requirements: fields, documents, questionnaires, agreements) and holds what has been provided so far.
- Every time Due needs information from a user, it is collected through a submission. There is no other way to provide data.
- You fill it through the API (
/v1/kyc/submissions/{id}/…) or send the user to a hosted link. - It is editable while it is
open. Once completed it is frozen: a correction is always a new submission, never an edit of the old one.
A submission tells you what to collect. It does not tell you why it is needed or whether it was accepted. That is what the next objects are for.
→ Submission statuses · How to fill one: Individual KYC process, Business KYB process · Hosted submission link
Information request: the reason and the review
An information request is Due asking for information, with a reason, and reviewing the answer. Think of it as a container for submissions rather than as a record of its own: it points to the submission being reviewed (submissionId), and if that submission needs correcting it opens another one and asks again.
Due raises an information request when it needs something beyond base KYC:
| Why | Example | kind | About (submission.subject.kind) |
|---|---|---|---|
| Open a rail | A partner needs the state of residence for brl_bank_v1 | endorsement | applicant |
| Compliance check on the account | Source-of-funds questionnaire, enhanced due diligence, video identification | due_diligence | applicant |
| A partner's follow-up | A bank asks for proof of source of wealth | due_diligence | applicant |
| Let a transfer through | A large payment needs an invoice | due_diligence | transfer or transaction |
| Know the other party | The relationship with a recipient | due_diligence | counterparty |
Information requests can appear at any time, including long after onboarding. Only those with kind: endorsement belong to an endorsement (their presetCode is the endorsement code).
→ Information request statuses · Requests for information on transfers · Video-identification EDD
Endorsement: a rail to unlock
An endorsement is an additional approval that unlocks specific channels, for example named EUR accounts or USD accounts at a partner bank (see endorsementsRequired in GET /v1/channels). You request it; if more information is needed, Due raises information requests for it. Once they are approved, the endorsement continues by itself.
→ Endorsement statuses · KYC/KYB endorsements
Account KYC: the base verification
kyc.status on the account is the result of base KYC/KYB. Base KYC is the one case with no information request: its form is a submission on its own, and the outcome shows on kyc.status.
→ Account KYC statuses · Handle KYC status webhooks · Sumsub KYC Sharing
How they fit together
| You see | It means |
|---|---|
| A submission with an information request pointing to it | Due asked for this for the reason given by the information request |
| A submission with no information request | Base KYC (first verification, or something to redo) |
| An endorsement | A rail waiting for its information requests, then for the partner |
The one rule to build on: your user has a task when a submission is open and it either belongs to an information request that is requested, or is a base-KYC submission while kyc.status is pending or resubmission_required. See What the user sees and Opening a task.
Statuses
Submission
| Status | Meaning | User has work? |
|---|---|---|
open | Waiting for the user | Yes |
processing | The user completed it; Due is handing it to review (usually seconds) | No |
completed | Handed to review. Says nothing about whether it was accepted | No |
stateDiagram-v2
direction LR
[*] --> open
open --> processing: user completes
processing --> completed
A submission only moves forward. Once it leaves open it can't be read or written through /v1/kyc/submissions/{id}, and its hosted link stops working.
Information request
| Status | Meaning |
|---|---|
requested | Waiting for the information. It stays requested until its submission is completed, so for a moment after the user finishes it is still requested |
on_review | Under review |
approved | Accepted. Whatever was waiting on it continues |
rejected | Not accepted, and final. Nothing more can be submitted |
expired | It was approved and its validity has ended. Nothing is wrong and there is nothing to do: it simply stops counting as approved, and a new information request is raised the next time a policy needs it. Only due_diligence information requests expire |
stateDiagram-v2
direction LR
[*] --> requested
[*] --> approved: nothing was missing
requested --> on_review
on_review --> approved
on_review --> requested: resubmission needed, new submission
on_review --> rejected
rejected --> [*]
approved --> expired: validity ended
If a review needs something corrected, the information request goes back to requested rather than being rejected. It keeps its id and carries a new submissionId, and that new submission is the user's task. Watch the status, not the id.
rejected is final. It is not a step on the way to another attempt.
An information request with submissionId: null needed nothing new and was created approved. A fast review can go from requested straight to approved or rejected.
Endorsement
| Status | Meaning |
|---|---|
requested | Waiting for its information requests |
processing | Due is registering the account at the partner |
awaiting_approval | The partner is reviewing |
approved | The rail works |
processing_error | Registration failed or the partner refused. Contact Due |
There is no rejected. An endorsement can be requested only when kyc.status is passed. If nothing is missing it starts at processing or approved.
If an endorsement's information request is rejected, the endorsement stays requested. It does not move to an error status, and the rail does not open. Read the endorsement's information requests to know whether it is still progressing.
Account KYC
| Status | What to do |
|---|---|
pending / resubmission_required | The user completes the open base-KYC submission. If there is none, open the hosted KYC flow (POST /v1/kyc/session) |
manual_review | Wait |
passed | Verified. Keep handling information requests |
failed | Rejected. Keep KYC-gated services blocked |
A status can change again after a review (for example passed → resubmission_required). Always act on the latest value.
Building on webhooks
What to store
| Table | Key | Fields |
|---|---|---|
accounts | account id | status, kycStatus |
submissions | submission id | accountId, status, informationRequestId (empty for base KYC), subjectKind, presetCode |
information_requests | information request id | accountId, kind, presetCode, status, submissionId, createdAt |
endorsements | account id + code | status |
Where each row first comes from
| What starts it | Who | First seen |
|---|---|---|
| Base KYC (start or Sumsub sharing) | you | The response to your call |
| Base KYC has to be redone | Due | transfers.kyc.submission.status_changed with status: open |
| Endorsement | you, POST /v1/kyc/endorsements/{code} | The response: informationRequests[] and their submission. No webhook is sent for these |
| Compliance check, transfer, counterparty, partner follow-up, resubmission | Due | information_request.updated with status: requested (includes submission) |
Store what your own calls return, and let webhooks create everything Due starts. A submission webhook for an id no information request claims is base KYC.
What each webhook carries
Body: { id, type, data, occurredAt, attemptedAt }. occurredAt is when Due recorded the event for delivery, not when the change itself happened, but it stays the same across redeliveries of the same event. ownerId is the account id everywhere.
type | data | Sent when |
|---|---|---|
transfers.kyc.submission.status_changed | The submission | It becomes processing, then completed. Also once with open when Due opens a new base-KYC submission, either a redo or because Sumsub sharing left data missing |
information_request.updated | The information request, including submission | Due raises an information request (requested), or one becomes on_review, approved or rejected, or one under review goes back to requested with a new submission |
endorsement.status_changed | { endorsement: { ownerId, code, status } } | An endorsement changes status (not for the initial requested) |
bp.kyc.status_changed | The account, with status and kyc.status | kyc.status changes |
Deliveries can be retried and can arrive out of order. Make handlers idempotent; keep a submission's latest status (it only moves forward), and for the other objects ignore events with an older occurredAt than the one you last applied.
What the user sees
function tasks(account):
if account.status == "deactivated" or account.kycStatus == "failed":
return []
return submissions where accountId == account.id and status == "open" and (
(informationRequestId is set and information_requests[informationRequestId].status == "requested")
or (informationRequestId is empty and account.kycStatus in ["pending", "resubmission_required"]))If there are no tasks, the user waits:
| Your rows say | Show | You do |
|---|---|---|
kycStatus is resubmission_required and no open base-KYC submission | The hosted KYC flow (POST /v1/kyc/session) | |
An information request is rejected | The information was not accepted | Nothing (final) |
An information request is expired | Nothing | Nothing. A new one is raised when a policy needs it |
An endorsement is processing_error | "We're looking into it" | Contact Due |
| Anything else | "Under review" | Nothing |
Opening a task
Get the link when the user taps the task; don't store it.
- Submission with an information request:
POST /v1/information_requests/{informationRequestId}/session - Base-KYC submission:
POST /v1/kyc/sessionswith{"submissionId": "…"}
Then open https://app.due.network/kyc_submissions/{submissionId}?token={token}.
422 err_kyc_submission_finalizedmeans the submission was completed meanwhile: mark it as no longer open and refresh.- A token lasts 7 days. A token being issued does not mean the submission is open; your
submissions.statusdoes.
To render the form yourself instead, use the submission's requirements, write through /v1/kyc/submissions/{id}/… and finish with POST /v1/kyc/submissions/{id}/complete.
What webhooks don't tell you
| Change | How to catch it |
|---|---|
| An endorsement and its information requests are created | Store the response to your own call |
You don't need to track expiry. When an approved information request stops counting, Due raises a new one and you receive it as requested.
To repair one account's rows (missed delivery, new deployment), rebuild them from GET /v1/accounts/{id}, GET /v1/information_requests, GET /v1/kyc/submissions and GET /v1/kyc/endorsements.
When to contact Due
| Your rows say | Send |
|---|---|
An endorsement is processing_error | account id, endorsement code |
A submission has been processing for more than a few minutes | account id, submission id |
An information request is requested but no longer seems needed | account id, information request id |
Mistakes to avoid
- Driving tasks from the information request's status.
requestedoutlives the user's work; usesubmission.status == "open". - Waiting for a webhook to learn an endorsement's submissions. They are in the response to your call.
- Guessing what a submission is for from
presetCode. Use its information request; if none, it's base KYC. - Treating an information request id as one round of review. The same id can be sent back to
requestedwith a new submission. - Treating
requestedas always meaning a first attempt. It also means a correction was asked for. - Showing base-KYC submissions once
kycStatusispassedormanual_review. Leftover submissions are not closed. - Moving a submission backward because an older webhook arrived late.
Example: opening a rail that needs one more field
| What happens | You receive | Your rows after | Tasks |
|---|---|---|---|
You call POST /v1/kyc/endorsements/brl_bank_v1 | The response | endorsement requested, information request requested, submission open | the submission |
| The user completes the submission | transfers.kyc.submission.status_changed (processing) | submission processing | none |
| It reaches review | transfers.kyc.submission.status_changed (completed), information_request.updated (on_review) | submission completed, information request on_review | none |
| Review passes | information_request.updated (approved), endorsement.status_changed (processing) | information request approved, endorsement processing | none |
| The partner accepts | endorsement.status_changed (approved) | endorsement approved | none |
If the review needs something corrected, you receive information_request.updated (requested) for the same information request, now carrying a new submissionId. That new submission shows up as a task.
If the review rejects it, the information request becomes rejected and the endorsement stays requested. The rail does not open and there is nothing for the user to do.
Updated about 1 hour ago