Verifications
Contact verification proves that a user controls a phone number or email address. You start a verification, Nuansis delivers a one-time code on the chosen channel, and you submit the code the user gives you.
The code is never returned by any endpoint. Only a salted SHA-256 hash is stored (voice verifications additionally keep a short-lived plaintext copy so the telephony layer can read it aloud).
Endpoints
Section titled “Endpoints”| Method | Path | Scope | Success |
|---|---|---|---|
POST |
/v1/verifications |
verification:write |
201 |
POST |
/v1/verifications/:reference/check |
verification:write |
201 |
GET |
/v1/verifications/:reference |
read |
200 |
Start a verification
Section titled “Start a verification”POST /v1/verifications
| Field | Type | Required | Notes |
|---|---|---|---|
channel |
string | Yes | sms, voice, or email |
destination |
string | Yes | E.164 phone for sms/voice; email address for email |
metadata |
object | No | Stored with the request and returned nowhere in the public view |
code |
string | No | Operator-supplied 4–8 digit code. If omitted, Nuansis generates a 4-digit code |
Phone destinations are normalised to E.164 and are currently limited to Nigerian numbers
(+234 followed by 10 digits). Email destinations are lowercased. An invalid destination fails
validation before anything is sent.
curl -X POST https://api.nuansis.co/v1/verifications \ -H "Authorization: Bearer $NUANSIS_API_KEY" \ -H "Idempotency-Key: signup-8f2c1e" \ -H "Content-Type: application/json" \ -d '{ "channel": "sms", "destination": "+2348012345678", "metadata": { "user_id": "usr_1042" } }'{ "data": { "reference": "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f", "channel": "sms", "destination": "+2348****678", "expiresAt": "2026-10-18T10:31:07.412Z", "status": "pending" }, "meta": { "request_id": "b3a1c9d2-7e4f-4a1b-9c8d-2e3f4a5b6c7d" }}Note that this response uses expiresAt in camelCase. The GET representation uses expires_at.
If the provider cannot deliver the code, the request fails with 400 invalid_request and a message
beginning Could not deliver the code:. The verification row is retained with status failed.
Check a code
Section titled “Check a code”POST /v1/verifications/:reference/check
| Field | Type | Required | Notes |
|---|---|---|---|
code |
string | Yes | The code the user received |
curl -X POST https://api.nuansis.co/v1/verifications/6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f/check \ -H "Authorization: Bearer $NUANSIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "4821" }'A check is a state machine, not a boolean. It always returns a status, and adds a reason on
failure:
| Response body | Meaning |
|---|---|
{ "status": "verified" } |
Correct code; the verification is now verified |
{ "status": "failed", "reason": "invalid_code", "attempts_remaining": 4 } |
Wrong code |
{ "status": "failed", "reason": "expired" } |
The code expired |
{ "status": "failed", "reason": "not_found" } |
No verification with that reference in this workspace |
{ "status": "failed", "reason": "failed" } |
The request was already terminal |
{ "data": { "status": "failed", "reason": "invalid_code", "attempts_remaining": 4 }, "meta": { "request_id": "c4b2d0e3-8f5a-4b2c-8d9e-3f4a5b6c7d8e" }}Important behaviours:
- A wrong code is not an HTTP error. It returns
201withstatus: "failed"so you can render the attempt count in your UI. - An unknown reference also returns
201withreason: "not_found"— unlike theGETendpoint, which returns404. - Checking an already-verified verification returns
{ "status": "verified" }and is safe to retry. - Defaults: 5 attempts (
VERIFY_MAX_ATTEMPTS) and a 300-second TTL (VERIFY_CODE_TTL_SECONDS). - The final wrong attempt returns
reason: "invalid_code"withattempts_remaining: 0and flips the row tofailed. Further checks returnreason: "failed"because the row is no longer pending. - The webhook layer can also report
reason: "too_many_attempts"for the defensive path where the counter is already at the maximum but the row has not yet been marked failed.
An optional operator-supplied code must be 4–8 digits. Supplying one bypasses code generation, so
never accept it from an untrusted client.
Get a verification
Section titled “Get a verification”GET /v1/verifications/:reference
{ "data": { "reference": "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f", "channel": "sms", "destination": "+2348****678", "status": "pending", "attempts": 0, "max_attempts": 5, "provider": "twilio", "expires_at": "2026-10-18T10:31:07.412Z", "verified_at": null, "created_at": "2026-10-18T10:26:07.412Z" }, "meta": { "request_id": "d5c3e1f4-9a6b-4c3d-8e0f-4a5b6c7d8e9f" }}Unknown references return 404 not_found. The response never includes the code, its hash, or the
salt.
GET is a pure read: it does not expire a stale row. A verification whose TTL has passed still
reports its stored status (pending) until a check is attempted, at which point it moves to
expired and emits verification.expired.
Common errors
Section titled “Common errors”| Status | Code | Cause |
|---|---|---|
400 |
invalid_request |
Unsupported channel, invalid email, invalid E.164 number, non-Nigerian phone, malformed supplied code, or delivery failure |
401 |
unauthorized |
Missing, malformed, unknown, or revoked key |
403 |
insufficient_scope |
Key lacks verification:write |
404 |
not_found |
GET for an unknown reference |
429 |
rate_limited |
Per-destination or per-business verification limit (see Rate limits) |
Webhooks
Section titled “Webhooks”| Event | Fired when |
|---|---|
verification.verified |
A code is accepted |
verification.failed |
Attempts are exhausted (reason: invalid_code or too_many_attempts) |
verification.expired |
A check is attempted after the code’s TTL has passed |
Payloads carry the reference, channel, masked destination, status, attempts, and verified_at
where applicable. See Webhooks.