Email verification
Capture a recipient email and prove control of it with a one-time code. PUT /recipient/account/email issues a code, POST /recipient/account/email/verify redeems it, both idempotent.
A recipient's email is what notifications route to and what initiates account
recovery, so Keepable verifies control of it before trusting it. Verification is
two steps: capture the address (which issues a one-time code), then
redeem the code. Both steps are mutations and carry an
Idempotency-Key.
Step 1: capture the email
PUT /recipient/account/email records the address and dispatches a six-digit
one-time code to it.
curl -X PUT https://api.keepable.co/recipient/account/email \
-H "Authorization: Bearer $RECIPIENT_TOKEN" \
-H "Keepable-Version: 2026-05-24" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "email": "ada.new@example.ng" }'const res = await fetch("https://api.keepable.co/recipient/account/email", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Keepable-Version": "2026-05-24",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "ada.new@example.ng" }),
});
const { challenge_id, expires_at } = await res.json();{
"challenge_id": "evc_3f2a...",
"expires_at": "2026-06-01T12:15:00Z"
}The 202 means a code was issued, not that the email is verified yet. The code
is valid until expires_at (a window of roughly 15 minutes). Requesting a new
code supersedes any outstanding one, and requests are rate-limited per
recipient, so a flood of requests returns
429 Too Many Requests.
In development (where no real email is sent) the response also carries a
dev_code field with the six-digit code, so a developer can complete the flow
without an inbox. dev_code is never present in production, where the code
is delivered only by email.
Step 2: redeem the code
POST /recipient/account/email/verify redeems the code issued above. On success
the email is linked as a hashed identifier and the profile's email_verified
flips to true.
curl -X POST https://api.keepable.co/recipient/account/email/verify \
-H "Authorization: Bearer $RECIPIENT_TOKEN" \
-H "Keepable-Version: 2026-05-24" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "code": "048213" }'const res = await fetch(
"https://api.keepable.co/recipient/account/email/verify",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Keepable-Version": "2026-05-24",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({ code: "048213" }),
},
);
if (res.status === 204) {
// Email verified and linked.
}A success is 204 No Content. There is no body: re-read
GET /recipient/account to see email_verified: true
and the linked email.
When verification fails
The code is checked in constant time, and a wrong code counts against an attempts cap. The failure modes map to distinct statuses so the apps can give the recipient the right next step:
| Status | Problem | What happened | What to do |
|---|---|---|---|
400 | Bad request | The submitted code is incorrect (but the challenge is still open). | Let the recipient retry with the right code. |
404 | Not found | There is no pending verification to redeem. | Send the recipient back to step 1 to request a code. |
409 | Conflict | The email is already linked to another account. An email belongs to exactly one recipient. | Prompt for a different address. |
422 | Unprocessable | The code has expired. | Request a fresh code (step 1). |
429 | Too many requests | Too many failed attempts; the challenge is locked. | Request a fresh code (step 1). |
Re-verifying to change email
Verifying a new address replaces any prior email identifier: the same flow both sets an email for the first time and changes an existing one. The new address is not trusted until its code is redeemed, so a recipient's verified email never silently changes out from under them.
What a verified email unlocks
The moment an email is verified and linked it becomes a resolvable identifier. Any content a sender addressed to that email while the recipient was unreachable is released into their inbox, and they are notified. That held-pending to delivered release is covered in Delivery notifications.
Account profile
GET /recipient/account returns the recipient's profile, the masked NIN, legal name, email, verification flags, and the assurance level they were onboarded at.
Assurance levels
The two recipient assurance tiers, email and id_verified, what each one means, how it is established, and why it gates what a recipient can receive.