---
url: /guides/call-quarzo-from-a-broker-backend.md
---
# Call Quarzo from a Broker backend

Use this guide to authenticate a Broker backend and call Quarzo as a Broker Service Account.
For the other authentication methods, see [Authentication](/topics/authentication).

## Prerequisites

* Node.js 24 or later for the example.
* A Broker Administrator who can sign in to `broker-front` in your target environment. To accept the invitation and sign in, see [Sign in to Quarzo](/guides/sign-in-to-quarzo).
* The API base URL, ZITADEL issuer URL, and Quarzo project ID of your target environment. See [Environments](/topics/environments).

## 1. Get a Service Account and a Service Key

Ask the Broker Administrator to complete these steps in `broker-front`:

1. In the sidebar, open **Admin → Service Accounts**.
2. Select **Create a Service Account** and enter a display name for the backend.
3. For that account, select **Manage keys**.
4. Select **Generate a key** and enter a label that identifies the backend that holds the key.
5. Select **Download** before you close the key dialog.
6. Give the file to the backend operator through a secure channel, for example a secrets manager. Do not send it by email or chat.

The download has the name `<label>-<keyId>.json`. Quarzo provides the private key only once and does not store it.
The file contains these fields:

```json
{
  "type": "serviceaccount",
  "keyId": "<key-id>",
  "key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
  "userId": "<service-account-user-id>"
}
```

Store the file in a secrets manager or a file that only the backend process can read.
Never commit the file. Never log the file, its private key, JWT assertions, or access tokens.
Keep the file out of browser code and public assets.
If the file is lost before storage, ask the Broker Administrator to generate a new key.
Ask the Broker Administrator to revoke the lost key.

## 2. Get an access token

Use the JWT Bearer Grant to exchange a signed assertion at `{authority}/oauth/v2/token`.
Here, `authority` is the ZITADEL issuer URL of the target environment, from [Environments](/topics/environments).

The assertion uses these values:

| Field        | Value                                                   |
| ------------ | ------------------------------------------------------- |
| Header `alg` | `RS256`                                                 |
| Header `kid` | The `keyId` from the downloaded file.                   |
| Claim `iss`  | The `userId` from the downloaded file.                  |
| Claim `sub`  | The same `userId`.                                      |
| Claim `aud`  | The exact ZITADEL issuer URL, with no trailing slash.   |
| Claim `iat`  | The current Unix time in seconds.                       |
| Claim `exp`  | A short expiration time. The example uses five minutes. |

::: warning Match the issuer exactly
The assertion's `aud` must equal the OIDC issuer for your environment, with no trailing slash.
A URL from the ZITADEL administration console can have a trailing slash.
A mismatch causes `400 invalid_grant` with an `invalid assertion` message.
The message does not identify the trailing slash as the cause.
:::

::: warning Include the audience scope
Request `urn:zitadel:iam:org:project:id:{quarzoProjectId}:aud`.
Replace `{quarzoProjectId}` with the Quarzo project ID of your environment, from [Environments](/topics/environments).
Without this scope, ZITADEL can issue a token with the wrong audience. Quarzo rejects that token with `401`.
:::

The token request uses form encoding:

```http
POST {authority}/oauth/v2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<signed-jwt-assertion>
&scope=urn:zitadel:iam:org:project:id:{quarzoProjectId}:aud
```

Save this example as `call-quarzo.mjs`. It uses only built-in Node.js modules.
It obtains a token and calls the current-identity endpoint without displaying the token.

```js
import { readFile } from "node:fs/promises";
import { sign } from "node:crypto";

const required = (name) => {
  const value = process.env[name];
  if (!value) throw new Error(`Set ${name} before running this example.`);
  return value;
};

const authority = required("QUARZO_AUTHORITY");
const apiBaseUrl = required("QUARZO_API_BASE_URL").replace(/\/$/, "");
const projectId = required("QUARZO_PROJECT_ID");
// Optional. Set it only for delegation, as section 5 shows.
const exchangeProjectId = process.env.QUARZO_EXCHANGE_PROJECT_ID;
const serviceKey = JSON.parse(
  await readFile(required("QUARZO_SERVICE_KEY_FILE"), "utf8"),
);

if (authority.endsWith("/")) {
  throw new Error(
    "Set QUARZO_AUTHORITY to the issuer without a trailing slash.",
  );
}

const encode = (value) =>
  Buffer.from(JSON.stringify(value)).toString("base64url");
const now = Math.floor(Date.now() / 1000);
const signingInput = [
  encode({ alg: "RS256", typ: "JWT", kid: serviceKey.keyId }),
  encode({
    iss: serviceKey.userId,
    sub: serviceKey.userId,
    aud: authority,
    iat: now,
    exp: now + 300,
  }),
].join(".");
const signature = sign("RSA-SHA256", Buffer.from(signingInput), serviceKey.key);
const assertion = `${signingInput}.${signature.toString("base64url")}`;

const tokenResponse = await fetch(`${authority}/oauth/v2/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
    assertion,
    scope: [projectId, exchangeProjectId]
      .filter(Boolean)
      .map((id) => `urn:zitadel:iam:org:project:id:${id}:aud`)
      .join(" "),
  }),
});
if (!tokenResponse.ok) {
  throw new Error(`Token request failed: HTTP ${tokenResponse.status}`);
}
const token = await tokenResponse.json();
if (typeof token.access_token !== "string" || !token.access_token) {
  throw new Error("The token response has no access_token.");
}

