Skip to content

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).

Method Path Scope Success
POST /v1/verifications verification:write 201
POST /v1/verifications/:reference/check verification:write 201
GET /v1/verifications/:reference read 200

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.

Terminal window
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" }
}'
201 Created
{
"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.

POST /v1/verifications/:reference/check

Field Type Required Notes
code string Yes The code the user received
Terminal window
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
201 — wrong code
{
"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 201 with status: "failed" so you can render the attempt count in your UI.
  • An unknown reference also returns 201 with reason: "not_found" — unlike the GET endpoint, which returns 404.
  • 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" with attempts_remaining: 0 and flips the row to failed. Further checks return reason: "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 /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.

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)
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.