Connect your systems to TrustChain
Your student records, HR or customer systems can draft credentials, upload batches, invite people to link their wallets, ask for proofs and hear what happened. People still approve, sign and revoke, in your Issuer Console or Verifier Portal: no API client can.
- Get started
- Tokens
- Draft a credential
- Batches
- Wallet links
- Ask for proof
- Webhooks
- Errors, retries and limits
- Testnet and mainnet
- What never goes through the API
Get started
- An administrator of your organisation opens Integrations in the Issuer Console or Verifier Portal and adds a client for your system, choosing what it may do.
- Your system gets a token, then calls the API with
Authorization: Bearer <token>. - Add a webhook endpoint so you hear about approvals and results instead of asking.
Base address: https://sandbox-api.ssi-trustchain.com/api/v1. Machine-readable description: openapi.json (OpenAPI 3.1). Health: /health. Status: status page.
| Scope | Lets the client | For |
|---|---|---|
templates:read | Read credential templates | Issuers |
credentials:draft | Draft credentials for people to approve | Issuers |
requests:read | Read the status of drafts and signed credentials | Issuers |
connections:write | Invite people to link their wallet | Issuers |
batches:write | Upload batches for people to approve | Issuers |
batches:read | Read batch progress and results | Issuers |
proofs:write | Ask people for proof, from approved templates | Verifiers |
proofs:read | Read proof results | Verifiers |
webhooks:manage | Manage webhook endpoints | Any |
Tokens
OAuth 2.0 client credentials. Tokens last 15 minutes: keep one and reuse it until it expires. Requests come only from the addresses you allowed, if you set any.
With a key pair (recommended; the only way on mainnet)
Make an Ed25519 key pair and give TrustChain the public half (in Integrations). For each token, sign a short JWT with the private key (RFC 7523, private_key_jwt): iss and sub your client ID, aud exactly https://sandbox-api.ssi-trustchain.com/api/v1/oauth/token, exp at most 5 minutes ahead, and a jti you never reuse.
// Node 18+: make the key pair once
import { generateKeyPairSync, sign, randomUUID } from 'node:crypto';
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
console.log(JSON.stringify(publicKey.export({ format: 'jwk' }))); // paste this in Integrations
// keep privateKey in your secret store
// For each token
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
const now = Math.floor(Date.now() / 1000);
const input = b64({ alg: 'EdDSA', typ: 'JWT' }) + '.' + b64({ iss: 'CL-0001', sub: 'CL-0001', aud: 'https://sandbox-api.ssi-trustchain.com/api/v1/oauth/token', iat: now, exp: now + 60, jti: randomUUID() });
const assertion = input + '.' + sign(null, Buffer.from(input), privateKey).toString('base64url');
const res = await fetch('https://sandbox-api.ssi-trustchain.com/api/v1/oauth/token', { method: 'POST', body: new URLSearchParams({
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: assertion,
}) });
const { access_token } = await res.json();
With a client secret (testnet only)
curl -s https://sandbox-api.ssi-trustchain.com/api/v1/oauth/token -d grant_type=client_credentials -d client_id=CL-0001 -d client_secret=tc_sk_test_…
Draft a credential
List your templates, then draft. The draft goes to the people who approve that credential; you hear the result by webhook (request.approved, credential.signed, request.rejected) or by reading the request. Identity credentials are never drafted through the API: an officer checks the person in person.
curl -s https://sandbox-api.ssi-trustchain.com/api/v1/templates -H "Authorization: Bearer $TOKEN"
curl -s https://sandbox-api.ssi-trustchain.com/api/v1/credentials -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Idempotency-Key: enrol-2026-0001" \
-d '{"template":"TPL-…","holder_ref":"HX-123456","reference":"STU-2026-0042",
"claims":{"Programme":"BSc Computer Science","Student number":"S-1001"}}'
holder_ref is the reference the person's wallet shows (HX- and digits). reference is your own record number, so you can match the result: never a name or ID number.
Batches
Many credentials of one template at once: up to 10,000 rows (8 MB), as JSON or CSV. Every row is checked first; if any row has a problem, nothing goes forward and each problem is listed by row. Each approver then checks a random sample of rows against your records before approving; when the last approves, TrustChain signs every row in the background (about 25 a second) and sends each person their credential.
holder_ref,connection,reference,Programme,Final grade HX-123456,,TR-1,"BSc Computer Science, Honours",A ,STU-2026-0042,TR-2,BA Economics,B
curl -s "https://sandbox-api.ssi-trustchain.com/api/v1/batches?template=TPL-…&name=December%202026%20transcripts&reference=TR-2026-12" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: text/csv" -H "Idempotency-Key: transcripts-2026-12" \ --data-binary @transcripts.csv curl -s https://sandbox-api.ssi-trustchain.com/api/v1/batches/BT-0007 -H "Authorization: Bearer $TOKEN" # progress curl -s "https://sandbox-api.ssi-trustchain.com/api/v1/batches/BT-0007/results?after=0&limit=1000" -H "Authorization: Bearer $TOKEN" # row → credential
You hear batch.validated after the check and batch.completed when it ends (signed, rejected, cancelled, or expired after 30 days without approval). The rows are encrypted while they wait and deleted when the batch ends.
Wallet links
Invite a person to link their wallet to your record of them. You send them the link yourself (TrustChain never contacts citizens); they see who asks and why, and accept or decline. You then name them by your record number in batches (connection column), and learn only their holder reference.
curl -s https://sandbox-api.ssi-trustchain.com/api/v1/connections -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: link-stu-2026-0042" -d '{"reference":"STU-2026-0042","purpose":"Receive your degree in your wallet"}'
Ask for proof
For verifying organisations: ask with one of your network-reviewed proof templates, show the returned wallet_url as a QR code (or send it), and read the outcome. What the person chose to reveal is included in the result for 24 hours, then deleted; webhooks carry the outcome and receipt only. Re-checks are free.
curl -s https://sandbox-api.ssi-trustchain.com/api/v1/proof-requests -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: kyc-8812" -d '{"template":"PT-…"}'
curl -s https://sandbox-api.ssi-trustchain.com/api/v1/proof-requests/PR-00042 -H "Authorization: Bearer $TOKEN"
Webhooks
Add an endpoint (public HTTPS only) in Integrations or with POST /webhooks, and keep its signing secret. Every message carries TrustChain-Event, TrustChain-Delivery and TrustChain-Signature: t=<seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t + "." + body. Check it, refuse anything older than 5 minutes, and answer 2xx within 10 seconds. Failures are retried after 1, 5 and 30 minutes, then 2, 6 and 12 hours; after 50 failures in a row the endpoint is paused until you resume it.
import { createHmac, timingSafeEqual } from 'node:crypto';
function fromTrustChain(secret, header, rawBody) {
const t = /t=(\d+)/.exec(header)?.[1], v1 = /v1=([0-9a-f]{64})/.exec(header)?.[1];
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = createHmac('sha256', secret).update(t + '.' + rawBody).digest();
return timingSafeEqual(want, Buffer.from(v1, 'hex'));
}
| Event | When |
|---|---|
request.submitted | A draft went for approval |
request.approved | An approval was given |
request.rejected | A draft was rejected |
credential.signed | A credential was signed and sent to the wallet |
credential.revoked | A credential was revoked |
connection.accepted | A person linked their wallet |
connection.declined | A person declined to link their wallet |
batch.validated | A batch was checked |
batch.completed | A batch ended: signed, rejected, cancelled or expired |
proof.completed | A person answered a proof request |
ping | A test message |
Messages carry references and statuses only, never claim values or anything a person revealed.
Errors, retries and limits
- Errors are RFC 9457 problem details (
application/problem+json) with a plaindetail. A refused rule is 422, a missing scope 403, an expired token 401. - Every write needs an
Idempotency-Key. Retrying with the same key and body gives the first answer (Idempotent-Replayed: true); kept 24 hours. Retry on network errors, 429 and 5xx, with the same key. - Each client may make 300 requests a minute on testnet. Headers
RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetsay where you are; 429 comes withRetry-After. - Your administrators see each client's live health (calls, success rate, response times, errors) in Integrations.
Testnet and mainnet
| Testnet | Mainnet | |
|---|---|---|
| Address | https://sandbox-api.ssi-trustchain.com | https://api.ssi-trustchain.com (later) |
| Data | Test wallets and fictional people only | Real people |
| Client sign-in | Key pair, or secret | Key pair only |
| Rate limit | 300 a minute | 1200 a minute |
| Sandbox | POST /sandbox/wallets makes a test wallet; POST /sandbox/proof-requests/{id}/decline declines as one | None |
The two networks share nothing: separate ledgers, keys, accounts and clients. A credential signed on testnet is not valid on mainnet.
What never goes through the API
- Approving, signing or revoking: people do these in the consoles.
- Identity credentials: an officer checks each person in person.
- Names, ID numbers and dates of birth: use holder references and your own record numbers.
- Messages to citizens: you contact your people yourself.