const callQuarzo = async (path) => {
  const response = await fetch(`${apiBaseUrl}${path}`, {
    headers: { Authorization: `Bearer ${token.access_token}` },
  });
  console.log(`GET ${path}: HTTP ${response.status}`);
  if (!response.ok) throw new Error("The Quarzo request failed.");
  return response;
};

await callQuarzo("/v1/authentication/me");
```

Replace every placeholder before execution. [Environments](/topics/environments) gives the first three values:

```bash
export QUARZO_AUTHORITY='<zitadel-issuer-without-trailing-slash>'
export QUARZO_API_BASE_URL='<api-base-url>'
export QUARZO_PROJECT_ID='<quarzo-project-id>'
export QUARZO_SERVICE_KEY_FILE='/private/path/to/service-key.json'
node call-quarzo.mjs
```

Expected result: `GET /v1/authentication/me: HTTP 200`.
An assertion proves the machine's identity to ZITADEL. Send the resulting access token to Quarzo.

## 3. Call a business endpoint

Send the access token in the `Authorization` header:

```http
GET /v1/premium-scheduling/subscribers/{subscriberId}/payers
Authorization: Bearer <access_token>
```

In the example, call `callQuarzo` with the path of the endpoint.
Use the [API reference](/api/) for other business endpoints.

Reuse an access token until its expiration, as indicated by `expires_in` in the token response.
Before expiration, obtain a new token with a new signed assertion.

Quarzo returns `401` for an expired token.
After a revocation, ZITADEL refuses a token request signed with the revoked key.
A token that the backend got before the revocation stays valid until it expires, 15 minutes at most.
If a request returns `401`, check the environment, issuer, audience scope, key status, and token expiration.
If the key is revoked or expired, obtain an active key from the Broker Administrator.
If a request returns `403`, check the permissions for that endpoint.

When you ask Quarzo for help, send the environment, the HTTP status, the error code and the time of the request.
Never send the key file, a private key, a JWT assertion or an access token.

## 4. Rotate the Service Key

Each Service Key expires one year after its generation. Rotate the key before the displayed expiration date.
More than one key can be active at the same time.

1. Ask the Broker Administrator to generate and download a new key for the same Service Account.
2. Deploy the new key to every backend instance that uses the old key.
3. Obtain a new access token with the new key.
4. Verify a Quarzo request with that token.
5. Ask the Broker Administrator to revoke the old key on the Service Keys page.
6. Remove the old private key from backend storage.

## 5. Call Quarzo for a person

Use delegation when a person performs an operation in your Broker system.
The person is a Subscriber or a Broker Advisor of your Broker.
A Broker Administrator cannot be the person of a delegated token.
The Broker backend uses a machine token to get the person's ZITADEL user ID.
It then exchanges that ID and the machine token for a delegated token.

### Get an Exchange Client Key

Follow [Create an Exchange Client Key](/guides/create-an-exchange-client-key) to generate, download, and store the key.
Use the key file from the same Broker and environment as the Service Account.

### Get a delegated token

Get a machine token as described in [Get an access token](#2-get-an-access-token), with one more audience.
The token exchange accepts the machine token only when its audience contains the project of your Exchange Client.
Set `QUARZO_EXCHANGE_PROJECT_ID` to the project ID of your Exchange Client.
Open **Administration → Delegation** and copy **Project id** from the **Exchange Client** section.
The value is not secret.
The example then requests two audience scopes for the machine token:

```text
urn:zitadel:iam:org:project:id:{quarzoProjectId}:aud urn:zitadel:iam:org:project:id:{exchangeProjectId}:aud
```

Quarzo accepts this machine token, because its audience contains the Quarzo project.

Use the machine token to resolve the person's ZITADEL user ID:

```http
GET {apiBaseUrl}/v1/authentication/subjects/{externalUserId}
Authorization: Bearer <machine_access_token>
```

The response includes `kind` and `insurerIdpUserId`. Use `insurerIdpUserId` as the subject in the token exchange.
`kind` is `subscriber` for a Subscriber and `brokerUser` for a Broker Advisor.
The `externalUserId` must identify a Subscriber or a Broker Advisor of your Broker in the target environment.
For a Broker Advisor, the Broker Administrator sets the external user ID on the **Users** page.

Append this code to the end of `call-quarzo.mjs`:

```js
const exchangeClient = JSON.parse(
  await readFile(required("QUARZO_EXCHANGE_CLIENT_FILE"), "utf8"),
);
const externalUserId = required("QUARZO_EXTERNAL_USER_ID");

