Verify recipient identity

Every recipient can be required to prove who they are before the document opens — with a one-time code, a password, or a verified identity document. This guide sends an agreement behind an SMS code and then raises the requirement to a checked passport. It takes two calls.

Before you begin

  • Each verification method needs a right of its own, on top of the right to start a transaction: share_mail_otp, share_sms_otp, share_password, share_oauth_otp (which also covers Nafath) and share_kyc_otp. Asking for a method you do not hold the right for returns 403.

  • Sending to people outside your organization must be enabled for your account. Check settings.canShareWithExternalUsers in GET /me — when it is off, a recipient without a Circularo account is rejected with 403.

  • Identity checks need a provider. The KYC provider names you can use are the ones configured for your environment; the examples use identomat.

What verification asks of a recipient

Verification is set per recipient, and one recipient can be asked for exactly one thing. The methods differ in what the recipient has to produce:

  • Something they receive — a one-time code sent to their phone (sms) or their e-mail (email). The quickest to set up, and enough for most agreements.

  • Something they know — a password you agree with them through your own channel. Circularo never sends it for you.

  • Who they are — a sign-in with an identity provider (oauth), the Saudi national identity service (nafath), or a kyc check where a provider inspects a real identity document.

Whichever you pick, the recipient meets it the moment they open the document — from the e-mail invitation or from a link you handed them yourself.

Step 1: Send the document behind an SMS code

Configure the method on the recipient when you send:

POST /transactions
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "document": {
        "title": "Employment Agreement",
        "fileContentBase64": "JVBERi0xLjQKJeLjz9MK..."
    },
    "send": {
        "recipients": [
            {
                "recipient": "jane.doe@example.com",
                "purpose": "sign",
                "verification": {
                    "sms": {
                        "phone": "+442079460958"
                    }
                }
            }
        ],
        "signatureFields": [
            {
                "recipients": [
                    "jane.doe@example.com"
                ],
                "pages": [
                    1
                ],
                "position": {
                    "x": 0.15,
                    "y": 0.6,
                    "width": 0.25,
                    "height": 0.06
                }
            }
        ]
    }
}
HTTP/2 201

{
    "id": "Vt7RfKq2LmXe5ZwGyB4N",
    "recipients": [
        {
            "completedAt": null,
            "expiresAt": null,
            "id": "Gk5cPdM9XvT3qHnU7jWs",
            "isMandatory": true,
            "language": "en",
            "message": null,
            "order": null,
            "permissions": [
                "print",
                "viewMetadata",
                "annotateOnReject",
                "rejectWithoutComment",
                "uploadFiles"
            ],
            "purpose": "sign",
            "quorum": null,
            "recipient": "jane.doe@example.com",
            "reminder": null,
            "status": "pending",
            "turnStartedAt": "2026-05-11T09:30:00.000Z",
            "verification": {
                "email": null,
                "kyc": null,
                "nafath": null,
                "oauth": null,
                "password": null,
                "sms": {
                    "phone": "*****58"
                }
            }
        }
    ],
    ...
}

The recipient's verification reports the configured method with the phone number anonymized — Circularo keeps the number to send the code to, and never returns it. Every method the recipient was not asked for is null.

Step 2: Raise the requirement to a verified identity document

Verification can be changed while the recipient has not acted yet. For an agreement that warrants it, ask for a checked identity document instead:

PATCH /transactions/Vt7RfKq2LmXe5ZwGyB4N/recipients/Gk5cPdM9XvT3qHnU7jWs
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "verification": {
        "kyc": {
            "providers": [
                "identomat"
            ],
            "documentTypes": [
                "passport"
            ],
            "expectedData": {
                "documentNumber": "X1234567",
                "nationality": "GBR"
            }
        }
    }
}
HTTP/2 200

{
    "id": "Gk5cPdM9XvT3qHnU7jWs",
    "recipient": "jane.doe@example.com",
    "status": "pending",
    "verification": {
        "email": null,
        "kyc": {
            "documentTypes": [
                "passport"
            ],
            "factors": [
                "idVerification"
            ],
            "providers": [
                "identomat"
            ],
            "requiredMatches": [
                "documentNumber",
                "nationality"
            ]
        },
        "nafath": null,
        "oauth": null,
        "password": null,
        "sms": null
    },
    ...
}

The new method replaces the old one: kyc is now set and sms is back to null. documentTypes limits what the recipient may identify themselves with — a passport here, since the passport number is the number expectedData pins down. The response confirms which details must match under requiredMatches, and never repeats the values you sent.

Important

Configure verification before you hand out a direct link. Creating one freezes the recipient's verification, and a later change is refused — see Hand out signing links yourself.

Note

The outcome of an identity check is recorded in your organization's audit log: the provider, the session, the kind of identity document presented and which of the expected details matched.

Complete example

Sending behind a one-time code, and asking for an identity check when the agreement warrants it, as a Node.js script. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.

JavaScript
import fs from "node:fs/promises";

const BASE_URL = "https://sandbox.circularo.com/api/v1/public"; // your instance's base URL
const API_KEY = "YOUR_API_KEY";
const AUTH_HEADER = { "Authorization": `Bearer ${API_KEY}` };
const JSON_HEADERS = { ...AUTH_HEADER, "Content-Type": "application/json" };

const pdfBase64 = (await fs.readFile("employment-agreement.pdf")).toString("base64");
const contractValue = 250_000; // whatever your own process knows about the agreement

const createRes = await fetch(`${BASE_URL}/transactions`, {
    method: "POST",
    headers: JSON_HEADERS,
    body: JSON.stringify({
        document: {
            title: "Employment Agreement",
            fileContentBase64: pdfBase64
        },
        send: {
            recipients: [
                {
                    recipient: "jane.doe@example.com",
                    purpose: "sign",
                    verification: {
                        sms: {
                            phone: "+442079460958"
                        }
                    }
                }
            ],
            signatureFields: [
                {
                    recipients: [
                        "jane.doe@example.com"
                    ],
                    pages: [
                        1
                    ],
                    position: {
                        x: 0.15,
                        y: 0.6,
                        width: 0.25,
                        height: 0.06
                    }
                }
            ]
        }
    })
});
if (!createRes.ok) throw new Error(`Transaction failed: ${createRes.status}`); // error handling kept minimal for brevity

const transaction = await createRes.json();
const recipient = transaction.recipients[0];

if (contractValue > 100_000) {
    const upgraded = await fetch(`${BASE_URL}/transactions/${transaction.id}/recipients/${recipient.id}`, {
        method: "PATCH",
        headers: JSON_HEADERS,
        body: JSON.stringify({
            verification: {
                kyc: {
                    providers: [
                        "identomat"
                    ],
                    documentTypes: [
                        "passport"
                    ],
                    expectedData: {
                        documentNumber: "X1234567",
                        nationality: "GBR"
                    }
                }
            }
        })
    });
    console.log(`${recipient.recipient} must now pass:`, (await upgraded.json()).verification.kyc.factors);
}

Next steps