Socious
Reference

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

MethodPathAuthNotes
GET/verificationssessionPaginated list
POST/verificationssessionPaid plan, unless "sandbox": true. Create a verification request
GET/verifications/{id}sessionOne verification request
PUT/verifications/{id}sessionUpdate. Owner only
DELETE/verifications/{id}sessionOwner only
GET/verifications/{id}/individualssessionPaginated runs of one verification
GET/verifications/{id}/individuals/{customer}apikey or sessionResult for one customer_id. The call to decide on
GET/verifications/individuals/{id}sessionOne run by its own ID
POST/verifications/individualsnoneOpen a run. Idempotent per (customer_id, verification_id)
GET/verifications/{individual_id}/connectnoneConnection URL for the QR code. Reused for 2 minutes
GET/verifications/{individual_id}/callbacknoneCalled by the wallet. Not for you
GET/verifications/{individual_id}/verifynoneCurrent status; settles the presentation
POST/verifications/{individual_id}/simulateapikey or sessionSandbox only. Settle a test individual through the real validator. 403 NOT_SANDBOX on a live verification

Credentials

MethodPathAuthNotes
GET/credentialssessionPaginated list
GET/credentials/{id}session
POST/credentialssessionPaid plan. Emails a claim link if the recipient has an address
POST/credentials/with-recipientsessionPaid plan. Creates the recipient too
POST/credentials/importsessionPaid plan. Multipart: file (CSV) + schema_id
GET/credentials/import/{id}sessionImport progress
GET/credentials/import/download-sample/{schema_id}noneSchema-shaped CSV template
POST/credentials/notifysessionClaim email for a list of credentials
POST/credentials/notify/via-schemasessionClaim email for every credential on a schema
PUT/credentials/{id}session
PATCH/credentials/{id}/revokesessionOwner only
PATCH/credentials/revokesessionBulk, asynchronous
DELETE/credentials/{id}session
POST/credentials/deletesessionBulk delete
GET/credentials/{id}/connectnoneClaim connection URL. Reused for 2 minutes
GET/credentials/{id}/callbacknoneCalled by the wallet

Schemas and recipients

MethodPathAuthNotes
GET/schemassessionPaginated
GET/schemas/{id}session
POST/schemassessionPublish a schema
DELETE/schemas/{id}session
GET/recipientsapikey or session?q= searches
GET/recipients/{id}apikey or session
POST/recipientsapikey or session
PUT/recipients/{id}apikey or session
DELETE/recipients/{id}apikey or session

Organizations, plan and billing

MethodPathAuthNotes
GET/organizationssessionOrganizations you belong to
GET/organizations/{id}session
POST/organizationssession
PUT/organizations/{id}sessionMembers only
GET/organizations/{id}/usagesessionPlan, quota, usage, 12-month history
GET/organizations/{id}/billing/subscriptionsessionnull where the tier was granted by hand
POST/organizations/{id}/billing/checkoutsessionBuy Standard. Returns a Stripe Checkout URL; the webhook applies the tier
POST/organizations/{id}/billing/portalsessionStripe portal. 409 if there is no billing account

Organization codes and vouchers

MethodPathAuthNotes
POST/organizations/{id}/codessessionPaid plan. Mint a code. Types narrowed to the plan — identity always covered, phone/address Premium+
GET/organizations/{id}/codessessionList with redemption counts
DELETE/organizations/{id}/codes/{codeId}sessionRevoke
POST/voucherssessionPremium+. Mint a grant directly
POST/vouchers/exchangenoneWallet exchanges a typed code. 200/hour/IP
POST/vouchers/redeemnoneService-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.

MethodPathAuthNotes
POST/policiessessionPremium+
GET/policies/{id}sessionMembers of the owning organization only
POST/policy-requestsapikey or sessionReturns a deep-link token
GET/policy-requests/{token}/definitionnoneToken-scoped, no personal data
GET/policy-requests/{id}/statusapikey or sessionVerdict with per-requirement reasons
POST/policy-requests/{id}/evaluateadminInternal

Other

MethodPathAuthNotes
GET/pingnoneHealth check. Returns {"message":"pong"}
GET/integrations/keyssessionYour API keys
POST/integrations/keyssessionPaid plan. Mint a key
PUT/integrations/keys/{id}sessionRename
DELETE/integrations/keys/{id}sessionRevoke
POST/kyb/{org_id}sessionSubmit KYB documents
GET/kyb, /kyb/{id}sessionKYB status
POST/media/uploadsessionMultipart upload
GET/userssessionYour profile
PUT/users/profilesession

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.

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.