> ## Documentation Index
> Fetch the complete documentation index at: https://docs.masterid.co.ug/llms.txt
> Use this file to discover all available pages before exploring further.

# Run a verification

> Submit identity checks from product selection through result retrieval, including consent, idempotency and result handling.

Before you submit a verification, you must first choose a product from the catalog, obtain lawful authorisation from the data subject, and ensure your wallet has sufficient funds. This guide walks through the full end-to-end flow.

**1. Choose a product from the catalog**

Call `GET /catalog` to list the verification products available to your workspace, their required fields, allowed selections and prices in UGX. Store product identifiers, required field names and exact select values in your integration. Use the response to build your submission form.

**2. Obtain lawful authorisation and request consent wording**

You must confirm that the data subject has consented or that another lawful basis applies. Use the fields you intend to submit to request the exact confirmation text:

```bash theme={null}
POST /verifications/consent
{
  "product": "tin",
  "fields": { "tin": "<APPROVED_TIN>" }
}
```

The response returns `data.consentText` for your account and subject. Present this text for genuine confirmation. Do not silently accept it.

**3. Submit the verification with an Idempotency-Key**

Include `Idempotency-Key` in the request header. The key must be 8-100 characters of letters, digits, `.`, `_`, `:`, or `-`.

```bash theme={null}
POST /verifications
{
  "product": "tin",
  "fields": { "tin": "<APPROVED_TIN>" },
  "consentConfirmed": true,
  "consentText": "<EXACT_CONFIRMED_TEXT>"
}
```

**4. Handle the response**

`200` means a completed result is returned immediately. `202` means the check is pending. Do not create another check for the same subject. Store the returned `sessionId` for polling or webhook correlation.

**5. Retrieve or receive the final result**

Poll `GET /verifications/{sessionId}` no more frequently than every 15 seconds, or handle the `verification.completed` webhook. Only records in your workspace are returned.

## Verification states

| Status | Meaning | Billing |
| - | - | - |
| `pending` | Processing. Do not create another check. | No charge yet |
| `legitimate` | Matching result returned. | Charged at catalog price |
| `fake` | No match or detection result returned. | Charged at catalog price |
| `failed` | No completed verification result. | Not charged |

Both matching and no-match results are charged at the catalog price. Failed checks are not charged. Pending checks have no charge until a final result.

## Wallet and billing

Your workspace wallet balance is read-only through the API. Manage funding in the MasterID app. A balance check is **not** a reservation: other workspace activity can change the balance before a verification is submitted. Ensure sufficient funds before you submit each check.

## End-to-end examples

The examples below read credentials from environment variables, fetch the catalog, request consent, submit a verification, and poll for the result.

```bash cURL walkthrough theme={null}
# 1. Fetch catalog
export MASTERID_API_KEY="<YOUR_API_KEY>"
export MASTERID_API_SECRET="<YOUR_API_SECRET>"

curl -s "https://masterid.me/api/public/v1/catalog" \
  -H "X-API-Key: $MASTERID_API_KEY" \
  -H "X-API-Secret: $MASTERID_API_SECRET" \
  -H "Content-Type: application/json" | jq .

# 2. Request consent wording
product="tin"
fields='{"tin":"<APPROVED_TIN>"}'

curl -s -X POST "https://masterid.me/api/public/v1/verifications/consent" \
  -H "X-API-Key: $MASTERID_API_KEY" \
  -H "X-API-Secret: $MASTERID_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "{\"product\":\"$product\",\"fields\":$fields}" | jq '.data.consentText'

# 3. Submit verification
consentText="<EXACT_CONFIRMED_TEXT>"
idempotencyKey="$(openssl rand -hex 16)"

curl -s -X POST "https://masterid.me/api/public/v1/verifications" \
  -H "X-API-Key: $MASTERID_API_KEY" \
  -H "X-API-Secret: $MASTERID_API_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $idempotencyKey" \
  -d "{\"product\":\"$product\",\"fields\":$fields,\"consentConfirmed\":true,\"consentText\":\"$consentText\"}" | jq '.data.sessionId'

# 4. Poll for result
curl -s "https://masterid.me/api/public/v1/verifications/<SESSION_ID>" \
  -H "X-API-Key: $MASTERID_API_KEY" \
  -H "X-API-Secret: $MASTERID_API_SECRET" \
  -H "Content-Type: application/json" | jq '.data.status'
```

```javascript Node.js (18+, native fetch, crypto) theme={null}
const crypto = require('node:crypto');

const API_KEY = process.env.MASTERID_API_KEY;
const API_SECRET = process.env.MASTERID_API_SECRET;
const BASE = 'https://masterid.me/api/public/v1';

async function masterid(path, opts = {}) {
  const res = await fetch(`${BASE}${path}`, {
    headers: {
      'X-API-Key': API_KEY,
      'X-API-Secret': API_SECRET,
      'Content-Type': 'application/json',
      ...(opts.headers || {}),
    },
    ...opts,
  });
  const body = await res.json().catch(() => null);
  return { status: res.status, body };
}

async function runVerification() {
  // 1. Fetch catalog
  const catalog = await masterid('/catalog');
  console.log('Catalog products count:', catalog.body?.data?.length);

  // 2. Request consent
  const product = 'tin';
  const fields = { tin: '<APPROVED_TIN>' };
  const consentRes = await masterid('/verifications/consent', {
    method: 'POST',
    body: JSON.stringify({ product, fields }),
  });
  const consentText = consentRes.body?.data?.consentText;
  if (!consentText) throw new Error('Consent text missing');

  // 3. Present for genuine confirmation, then submit
  const idempotencyKey = crypto.randomBytes(16).toString('hex');
  const subRes = await masterid('/verifications', {
    method: 'POST',
    headers: { 'Idempotency-Key': idempotencyKey },
    body: JSON.stringify({
      product,
      fields,
      consentConfirmed: true,
      consentText,
    }),
  });
  const sessionId = subRes.body?.data?.sessionId;
  if (!sessionId) throw new Error('Session ID missing');
  console.log('Submitted sessionId:', sessionId);

  // 4. Stop if completed immediately
  if (subRes.status === 200) {
    console.log('Completed immediately:', subRes.body);
    return subRes.body;
  }

  // 5. Poll for result (no more frequently than every 15 seconds)
  while (true) {
    await new Promise(r => setTimeout(r, 15_000));
    const poll = await masterid(`/verifications/${sessionId}`);
    const status = poll.body?.data?.status;
    console.log('Poll status:', status);
    if (status !== 'pending') {
      return poll.body;
    }
  }
}

runVerification().catch(console.error);
```

