API reference
Every endpoint, what authenticates it, and what a paid plan is needed for. Base URL is
https://api.shinid.com.
Auth column: apikey = integration Secret Key in the apikey header · session =
Authorization: Bearer from a dashboard sign-in · none = open, scoped by an unguessable
ID · admin = internal, not available to customers.
Endpoints marked Paid plan or Premium+ check the tier of the acting
organization — named by a current-identity header, defaulting to the organization you
have belonged to longest. See Authentication.
Verifications
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /verifications | session | Paginated list |
POST | /verifications | session | Paid plan, unless "sandbox": true. Create a verification request |
GET | /verifications/{id} | session | One verification request |
PUT | /verifications/{id} | session | Update. Owner only |
DELETE | /verifications/{id} | session | Owner only |
GET | /verifications/{id}/individuals | session | Paginated runs of one verification |
GET | /verifications/{id}/individuals/{customer} | apikey or session | Result for one customer_id. The call to decide on |
GET | /verifications/individuals/{id} | session | One run by its own ID |
POST | /verifications/individuals | none | Open a run. Idempotent per (customer_id, verification_id) |
GET | /verifications/{individual_id}/connect | none | Connection URL for the QR code. Reused for 2 minutes |
GET | /verifications/{individual_id}/callback | none | Called by the wallet. Not for you |
GET | /verifications/{individual_id}/verify | none | Current status; settles the presentation |
POST | /verifications/{individual_id}/simulate | apikey or session | Sandbox only. Settle a test individual through the real validator. 403 NOT_SANDBOX on a live verification |
Credentials
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /credentials | session | Paginated list |
GET | /credentials/{id} | session | |
POST | /credentials | session | Paid plan. Emails a claim link if the recipient has an address |
POST | /credentials/with-recipient | session | Paid plan. Creates the recipient too |
POST | /credentials/import | session | Paid plan. Multipart: file (CSV) + schema_id |
GET | /credentials/import/{id} | session | Import progress |
GET | /credentials/import/download-sample/{schema_id} | none | Schema-shaped CSV template |
POST | /credentials/notify | session | Claim email for a list of credentials |
POST | /credentials/notify/via-schema | session | Claim email for every credential on a schema |
PUT | /credentials/{id} | session | |
PATCH | /credentials/{id}/revoke | session | Owner only |
PATCH | /credentials/revoke | session | Bulk, asynchronous |
DELETE | /credentials/{id} | session | |
POST | /credentials/delete | session | Bulk delete |
GET | /credentials/{id}/connect | none | Claim connection URL. Reused for 2 minutes |
GET | /credentials/{id}/callback | none | Called by the wallet |
Schemas and recipients
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /schemas | session | Paginated |
GET | /schemas/{id} | session | |
POST | /schemas | session | Publish a schema |
DELETE | /schemas/{id} | session | |
GET | /recipients | apikey or session | ?q= searches |
GET | /recipients/{id} | apikey or session | |
POST | /recipients | apikey or session | |
PUT | /recipients/{id} | apikey or session | |
DELETE | /recipients/{id} | apikey or session |
Organizations, plan and billing
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /organizations | session | Organizations you belong to |
GET | /organizations/{id} | session | |
POST | /organizations | session | |
PUT | /organizations/{id} | session | Members only |
GET | /organizations/{id}/usage | session | Plan, quota, usage, 12-month history |
GET | /organizations/{id}/billing/subscription | session | null where the tier was granted by hand |
POST | /organizations/{id}/billing/checkout | session | Buy Standard. Returns a Stripe Checkout URL; the webhook applies the tier |
POST | /organizations/{id}/billing/portal | session | Stripe portal. 409 if there is no billing account |
Organization codes and vouchers
| Method | Path | Auth | Notes |
|---|---|---|---|
POST | /organizations/{id}/codes | session | Paid plan. Mint a code. Types narrowed to the plan — identity always covered, phone/address Premium+ |
GET | /organizations/{id}/codes | session | List with redemption counts |
DELETE | /organizations/{id}/codes/{codeId} | session | Revoke |
POST | /vouchers | session | Premium+. Mint a grant directly |
POST | /vouchers/exchange | none | Wallet exchanges a typed code. 200/hour/IP |
POST | /vouchers/redeem | none | Service-to-service. The signature is the authorization |
Policies
A multi-requirement policy engine — several credentials, per-requirement reasons — is in the API but not yet wired end to end: the evaluation step is restricted to internal calls, so a policy request cannot currently be driven to a verdict by a customer integration. Documented here so you know it exists. Use verification requests for anything you are shipping now.
| Method | Path | Auth | Notes |
|---|---|---|---|
POST | /policies | session | Premium+ |
GET | /policies/{id} | session | Members of the owning organization only |
POST | /policy-requests | apikey or session | Returns a deep-link token |
GET | /policy-requests/{token}/definition | none | Token-scoped, no personal data |
GET | /policy-requests/{id}/status | apikey or session | Verdict with per-requirement reasons |
POST | /policy-requests/{id}/evaluate | admin | Internal |
Other
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /ping | none | Health check. Returns {"message":"pong"} |
GET | /integrations/keys | session | Your API keys |
POST | /integrations/keys | session | Paid plan. Mint a key |
PUT | /integrations/keys/{id} | session | Rename |
DELETE | /integrations/keys/{id} | session | Revoke |
POST | /kyb/{org_id} | session | Submit KYB documents |
GET | /kyb, /kyb/{id} | session | KYB status |
POST | /media/upload | session | Multipart upload |
GET | /users | session | Your profile |
PUT | /users/profile | session |
Pagination
List endpoints take page (default 1) and limit (default 10, maximum 100 — values above
100 fall back to 10, they do not error). Filter with filter.<field>=<value>. Responses
are { "results": [...], "total": n }.
Worked example — verify-demo
verify-demo.socious.io is a small open integration that runs this API end to end: a membership vote where casting a ballot requires verified credentials. Two pieces of it are worth copying into any integration.
Send people to get verified — one link, without making them type your organization code
Every wallet verification must be covered by an
organization code, but nobody has to transcribe one:
carry it on the wallet link as ?code= and the wallet redeems it itself on arrival. The
code survives onboarding, too — a member who opens the link with no wallet yet still has
it applied once their wallet exists. And the same link can carry the presentation: the
member’s connection link with the code and a tier appended walks them through whatever
checks they are missing and presents to your verification in one trip. This is
verify-demo’s own link builder, verbatim:
/**
* The ONE-LINK member journey — exactly the link an organization
* hands its members: the per-member connection link with the org code and the
* KYC + phone tier appended. shin-api's shortener forwards these params, so the
* wallet lands on /connect carrying them: a new member is walked through
* onboarding and the KYC + phone run, a member with KYC gets phone-only, a
* fully-verified member goes straight to presenting — then the stored proof
* request resumes and the result lands back on this verification. One link, no
* second trip.
*/
export function walletOneLink(env: Env, connectionUrl: string, scenario?: string): string {
const url = new URL(connectionUrl);
if (env.WALLET_ORG_CODE) url.searchParams.set('code', env.WALLET_ORG_CODE);
url.searchParams.set('tier', 'kyc_phone');
if (scenario) url.searchParams.set('sandbox_scenario', scenario);
return url.toString();
}
connectionUrl is the link from GET /verifications/{individual_id}/connect, so the
member clicks something like
https://api.shinid.com/qjrNTl0k?code=SHN-7K4M-9XQ2TV&tier=kyc_phone
and is never asked for the code. The demo adds sandbox_scenario because it always runs
canned sandbox outcomes against a TEST- code; a live integration drops that parameter
and puts a live code on the link. The code is a bounded instrument, not a secret — cap it
with max_redemptions and expires_at when you mint it, and revoke it at will — so
putting it in a link (or on a portal page) is the intended distribution, not a leak.
The three calls that decide access
Server-side only — the Secret Key must never reach the browser. customer_id is any
stable identifier from your system.
const headers = { apikey: SECRET_KEY, 'Content-Type': 'application/json' };
// 1. Create a run for one of your users. Idempotent per
// (customer_id, verification_id) — safe to call on every page load.
const individual = await post('https://api.shinid.com/verifications/individuals', {
verification_id: VERIFICATION_ID,
customer_id: member.id,
});
// 2. Connection link to show them — QR code on desktop, open-in-app on mobile.
// No key: an end-user endpoint. Links are reused for 2 minutes.
const { connection_url } = await get(
`https://api.shinid.com/verifications/${individual.id}/connect`,
);
// 3. The result to act on — keyed by YOUR customer id, Secret-Key-authenticated.
const result = await get(
`https://api.shinid.com/verifications/${VERIFICATION_ID}/individuals/${member.id}`,
{ headers },
);
// Grant access only when result.status === 'VERIFIED'.
Re-run step 3 at the moment of the guarded action, server-side, rather than trusting
that the user only saw an enabled button — hiding a button is not access control, and
verify-demo’s ballot endpoint exists to demonstrate exactly that refusal. For counting,
GET /verifications/{id}/individuals pages through every run of your verification with
per-run status, and GET /organizations/{id}/codes reports redemption_count per code.
A note on /docs
api.shinid.com/docs and api.shinid.com/swagger.yaml now redirect here. They used to
serve an OpenAPI page describing eleven of these routes, last updated in August 2025, and
several of the shapes it documented had since changed — so anyone who found it by guessing
got worse information than this page. These pages are the reference.