Direct Onboarding via the API
Direct onboarding adds an organisation to a Trust List in a single API call, without the
Verifiable Credential based flow (onboardingInit / onboardingStatus). The Trust List
owner collects the applicant's organisation details and public key material out-of-band
and creates the Trust List Record (TLR) with tlrCreate.
This is the programmatic counterpart of the direct onboarding in the web console. It is useful when you want to onboard many organisations, integrate onboarding into your own back office, or automate tests.
How it works
Applicant Trust List owner (your code) Trust List API
│ CSR / did:key / X.509 cert │ │
│ + organisation details ───────▶│ │
│ │── client_credentials ──▶ Auth0 (access token) │
│ │── POST /{tl_id}/tlrs ───────────────────────▶│
│ │ │── issue X.509 (CSR only)
│ │ │── add entity to ETSI XML
│ │ │── create did:web + DID Document
│ │◀────────────── { did, x509Certificate } ─────│
│◀──── did:web + certificate ──────│ │
| Step | SDK method | HTTP endpoint | Auth |
|---|---|---|---|
| Find the Trust List | tlListByOwner / tlGet | GET /tl/owner, GET /{tl_id} | owner |
| Create the record | tlrCreate | POST /{tl_id}/tlrs | owner |
| Read the record | tlrGet, tlrsGet | GET /{tl_id}/{tlr_id}, GET /{tl_id}/tlrs | public |
| Resolve the DID | didGet | GET /{tl_id}/{tlr_id}/did.json | public |
| Check the ETSI XML | etsiTlGet | GET /{tl_id}/etsi/tl.xml | public |
| Revoke the key | didVerificationMethodPatch | PATCH /{tl_id}/dids/{tlr_id}/verificationMethod | owner |
| Remove the record | tlrDelete | DELETE /{tl_id}/{tlr_id} | owner |
Only the owner of the Trust List can create records in it. The API looks the Trust
List up under the identity in the access token; for any other caller tlrCreate fails
with 400 Bad Request.
Prerequisites
- A Trust List that you own (see Trust List Creation).
- API credentials for that owner: an OAuth 2.0 client ID and client secret of an Auth0 machine-to-machine application linked to your account.
- Node.js ≥ 18 and the SDK:
npm install @idunion/tl-sdk
- From the applicant: the organisation details and one of the following as key material:
| Key material | Payload field | What the API does |
|---|---|---|
| Certificate Signing Request (PEM) | csr | Issues an X.509 certificate (valid for 24 months) with the Trust List's governance wallet, adds the organisation to the ETSI XML and returns the certificate. |
| X.509 certificate (PEM) | x509RootCert | Adds the organisation with the supplied certificate to the ETSI XML as-is. |
did:key with a P-256 key (p256-pub or jwk_jcs-pub multicodec) | didKey | Creates the did:web DID Document only. The organisation is not added to the ETSI XML. |
If several are set, didKey wins over csr, and csr over x509RootCert. To have the
organisation listed in the ETSI TS 119 602/612 XML, send a csr or an x509RootCert.
The applicant can generate a CSR with OpenSSL as described in Generate a Certificate Signing Request. The private key stays with the applicant; only the CSR is sent to you.
1. Create an authenticated client
The Client class wraps DefaultApi and gets an access token from Auth0 with the
OAuth 2.0 client credentials grant. The token is attached to every request that requires
authentication.
import { Client } from "@idunion/tl-sdk";
const tlClient = new Client(
process.env.TL_TOKEN_URL!, // https://idunion-dev.eu.auth0.com/oauth/token
process.env.TL_AUDIENCE!, // https://tl-api.dev.idunion.info
process.env.TL_CLIENT_ID!,
process.env.TL_CLIENT_SECRET!,
process.env.TL_API_URL!, // https://tl-api.dev.idunion.info/api/v1
);
The values shown are for the IDunion development environment.
Keep the client secret out of source code. Load it from an environment variable or a secrets manager. Anyone holding it can manage your Trust Lists.
Client requests the token once, when it is constructed. Long-running processes should
create a new Client before the token expires.
If you already have an access token, for example from your own OAuth flow, use
DefaultApi directly:
import { DefaultApi } from "@idunion/tl-sdk";
const tlClient = new DefaultApi(
{ accessToken: process.env.TL_ACCESS_TOKEN, isJsonMime: () => true },
process.env.TL_API_URL,
);
2. Find the Trust List
tlrCreate needs the ID of the Trust List. List the Trust Lists you own:
const { data: tls } = await tlClient.tlListByOwner();
const ownTls = tls.items ?? [];
for (const tl of ownTls) {
console.log(tl.id, tl.name, tl.type);
}
const tlId = ownTls.find((tl) => tl.name === "My Wallet Provider Trust List")!.id;
3. Create the Trust List Record
With a CSR (recommended)
import { readFile } from "node:fs/promises";
import { TlrCreatePayload } from "@idunion/tl-sdk";
// organisation details (all required)
const orgDetails = {
orgName: "Example GmbH",
orgTradeName: "Example",
orgStreetAddress: "Musterstraße 1",
orgCity: "Berlin",
orgState: "Berlin",
orgPostalCode: "10115",
orgCountry: "DE", // ISO 3166-1 alpha-2
orgEmail: "contact@example.com",
orgPhone: "+49 30 123456",
orgWebAddress: "https://example.com",
orgToC: "https://example.com/terms",
};
const payload: TlrCreatePayload = {
csr: await readFile("request.csr", "utf8"),
...orgDetails,
// wallet solution details (Wallet Provider Trust Lists only)
walletSolutionName: "Example Wallet",
walletSolutionRefId: "wallet-ref-001",
walletSolutionUrl: "https://wallet.example.com",
};
const { data: created } = await tlClient.tlrCreate(tlId, payload);
console.log("DID:", created.did);
// did:web:tl-api.dev.idunion.info:api:v1:<tl_id>:<tlr_id>
console.log("Certificate:\n", created.x509Certificate);
// -----BEGIN CERTIFICATE----- ...
Return did and x509Certificate to the applicant. They use the certificate together
with their private key, and the did:web as their identifier in the ecosystem.
With an existing X.509 certificate
Use this if the applicant already holds a certificate, for example one issued by their own CA:
const payload: TlrCreatePayload = {
x509RootCert: await readFile("certificate.pem", "utf8"),
...orgDetails,
};
const { data: created } = await tlClient.tlrCreate(tlId, payload);
The response's x509Certificate contains the certificate you sent.
With a did:key
Use this if the applicant only needs a did:web DID Document, without an entry in the
ETSI XML:
const payload: TlrCreatePayload = {
didKey: "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
...orgDetails,
};
const { data: created } = await tlClient.tlrCreate(tlId, payload);
4. Verify the result
The record ID (tlr_id) is the last segment of the returned DID:
const tlrId = created.did.split(":").pop()!;
The record, its DID Document and the ETSI XML are public. Verifiers need no credentials to read them.
// Trust List Record: status is "onboarded" right after direct onboarding
const { data: tlr } = await tlClient.tlrGet(tlId, tlrId);
console.log(tlr.orgName, tlr.status);
// DID Document, also resolvable by any did:web resolver at
// https://tl-api.dev.idunion.info/api/v1/<tl_id>/<tlr_id>/did.json
const { data: didDoc } = await tlClient.didGet(tlId, tlrId);
console.log(didDoc.verificationMethod);
// ETSI TS 119 602/612 Trust List, now including the organisation (CSR / X.509 only)
const { data: xml } = await tlClient.etsiTlGet(tlId, { responseType: "text" });
console.log(String(xml).includes("Example GmbH"));
5. Revoke or remove the organisation
Revoke the key while keeping the record. The record's status changes to revoked:
const keyId = didDoc.verificationMethod![0].id;
await tlClient.didVerificationMethodPatch(tlId, tlrId, {
id: keyId,
revoked: new Date().toISOString(),
});
Delete the record. This removes the organisation from the ETSI XML, re-signs the list and deletes the DID Document:
await tlClient.tlrDelete(tlId, tlrId);
Complete example
import { readFile } from "node:fs/promises";
import { isAxiosError } from "axios";
import { Client, TlrCreatePayload } from "@idunion/tl-sdk";
async function onboard(tlId: string, csrPath: string): Promise<void> {
const tlClient = new Client(
process.env.TL_TOKEN_URL!,
process.env.TL_AUDIENCE!,
process.env.TL_CLIENT_ID!,
process.env.TL_CLIENT_SECRET!,
process.env.TL_API_URL!,
);
const payload: TlrCreatePayload = {
csr: await readFile(csrPath, "utf8"),
orgName: "Example GmbH",
orgTradeName: "Example",
orgStreetAddress: "Musterstraße 1",
orgCity: "Berlin",
orgState: "Berlin",
orgPostalCode: "10115",
orgCountry: "DE",
orgEmail: "contact@example.com",
orgPhone: "+49 30 123456",
orgWebAddress: "https://example.com",
orgToC: "https://example.com/terms",
};
try {
const { data: created } = await tlClient.tlrCreate(tlId, payload);
const tlrId = created.did.split(":").pop()!;
const { data: tlr } = await tlClient.tlrGet(tlId, tlrId);
console.log(`Onboarded ${tlr.orgName} as ${created.did} (${tlr.status})`);
} catch (err) {
if (isAxiosError(err)) {
// 400: invalid payload, unreadable key material, or caller is not the TL owner
// 401: missing or expired access token
console.error(err.response?.status, err.response?.data);
}
throw err;
}
}
await onboard(process.argv[2], process.argv[3]);
Run it with:
export TL_TOKEN_URL=https://idunion-dev.eu.auth0.com/oauth/token
export TL_AUDIENCE=https://tl-api.dev.idunion.info
export TL_API_URL=https://tl-api.dev.idunion.info/api/v1
export TL_CLIENT_ID=...
export TL_CLIENT_SECRET=...
npx tsx onboard.ts <tl_id> request.csr
Troubleshooting
| Response | Likely cause |
|---|---|
401 Unauthorized | No access token, or it has expired. Check the client ID/secret, token URL and audience, and create a new Client. |
400 failed to get TL ... | The Trust List does not exist or is not owned by the authenticated client. |
400 no key material provided | None of csr, x509RootCert or didKey is set. |
400 failed to issue certificate | The CSR could not be signed by the governance wallet. Check that it is a valid PEM CSR (openssl req -text -noout -verify -in request.csr). |
400 did:key is expected to contain 3 parts | The didKey is not of the form did:key:<multibase key>. |