TrustChain APITestnet

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.

This is the testnet. Use test wallets only, never real people's details. Credentials signed here are not valid on mainnet. Sign-up is by invitation: ask TrustChain for an organisation on testnet.
  1. Get started
  2. Tokens
  3. Draft a credential
  4. Batches
  5. Wallet links
  6. Ask for proof
  7. Webhooks
  8. Errors, retries and limits
  9. Testnet and mainnet
  10. What never goes through the API

Get started

  1. 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.
  2. Your system gets a token, then calls the API with Authorization: Bearer <token>.
  3. 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.

ScopeLets the clientFor
templates:readRead credential templatesIssuers
credentials:draftDraft credentials for people to approveIssuers
requests:readRead the status of drafts and signed credentialsIssuers
connections:writeInvite people to link their walletIssuers
batches:writeUpload batches for people to approveIssuers
batches:readRead batch progress and resultsIssuers
proofs:writeAsk people for proof, from approved templatesVerifiers
proofs:readRead proof resultsVerifiers
webhooks:manageManage webhook endpointsAny

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'));
}
EventWhen
request.submittedA draft went for approval
request.approvedAn approval was given
request.rejectedA draft was rejected
credential.signedA credential was signed and sent to the wallet
credential.revokedA credential was revoked
connection.acceptedA person linked their wallet
connection.declinedA person declined to link their wallet
batch.validatedA batch was checked
batch.completedA batch ended: signed, rejected, cancelled or expired
proof.completedA person answered a proof request
pingA test message

Messages carry references and statuses only, never claim values or anything a person revealed.

Errors, retries and limits

Testnet and mainnet

TestnetMainnet
Addresshttps://sandbox-api.ssi-trustchain.comhttps://api.ssi-trustchain.com (later)
DataTest wallets and fictional people onlyReal people
Client sign-inKey pair, or secretKey pair only
Rate limit300 a minute1200 a minute
SandboxPOST /sandbox/wallets makes a test wallet; POST /sandbox/proof-requests/{id}/decline declines as oneNone

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