# Consent

> Ask a data subject to approve a check by link, email or USSD, then use the granted consent to unlock records such as the full BVN.

Every check needs a lawful basis. For most methods you attest that you obtained consent (`consent: { "obtained": true }`), or the method carries its own proof (a live selfie, a NINAuth share code). Some checks need more: the **full BVN record** (`method: "core"`) is released only after the data subject approves the request themselves.

Speedmap's consent service handles that approval for you. In live mode, BVN consent requests are registered with the **NIBSS Consent Hub**; the subject can approve on any phone, including by USSD without a smartphone.

## How it works

1. You create a consent request for an identifier, with a [purpose code](https://speedmap-delta.vercel.app/docs/compliance#purpose-codes) and the categories of data you need.
2. You share the `authorization_url` (or the USSD code) with the subject, or Speedmap emails it.
3. The subject reviews who is asking and why, and approves with a one-time code, or declines.
4. Speedmap sends `consent.updated` to your [webhook](https://speedmap-delta.vercel.app/docs/webhooks). The grant is valid for **24 hours**.
5. You call the product with `consent_id`.

## Create a consent request

`POST /v1/consents`

**Body parameters**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier_type` | string | Yes | `bvn` or `nin`. |
| `identifier` | string | Yes | The 11-digit BVN or NIN. |
| `categories` | string | No | Y/N mask over four data categories, in order: personal identification, contact information, demographic details, geographic details. At least one `Y`. Defaults to `YYNN`. |
| `channel` | string | No | `redirect` (you send the subject to `authorization_url`), `email` (Speedmap emails the link) or `ussd` (a USSD code is issued). Defaults to `redirect`. |
| `reason` | string | No | One of the 38 [purpose codes](https://speedmap-delta.vercel.app/docs/compliance#purpose-codes). Defaults to `financialProducts`. |
| `email` | string | No | Required when `channel` is `email`. |
| `redirect_url` | string | No | http(s) URL to send the subject back to after they decide. |

_cURL_

```bash
curl -X POST "http://localhost:4000/v1/consents" \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
  "identifier_type": "bvn",
  "identifier": "22222222221",
  "categories": "YYNN",
  "channel": "ussd",
  "reason": "financialProducts"
}'
```

_Node.js_

```javascript
const res = await fetch("http://localhost:4000/v1/consents", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPEEDMAP_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "identifier_type": "bvn",
    "identifier": "22222222221",
    "categories": "YYNN",
    "channel": "ussd",
    "reason": "financialProducts"
  }),
});

const consent = await res.json();
if (!res.ok) throw new Error(consent.error.message);
console.log(consent);
```

_Python_

```python
import os, requests

