Skip to main content

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 ──────│ │
StepSDK methodHTTP endpointAuth
Find the Trust ListtlListByOwner / tlGetGET /tl/owner, GET /{tl_id}owner
Create the recordtlrCreatePOST /{tl_id}/tlrsowner
Read the recordtlrGet, tlrsGetGET /{tl_id}/{tlr_id}, GET /{tl_id}/tlrspublic
Resolve the DIDdidGetGET /{tl_id}/{tlr_id}/did.jsonpublic
Check the ETSI XMLetsiTlGetGET /{tl_id}/etsi/tl.xmlpublic
Revoke the keydidVerificationMethodPatchPATCH /{tl_id}/dids/{tlr_id}/verificationMethodowner
Remove the recordtlrDeleteDELETE /{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 materialPayload fieldWhat the API does
Certificate Signing Request (PEM)csrIssues 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)x509RootCertAdds 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)didKeyCreates 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​

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​

ResponseLikely cause
401 UnauthorizedNo 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 providedNone of csr, x509RootCert or didKey is set.
400 failed to issue certificateThe 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 partsThe didKey is not of the form did:key:<multibase key>.