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.
2. Get a connection link
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.