On this page
POST /api/kyc/verifications/:id/reviewApproves, declines or sends back a finished verification, or changes a decision made earlier. This is the API twin of the review controls in your dashboard. The two run the same core, so a decision made here does exactly what one made there does:
- the person's record moves to approved, rejected or under review
- declining pauses their ongoing screening, and approving resumes it
- the change is added to the verification's timeline in your dashboard, and to
statusHistoryon the result endpoint - your webhook endpoints receive
verification.status_updatedwithsource: "api" - the applicant is emailed about it when you ask, or when the workflow does (see Emailing the applicant)
The verification itself is never rewritten. checkStatus keeps saying what the checks found, so an approval granted despite a failed check stays visible.
Authentication: Authorization: Bearer sk_… (secret key required). A publishable key ships in a browser, and a browser must never be able to approve its own applicant, so it receives 403 secret_key_required.
Content type: application/json.
Production note: sk_live_ keys require an approved business.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
decision | string | yes | APPROVED, DECLINED or RESUBMISSION. |
note | string | no | Why, up to 2,000 characters. Kept in your audit log and on the verification's timeline in your dashboard. It is never sent to your webhook endpoints or returned by the API. |
steps | string[] | no | RESUBMISSION only. The steps to redo, named as in Verify again. Absent or empty means the whole flow. |
message | string | no | RESUBMISSION only. Shown to the applicant when they open the link, up to 500 characters. Write it for them, not for your logs. |
policy | string | no | RESUBMISSION only. original runs the workflow version the applicant walked; latest runs today's. Absent follows the workflow's own setting. |
notifyApplicant | boolean | no | APPROVED or DECLINED only. true emails the applicant about the decision and false does not. Absent follows the workflow: an approval is emailed when its Email the applicant when approved setting is on, and a decline never is. |
email | string | no | On a RESUBMISSION, send the link to the applicant at this address as well as returning it. On an approval or a decline sent with notifyApplicant: true, the address to email instead of the one the verification holds. |
A field sent with a decision it does not apply to is refused with 400 invalid_input rather than ignored, because a caller who sent it has misunderstood something: steps, message or policy with an approval or a decline, email with one that does not also send notifyApplicant: true, or notifyApplicant with a RESUBMISSION.
Request
curl "https://trust.myaza.app/api/kyc/verifications/ver_abc123/review" \
-H "Authorization: Bearer $MYAZA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "DECLINED", "note": "Duplicate of an existing customer" }'Response 200 OK
{
"review": {
"verificationId": "ver_abc123",
"decision": "DECLINED",
"status": "declined",
"previousStatus": "approved",
"checkStatus": "verified",
"reviewedAt": "2026-09-13T09:00:00.000Z",
"entityId": "ent_01j9def789",
"attempt": 1,
"delivery": null,
"applicantEmail": null,
"resubmission": null
}
}| Field | Description |
|---|---|
status | The verification's top-line status now, the same value Get verification status returns. |
previousStatus | The top-line status immediately before this decision. |
checkStatus | What the checks found. No decision changes it. |
entityId | The person's record the decision was applied to. Null when the checks never created one, for example on a failed check. |
attempt | The attempt the verification holds now. A verification you send back keeps its id, and the applicant's resubmission becomes the next attempt. |
delivery | On a RESUBMISSION with email: { emailed, reason }. emailed: false with reason: "send_failed" means the email could not be sent, and the link still works. Null otherwise. |
applicantEmail | On an approval or a decline where emailing the applicant was asked for, by notifyApplicant or by the workflow: { emailed, reason, recipient }. recipient is { masked, source }: the address masked (a•••@example.com) and where it came from, email_verification, sdk, entity or provided. When emailed is false, reason says why: no_recipient (there was no address to send to) or send_failed. The decision stands either way. Null when nobody asked. |
resubmission | On a RESUBMISSION, the link to send the applicant: sessionId, url, shortCode, expiresAt, steps and full. url is a credential: whoever opens it verifies as this applicant. Null otherwise. |
Emailing the applicant
We can tell the applicant about an approval or a decline, in your organisation's name:
{ "decision": "APPROVED", "notifyApplicant": true }The email says what was decided and who to contact. It never gives a reason: that is yours to give, in your own words.
We only write to an address the verification already holds, using the first of these that has one:
- the email the applicant proved they own with a code, when the workflow runs email verification;
- an email your integration passed, as
userData.emailon the SDK or on a session you created (the address you emailed the session link to counts too); - the email on the person's entity record.
To use another address, send it as email beside notifyApplicant: true. When there is no address, nothing is sent and applicantEmail.reason is no_recipient.
Leave notifyApplicant out and the workflow decides: when it has Email the applicant when approved switched on, an approval is emailed however it was made. A decline is only ever emailed when you ask. Every email is added to the verification's timeline in your dashboard.
Sending it back
{
"decision": "RESUBMISSION",
"steps": ["document-capture"],
"message": "Your document photo was too blurry to read. Please retake it in good light."
}The applicant gets a link on the same workflow and walks only the steps you name. Their resubmission becomes the next attempt of this verification, under the same id, so you never have a second verification to reconcile. This is the same operation as Verify again.
Add "email": "ada@example.com" and we send the link for you. Otherwise delivering it is yours to do. Either way it is in this response and in the webhook, and any link sent earlier for the same verification stops working.
Changing a decision
Call it again with a different decision. The latest decision is the verification's status, each earlier one stays on record, and every change sends its own verification.status_updated. Read the whole trail back with Get verification result, whose statusHistory lists every change; in your dashboard it is on the verification's timeline.
A decision can be made on any finished verification: one your workflow sent for review, one it approved or declined automatically, or one a person has already decided.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_input | The body is malformed, decision is not one of the three values, or a field came with a decision it does not apply to. |
| 400 | invalid_steps | A step name is not a valid step. The message lists the valid names. |
| 403 | secret_key_required | A publishable key was used. |
| 404 | verification_not_found | No verification with that id in your organisation and environment. |
| 409 | verification_in_progress | The checks have not finished. Wait for the result before deciding it. |
| 422 | no_workflow | A RESUBMISSION on a verification that did not run a workflow, so there is no flow to send the applicant back into. Approve or decline it instead. |
| 429 | rate_limited | Too many sessions started this hour. A send-back starts one. Try again shortly. |