Socious
Verifying people

Verify a person — direct API

Use this when you want the connection QR code inside your own interface — your branding, your copy, your layout — instead of sending the person to a Socious page.

It is the same flow as the hosted page with the middle steps done by you: four calls instead of two.

1. Open a verification for one person

curl -X POST https://api.shinid.com/verifications/individuals \
  -H "Content-Type: application/json" \
  -d '{
    "verification_id": "VERIFICATION_ID",
    "customer_id": "CUSTOMER_ID"
  }'

Returns 201 with the individual record. Keep its id — called INDIVIDUAL_ID below.

This call is idempotent on (customer_id, verification_id): calling it again for the same person and the same verification returns the existing record rather than creating a second one. So it is safe to call on every page load, and you do not need to remember whether you have called it before.

It takes no authentication, so you can call it from the browser. Calling it from your backend is better: you then hold the individual ID and can decide what to expose.

curl https://api.shinid.com/verifications/INDIVIDUAL_ID/connect

The response is the individual record with connection_url populated. Render it as a QR code for desktop, or open it directly on mobile, where it hands off to the Socious Wallet.

A connection link is reused for 2 minutes. Call connect again after that to mint a fresh one — which is what a “refresh QR code” button should do. Once the person has connected and the request is at REQUESTED, calling connect returns the record unchanged rather than restarting the flow.

3. Wait

Nothing for you to do. When the wallet connects, Socious Verify sends the proof request to it and the person approves sharing. That callback is between the wallet and our API.

4. Poll for the result

curl https://api.shinid.com/verifications/INDIVIDUAL_ID/verify

Each call returns the current individual record, and settles it if the presentation has arrived. Poll until status is VERIFIED or FAILED:

async function waitForResult(individualId, { intervalMs = 3000, timeoutMs = 300000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(`https://api.shinid.com/verifications/${individualId}/verify`);
    const individual = await res.json();
    if (individual.status === 'VERIFIED' || individual.status === 'FAILED') return individual;
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  return null; // the person never finished — your call what that means
}

Give it a deadline. A person who closes the tab never reaches a terminal status, and a loop without a timeout will run forever.

5. Make the decision from your backend

GET /verifications/{id}/verify takes no authentication, which is what lets you call it from the browser — and also means anyone holding the individual ID can call it. Do not let a browser response be what grants access in your system. Confirm server-side:

curl https://api.shinid.com/verifications/VERIFICATION_ID/individuals/CUSTOMER_ID \
  -H "apikey: YOUR_SECRET_KEY"

The rule of thumb: poll from the browser to drive your UI, read with your key to change your state.

Reading the failure

On FAILED, validation_error says which check did not pass. Two shapes you will see:

  • A condition was not satisfied — the credential is genuine, its values do not match what you asked for.
  • The issuer was refused — the credential’s issuer is not in the verification’s trusted set. The message names the issuer it refused. See Verification requests.

Either way body carries what was shared and issuer_did carries who signed it, so you can log the specifics without a second call.