Contact Verifications
Generated HTTP reference for the 2 operations the published OpenAPI document describes under contact-verifications.
5 min read
Contact Verifications
This page is generated from the published OpenAPI document. It is complete with respect to that document and says nothing about surfaces the document does not describe yet. See what is generated here for what that means.
Base URL: https://api.codespar.dev
Every operation below requires a Bearer token. See Authentication.
POST /v1/consumers/{consumerId}/contact-verifications
POST
https://api.codespar.dev/v1/consumers/{consumerId}/contact-verificationsSend a one-time code to a consumer's e-mail address or phone number
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
consumerId | string | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
channel | "email" | "phone" | yes | — |
locale | string | no | — |
target | string | yes | — |
Responses
| Status | Body | Description |
|---|---|---|
201 | object | OK |
400 | object | invalid_body: the body did not match the schema, and details.issues carries the validation errors. invalid_target: the contact does not canonicalize for this channel. |
422 | object | no_celcoin_account: the consumer has no active pix-celcoin funding source in this organization — or is not this organization's consumer at all. otp_sender_refused: the sender refused to deliver to this number and repeating will not change that; details.provider names which sender said so. Nothing was sent, and no attempt was consumed, in either case. |
429 | object | too_many_requests: the per-contact hourly ceiling was reached. details.window_seconds and details.max_sends give the window and the ceiling. Nothing was sent. |
500 | object | object | Two different bodies, because two different failures land here. contact_verification_error in the usual error envelope: an unclassified failure while starting. Nothing was sent that could be verified. THE OTHER SHAPE IS NOT THAT ENVELOPE. Resolving the consumer's account runs before the handler's own error handling, so a failure there is answered by the framework's default: \{ statusCode, error, message \}, plus code when the underlying failure carried one. A client that parses error.code will find error is a STRING here. Nothing was sent in this case either. |
503 | object | otp_sender_unavailable, and the three conditions behind it DO NOT agree on whether a retry is free. 1. No sender is configured for this channel on the deployment. Nothing was written and nothing was sent; retrying changes nothing until the deployment does. 2. The send itself did not complete. The verification row is removed, so the attempt does not count against the hourly ceiling and a retry is free. Removal is best effort: if it fails the row stands, unverifiable, and does consume one of the hour's sends. 3. THE CODE REACHED THE CONTACT and the row could not be completed. The row STANDS and DOES count against the hourly ceiling, and the consumer may receive a code that can never be verified. details.verification_id is present, and it is present ONLY in this case — that is how a caller tells the free retry from the expensive one. |
Response 201
| Field | Type | Required | Description |
|---|---|---|---|
channel | "email" | "phone" | yes | — |
expires_at | string (date-time) | yes | MAY ALREADY BE IN THE PAST. A second start for the same contact supersedes the earlier one by closing its validity, and if that lands while this send is in flight the row keeps the closed expiry and this field reports it. A superseded code answers 410 on verify. |
id | string (uuid) | yes | Send this back in the /verify step. |
provider | "dev" | "prelude" | "resend" | yes | Which sender delivered, and it decides the /verify step: prelude checks the code against the contact itself, so target is REQUIRED there. The other two are checked locally and do not need it. |
status | "sent" | yes | — |
target_masked | string | yes | The masked contact. The full value never comes back: the row keeps a keyed hash. |
Example request
curl -X POST https://api.codespar.dev/v1/consumers/{consumerId}/contact-verifications \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"target": "string",
"locale": "string"
}'const res = await fetch("https://api.codespar.dev/v1/consumers/{consumerId}/contact-verifications", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"channel": "email",
"target": "string",
"locale": "string"
}),
});
const data = await res.json();Example response 201
{
"id": "obj_0000000000000000",
"channel": "email",
"target_masked": "string",
"provider": "dev",
"expires_at": "2026-01-15T12:00:00.000Z",
"status": "sent"
}POST /v1/consumers/{consumerId}/contact-verifications/{id}/verify
POST
https://api.codespar.dev/v1/consumers/{consumerId}/contact-verifications/{id}/verifyCheck the code the consumer received
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
consumerId | string | yes | — |
id | string (uuid) | yes | — |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | — |
target | string | no | — |
Responses
| Status | Body | Description |
|---|---|---|
200 | object | OK |
400 | object | invalid_body: the body did not match the schema (details.issues). invalid_code: the code is not 4-8 digits, or it does not match; details.attempts_left says what is left, and only the second of those spent an attempt. invalid_target: this verification is checked against the contact and target did not come with the code. |
404 | object | not_found: there is no verification with this id for this consumer. It covers a malformed id, a row of another consumer, a row of another organization, a row whose send never completed, and a target that does not hash to the row's — that last one having spent an attempt. |
410 | object | verification_expired: the code expired, or a later send superseded it, or the sender itself reports it expired. Start a new verification. |
429 | object | too_many_attempts: this verification has spent every attempt. There is no reset; start a new one. |
500 | object | contact_verification_error: an unclassified failure while checking. This operation reads nothing outside its own error handling, so this is its only 500 body. |
503 | object | otp_sender_unavailable covers two conditions that cost different things. THE PROVIDER DID NOT ANSWER the check. The attempt was reserved before the call, so it IS spent and is not given back — returning it would reopen the race the reservation closes. details.attempts_left says how many remain. NO SENDER FOR THIS ROW'S PROVIDER exists on this deployment. The verify goes back to whichever sender delivered, whatever the deployment is configured for today, and this check runs BEFORE the reservation: NO attempt was spent and the response carries no details at all. Nothing the caller does resolves it — start a new verification, which will be sent through a sender that exists. |
Response 200
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes | — |
status | "verified" | yes | — |
verified_at | string (date-time) | yes | — |
Example request
curl -X POST https://api.codespar.dev/v1/consumers/{consumerId}/contact-verifications/{id}/verify \
-H "Authorization: Bearer $CODESPAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "string",
"target": "string"
}'const res = await fetch("https://api.codespar.dev/v1/consumers/{consumerId}/contact-verifications/{id}/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"code": "string",
"target": "string"
}),
});
const data = await res.json();Example response 200
{
"id": "obj_0000000000000000",
"status": "verified",
"verified_at": "2026-01-15T12:00:00.000Z"
}