---
title: Contact Verifications
description: Generated HTTP reference for the 2 operations the published OpenAPI document describes under contact-verifications.
---

# 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](/docs/api/reference) for what that means.

Base URL: `https://api.codespar.dev`

Every operation below requires a Bearer token. See [Authentication](/docs/concepts/authentication).

### POST `/v1/consumers/{consumerId}/contact-verifications`

<Endpoint method="POST" path="/v1/consumers/{consumerId}/contact-verifications" base="https://api.codespar.dev" />

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

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","TypeScript"]}>
<Tab value="curl">

```bash
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"
     }'
```

</Tab>
<Tab value="TypeScript">

```ts
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();
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 201">

```json title="application/json"
{
  "id": "obj_0000000000000000",
  "channel": "email",
  "target_masked": "string",
  "provider": "dev",
  "expires_at": "2026-01-15T12:00:00.000Z",
  "status": "sent"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/consumers/{consumerId}/contact-verifications" body={"{\"channel\":\"email\",\"target\":\"string\",\"locale\":\"string\"}"} />

### POST `/v1/consumers/{consumerId}/contact-verifications/{id}/verify`

<Endpoint method="POST" path="/v1/consumers/{consumerId}/contact-verifications/{id}/verify" base="https://api.codespar.dev" />

Check 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 | — |

<Split min={380}>
<SplitPane label="Example request">

<Tabs items={["curl","TypeScript"]}>
<Tab value="curl">

```bash
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"
     }'
```

</Tab>
<Tab value="TypeScript">

```ts
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();
```

</Tab>
</Tabs>

</SplitPane>
<SplitPane label="Example response 200">

```json title="application/json"
{
  "id": "obj_0000000000000000",
  "status": "verified",
  "verified_at": "2026-01-15T12:00:00.000Z"
}
```

</SplitPane>
</Split>

<TryIt method="POST" path="/v1/consumers/{consumerId}/contact-verifications/{id}/verify" body={"{\"code\":\"string\",\"target\":\"string\"}"} />