const subjectResponse = await fetch(
  `${apiBaseUrl}/v1/authentication/subjects/${encodeURIComponent(externalUserId)}`,
  { headers: { Authorization: `Bearer ${token.access_token}` } },
);
if (!subjectResponse.ok) {
  throw new Error(`Subject request failed: HTTP ${subjectResponse.status}`);
}
const subject = await subjectResponse.json();
if (
  !["subscriber", "brokerUser"].includes(subject.kind) ||
  !subject.insurerIdpUserId
) {
  throw new Error(
    "The external user id did not resolve to a Subscriber or a Broker Advisor.",
  );
}

const exchangeNow = Math.floor(Date.now() / 1000);
const exchangeInput = [
  encode({ alg: "RS256", typ: "JWT", kid: exchangeClient.keyId }),
  encode({
    iss: exchangeClient.clientId,
    sub: exchangeClient.clientId,
    aud: authority,
    iat: exchangeNow,
    exp: exchangeNow + 300,
  }),
].join(".");
const exchangeSignature = sign(
  "RSA-SHA256",
  Buffer.from(exchangeInput),
  exchangeClient.key,
);
const clientAssertion =
  `${exchangeInput}.${exchangeSignature.toString("base64url")}`;

const exchangeResponse = await fetch(`${authority}/oauth/v2/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    subject_token: subject.insurerIdpUserId,
    subject_token_type: "urn:zitadel:params:oauth:token-type:user_id",
    actor_token: token.access_token,
    actor_token_type: "urn:ietf:params:oauth:token-type:access_token",
    scope: `urn:zitadel:iam:org:project:id:${projectId}:aud`,
    client_assertion_type:
      "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    client_assertion: clientAssertion,
  }),
});
if (!exchangeResponse.ok) {
  throw new Error(`Token exchange failed: HTTP ${exchangeResponse.status}`);
}
const delegatedToken = await exchangeResponse.json();
if (
  typeof delegatedToken.access_token !== "string" ||
  !delegatedToken.access_token
) {
  throw new Error("The token exchange response has no access_token.");
}

const callQuarzoForThePerson = async (path) => {
  const response = await fetch(`${apiBaseUrl}${path}`, {
    headers: { Authorization: `Bearer ${delegatedToken.access_token}` },
  });
  console.log(`GET ${path} with delegated token: HTTP ${response.status}`);
  if (!response.ok) throw new Error("The delegated Quarzo request failed.");
  return response;
};

const me = await (await callQuarzoForThePerson("/v1/authentication/me")).json();
console.log(`kind: ${me.kind}, authenticationMode: ${me.authenticationMode}`);

// Optional: a business endpoint for a Subscriber of your Broker.
const delegatedSubscriberId = process.env.QUARZO_SUBSCRIBER_ID;
if (delegatedSubscriberId) {
  await callQuarzoForThePerson(
    `/v1/premium-scheduling/subscribers/${encodeURIComponent(delegatedSubscriberId)}/payers`,
  );
}
```

Keep the values of section 2, and add these values for the same environment.
Use the Exchange Client Key file from the same Broker and environment as the Service Key:

```bash
export QUARZO_EXCHANGE_PROJECT_ID='<exchange-client-project-id>'
export QUARZO_EXCHANGE_CLIENT_FILE='/private/path/to/exchange-client-key.json'
export QUARZO_EXTERNAL_USER_ID='<external-user-id-of-the-person>'
# Optional: the ID of a Subscriber of your Broker, for the business endpoint.
export QUARZO_SUBSCRIBER_ID='<subscriber-id>'
node call-quarzo.mjs
```

Expected result:

```text
GET /v1/authentication/me with delegated token: HTTP 200
kind: brokerUser, authenticationMode: delegated
```

For a Subscriber, `kind` is `subscriber`.
The response of `/v1/authentication/me` also contains an `actor` block: the Service Account that acts for the person.
When `QUARZO_SUBSCRIBER_ID` is set, the example also displays the HTTP status of the payers of that Subscriber.

Use the external user ID for the subject lookup. Use the Subscriber ID for the business endpoint.
Request the Quarzo project audience scope during token exchange.

For the difference between machine access and delegation, see [Authentication](/topics/authentication#delegation).