res = requests.post(
    "http://localhost:4000/v1/consents",
    headers={
        "Authorization": f"Bearer {os.environ['SPEEDMAP_API_KEY']}",
    },
    json={
        "identifier_type": "bvn",
        "identifier": "22222222221",
        "categories": "YYNN",
        "channel": "ussd",
        "reason": "financialProducts"
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
```

**201 Created**

```json
{
  "id": "cns_423a8f143e7f2f8ffe7dda3344d36710",
  "object": "consent",
  "identifier_type": "bvn",
  "subject": "*******2221",
  "categories": "YYNN",
  "category_labels": [
    "Personal identification",
    "Contact information"
  ],
  "channel": "ussd",
  "status": "pending",
  "reason": "financialProducts",
  "authorization_url": "https://speedmap-delta.vercel.app/consent/cns_423a8f143e7f2f8ffe7dda3344d36710",
  "ussd_code": "*565*1*7*70205378#",
  "redirect_url": null,
  "granted_at": null,
  "expires_at": "2026-10-20T09:41:07.512Z",
  "created_at": "2026-10-19T09:41:07.512Z",
  "mode": "sandbox"
}
```

### YNYN categories

`categories` is a four-letter mask. Each position is `Y` (requested) or `N`:

| Position | Category | Example |
| --- | --- | --- |
| 1 | Personal identification | names, date of birth, photo |
| 2 | Contact information | phone, email |
| 3 | Demographic details | gender, marital status, nationality |
| 4 | Geographic details | address, state and LGA |

`YYNN` asks for identification and contact data only. Ask for the minimum you need: the subject sees exactly these categories, as `category_labels`.

### Channels

- **redirect**: send the subject to `authorization_url`, the hosted consent page at `/consent/{id}`. Add `redirect_url` to bring them back to your app afterwards.
- **email**: Speedmap emails the link to `email`.
- **ussd**: the response includes a `ussd_code` such as `*565*1*7*70205378#`. The subject dials it on any phone, including a feature phone, to approve.

## The hosted consent page

`authorization_url` opens a Speedmap-hosted page that shows your organization's name, the purpose, the categories requested and the masked identifier. The subject approves with a one-time code or declines. The consent id in the URL is unguessable (128 bits) and acts as the capability for the page, so share it only with the subject.

- A pending request **expires after 24 hours**.
- Five wrong one-time codes lock the request; create a new one.
- In the sandbox the one-time code is always `123456`, and you can decide through `POST /v1/sandbox/consents/{id}/approve` or `/deny`. See [Sandbox](https://speedmap-delta.vercel.app/docs/sandbox#consent-in-the-sandbox).

## Use the consent

Once `status` is `granted`, pass the id to the product. For the full BVN record:

_cURL_

```bash
curl -X POST http://localhost:4000/v1/verify/bvn \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "method": "core",
  "bvn": "22222222221",
  "reason": "financialProducts",
  "consent_id": "cns_423a8f143e7f2f8ffe7dda3344d36710"
}'
```

_Node.js_

```javascript
const res = await fetch("http://localhost:4000/v1/verify/bvn", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPEEDMAP_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    "method": "core",
    "bvn": "22222222221",
    "reason": "financialProducts",
    "consent_id": "cns_423a8f143e7f2f8ffe7dda3344d36710"
  }),
});

const verification = await res.json();
if (!res.ok) throw new Error(verification.error.message);
console.log(verification.status, verification.match);
```

_Python_

```python
import os, uuid, requests

res = requests.post(
    "http://localhost:4000/v1/verify/bvn",
    headers={
        "Authorization": f"Bearer {os.environ['SPEEDMAP_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "method": "core",
        "bvn": "22222222221",
        "reason": "financialProducts",
        "consent_id": "cns_423a8f143e7f2f8ffe7dda3344d36710"
    },
    timeout=30,
)
res.raise_for_status()
verification = res.json()
print(verification["status"], verification["match"])
```

The check fails with `412` when the consent cannot be used:

| Code | Meaning |
| --- | --- |
| `consent_not_granted` | The consent is pending, denied, expired or revoked. |
| `consent_expired` | The grant is older than 24 hours. Create a new request. |
| `consent_subject_mismatch` | The consent was granted for a different identifier. |

An unknown `consent_id` returns `404 consent_not_found`.

## Retrieve and list

`GET /v1/consents/{id}`

`GET /v1/consents`

Lists take `limit` (up to 100, default 50) and the `before` cursor, like every other list. See [Pagination](https://speedmap-delta.vercel.app/docs/pagination).

## Revoke a consent

Revoke a granted consent as soon as you no longer need it, or when the subject withdraws it. Revoked consents can no longer unlock checks.

`POST /v1/consents/{id}/revoke`

_cURL_

```bash
curl -X POST "http://localhost:4000/v1/consents/cns_423a8f143e7f2f8ffe7dda3344d36710/revoke" \
  -H "Authorization: Bearer sk_test_..."
```

_Node.js_

```javascript
const res = await fetch("http://localhost:4000/v1/consents/cns_423a8f143e7f2f8ffe7dda3344d36710/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPEEDMAP_API_KEY}`,
  },
});

const consent = await res.json();
if (!res.ok) throw new Error(consent.error.message);
console.log(consent);
```

_Python_

```python
import os, requests

res = requests.post(
    "http://localhost:4000/v1/consents/cns_423a8f143e7f2f8ffe7dda3344d36710/revoke",
    headers={
        "Authorization": f"Bearer {os.environ['SPEEDMAP_API_KEY']}",
    },
    timeout=30,
)
res.raise_for_status()
print(res.json())
```

Only granted consents can be revoked; anything else returns `409 consent_not_granted`.

## Statuses and webhooks

| Status | Meaning |
| --- | --- |
| `pending` | Waiting for the subject. Expires 24 hours after creation. |
| `granted` | Approved. Usable for 24 hours from `granted_at`. |
| `denied` | The subject declined. |
| `expired` | Not decided in time, or locked after five wrong codes. |
| `revoked` | Withdrawn by you. |

Every decision fires `consent.updated` with the consent object as `data`. Deciding on a request that is no longer pending returns `409 consent_not_pending`.

> **Live availability:** Live consent requests are available for BVN through the NIBSS Consent Hub. If the Hub rejects a request you receive `502 consent_hub_error`. Live consent for NIN is not available yet; use the `in_person` or `share_code` NIN methods, which carry their own consent.

---

Source: https://speedmap-delta.vercel.app/docs/consent