```python Python (requests, hmac) theme={null}
import os, time, json, uuid
import requests

API_KEY = os.environ["MASTERID_API_KEY"]
API_SECRET = os.environ["MASTERID_API_SECRET"]
BASE = "https://masterid.me/api/public/v1"

HEADERS = {
    "X-API-Key": API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
}

def masterid(method, path, extra_headers=None, json_body=None):
    h = {**HEADERS, **(extra_headers or {})}
    resp = requests.request(method, f"{BASE}{path}", headers=h, json=json_body, timeout=30)
    return resp.status_code, resp.json() if resp.text else None

def run_verification():
    # 1. Catalog
    status, catalog = masterid("GET", "/catalog")
    print("Catalog:", catalog and catalog.get("data"))

    # 2. Consent wording
    product = "tin"
    fields = {"tin": "<APPROVED_TIN>"}
    status, consent_resp = masterid("POST", "/verifications/consent", json_body={"product": product, "fields": fields})
    consent_text = consent_resp["data"]["consentText"] if consent_resp else ""
    if not consent_text:
        raise ValueError("Consent text missing")

    # 3. Submit with idempotency key
    idempotency_key = str(uuid.uuid4())
    status, sub_resp = masterid("POST", "/verifications", extra_headers={"Idempotency-Key": idempotency_key}, json_body={
        "product": product,
        "fields": fields,
        "consentConfirmed": True,
        "consentText": consent_text,
    })
    session_id = sub_resp["data"]["sessionId"] if sub_resp else ""
    if not session_id:
        raise ValueError("Session ID missing")

    # 4. Immediate completion
    if status == 200:
        return sub_resp

    # 5. Poll
    while True:
        time.sleep(15)
        status, poll = masterid("GET", f"/verifications/{session_id}")
        status_value = poll["data"]["status"] if poll else None
        print("Poll status:", status_value)
        if status_value != "pending":
            return poll

if __name__ == "__main__":
    print(run_verification())
```

## Sequence diagram

```mermaid theme={null}
sequenceDiagram
  participant Client as Your Mobile/Web Client
  participant Server as Your Backend
  participant MasterID as MasterID API
  Server->>MasterID: GET /catalog + auth headers
  MasterID-->>Server: Products, fields, prices
  Client->>Server: Identity data + authorisation
  Server->>MasterID: POST /verifications/consent
  MasterID-->>Server: consentText
  Server->>Client: Display consent for confirmation
  Client->>Server: Confirms consent
  Server->>Server: Store idempotency key
  Server->>MasterID: POST /verifications + Idempotency-Key
  MasterID-->>Server: 200 or 202 + sessionId
  alt 200 immediate
    MasterID-->>Server: data with status
  else 202 pending
    loop Poll every >=15s
      Server->>MasterID: GET /verifications/{sessionId}
      MasterID-->>Server: pending / final status
    end
  end
  alt Webhook path
    MasterID->>Server: POST verification.completed
    Server-->>MasterID: 2xx
  end
```

## Common pitfalls

> **Skipped or silently accepted consent**
>
> The `consentText` returned by `POST /verifications/consent` is specific to your account, product and subject fields. You must present it for genuine confirmation from the data subject. Submitting `consentConfirmed: true` without real confirmation violates the integration agreement.

> **Changed identity after consent**
>
> If any identity detail or the consent text changes, request fresh consent wording before submission. A mismatch between the submitted `fields`, `product` and the consent text may result in a rejection or failed check.

> **Polling too frequently**
>
> If the response is `202 pending`, wait at least 15 seconds between polls. Excessive polling wastes resources and can trigger rate limiting.

> **Insufficient wallet balance**
>
> A balance check is not a reservation. Verify your balance before each submission and keep a buffer, because other workspace activity or charges can change the balance in between calls.

> **Duplicate checks for the same subject**
>
> Reusing the same `Idempotency-Key` with identical parameters returns the saved outcome and does not start a second operation. Changing the body under a used key can return an idempotency conflict. If an original request is interrupted, use the saved session reference rather than creating a new key.

> **Mixing up session and request IDs**
>
> The API request `id` is returned in every response envelope. The `sessionId` (6-12 digits) identifies the verification and is what you poll. Store the `sessionId` from the submission response.


## Related topics

- [Receive webhooks](https://docs.masterid.co.ug/guides/webhooks.md)
- [MasterID Integration API](https://docs.masterid.co.ug/index.md)
- [Collect mobile money payments](https://docs.masterid.co.ug/guides/payment-collection.md)
- [POST /verifications](https://docs.masterid.co.ug/api-reference/verifications/create-verification.md)
- [POST /verifications/consent](https://docs.masterid.co.ug/api-reference/verifications/request-consent.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.