Skip to main content

Authentication

Every API call carries a bearer token. Your integration gets one by signing in as its client application, then sends it on each request.

ClaimRev is moving API sign-in from Azure AD B2C to ZITADEL, its new identity platform. Both work today, and existing B2C integrations keep working unchanged. Which one you use depends on the credential your client application has:

Your application hasSign-in methodGo to
A key file (cr-...-key.json). The portal's buttons say Generate key / Add key.ZITADEL, JWT bearer grantSigning in with a key file
A client secret. The portal's buttons say Generate secret / Rotate secret.Azure AD B2C, client credentials grantSigning in with a client secret

To move an existing integration from a secret to a key, contact ClaimRev support. The two can run side by side while you switch.

Environments​

Test and production are separate. Credentials, tokens and API servers from one never work with the other.

Test / Staging​

ZITADEL (key file)Azure AD B2C (client secret)
Token URLhttps://auth-staging.claimrev.com/oauth/v2/tokenhttps://stagingclaimrevcom.b2clogin.com/stagingclaimrevcom.onmicrosoft.com/B2C_1_sign-in-only/oauth2/v2.0/token
Audience / scopeopenid urn:zitadel:iam:org:project:id:394262572515196931:audhttps://stagingclaimrevcom.onmicrosoft.com/portal/api/.default
API serverhttps://testapi.claimrev.comhttps://testapi.claimrev.com
Portalhttps://testportal.claimrev.comhttps://testportal.claimrev.com

Production​

ZITADEL (key file)Azure AD B2C (client secret)
Token URLhttps://auth.claimrev.com/oauth/v2/tokenhttps://portalclaimrev.b2clogin.com/portalclaimrev.onmicrosoft.com/B2C_1_sign-in-service/oauth2/v2.0/token
Audience / scopeopenid urn:zitadel:iam:org:project:id:394259562464542723:audhttps://portalclaimrev.onmicrosoft.com/portal/api/.default
API serverhttps://api.claimrev.comhttps://api.claimrev.com
Portalhttps://portal.claimrev.comhttps://portal.claimrev.com

Getting credentials​

Each ClaimRev account has its own client applications. The recommended pattern is one application per account and per system that calls the API. If you're an EMR or billing service onboarding multiple practices, expect to manage one set of credentials per practice.

  1. Sign in to the portal for your environment using the credentials supplied during enrollment. The test environment doesn't email passwords; use the Forgot password link on first login.
  2. Open Practice Admin, then API Integrations, then Client Applications.
  3. Copy the Client ID. Then, depending on the application:
    • Key file: click Generate key (or Add key) and download the key file. It can be downloaded once. Or, to keep the private key in your own system, register your own public key instead (see Using your own key pair).
    • Client secret: click Generate secret (or Rotate secret) and copy the secret. It is shown once.

If your account has no application yet, request one from the same page. See Client Applications for the full walkthrough, including roles and what to do when the page is missing from your menu.

Signing in with a key file (ZITADEL)​

A ZITADEL client application has no secret. It has a key file: a small JSON document holding a private key. Your integration proves who it is by signing a short-lived JWT (the assertion) with that key, then exchanges the assertion for an access token. This is the OAuth 2.0 JWT bearer grant (RFC 7523). The private key never leaves your system.

The key file​

{
"type": "serviceaccount",
"keyId": "339xxxxxxxxxxxxxxx",
"key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n",
"expirationDate": "2028-10-13T00:00:00Z",
"userId": "339yyyyyyyyyyyyyyy"
}

Treat it exactly like a password: store it in your secret manager, never in source control. A key belongs to one environment. A key from testportal.claimrev.com is rejected by the production token URL, and the reverse; this is the most common setup mistake.

1. Build the assertion​

A JWT signed with the key file's private key using RS256:

Value
Header kidthe key file's keyId
issthe key file's userId
subthe key file's userId
audthe environment's ZITADEL instance, https://auth.claimrev.com or https://auth-staging.claimrev.com (no trailing slash, no path)
iatnow, in seconds since the epoch
expa few minutes from now; 5 minutes is plenty. It only has to outlive the token request.

Your server's clock must be accurate: an assertion that looks issued in the future, or already expired, is rejected.

2. Exchange it for an access token​

POST https://auth.claimrev.com/oauth/v2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion={SIGNED_ASSERTION}
&scope=openid urn:zitadel:iam:org:project:id:394259562464542723:aud

The scope must include the ClaimRev API project as shown, so the token is addressed to the ClaimRev API. Use the staging project id with the staging token URL.

Successful response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"token_type": "Bearer",
"expires_in": 3599
}

3. Call the API, with the tenant header​

ZITADEL tokens need one extra header on every API call: x-auth-tenant: zitadel.

GET https://api.claimrev.com/api/UserProfile/v1/GetDefaultAccount
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
x-auth-tenant: zitadel

Without the header the API treats the token as an Azure AD B2C token and answers 401, even though the token is valid.

Examples​

C# (.NET) using Microsoft.IdentityModel.JsonWebTokens:

using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

const string Authority = "https://auth.claimrev.com";
const string ProjectId = "394259562464542723";

var keyFile = JsonDocument.Parse(File.ReadAllText("cr-xxxxxxxx-key.json")).RootElement;
var userId = keyFile.GetProperty("userId").GetString()!;

using var rsa = RSA.Create();
rsa.ImportFromPem(keyFile.GetProperty("key").GetString());
var now = DateTime.UtcNow;

var assertion = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Issuer = userId,
Audience = Authority,
Claims = new Dictionary<string, object> { ["sub"] = userId },
IssuedAt = now,
NotBefore = now,
Expires = now.AddMinutes(5),
SigningCredentials = new SigningCredentials(
new RsaSecurityKey(rsa) { KeyId = keyFile.GetProperty("keyId").GetString() },
SecurityAlgorithms.RsaSha256)
});

using var http = new HttpClient();
var tokenResponse = await http.PostAsync($"{Authority}/oauth/v2/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer",
["assertion"] = assertion,
["scope"] = $"openid urn:zitadel:iam:org:project:id:{ProjectId}:aud"
}));
tokenResponse.EnsureSuccessStatusCode();
var accessToken = JsonDocument.Parse(await tokenResponse.Content.ReadAsStringAsync())
.RootElement.GetProperty("access_token").GetString();

var request = new HttpRequestMessage(HttpMethod.Get, "https://api.claimrev.com/api/UserProfile/v1/GetDefaultAccount");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
request.Headers.Add("x-auth-tenant", "zitadel");
var account = await http.SendAsync(request);

Python using PyJWT (pip install pyjwt[crypto] requests):

import json, time
import jwt, requests

AUTHORITY = "https://auth.claimrev.com"
PROJECT_ID = "394259562464542723"

with open("cr-xxxxxxxx-key.json") as f:
key = json.load(f)

now = int(time.time())
assertion = jwt.encode(
{"iss": key["userId"], "sub": key["userId"], "aud": AUTHORITY, "iat": now, "exp": now + 300},
key["key"],
algorithm="RS256",
headers={"kid": key["keyId"]},
)

token = requests.post(
f"{AUTHORITY}/oauth/v2/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
"scope": f"openid urn:zitadel:iam:org:project:id:{PROJECT_ID}:aud",
},
).json()["access_token"]

account = requests.get(
"https://api.claimrev.com/api/UserProfile/v1/GetDefaultAccount",
headers={"Authorization": f"Bearer {token}", "x-auth-tenant": "zitadel"},
)

Any language with a JWT library that signs RS256 works the same way. Libraries that support "private key JWT" or "JWT profile" for ZITADEL can build the assertion for you.

Keys and rotation​

Adding a key does not invalidate the existing one, so you can deploy the new key file, verify it, then revoke the old key. A key can have an expiry date (shown in the key file and the portal) or none. See Client Applications → Rotating a key.

Using your own key pair​

Instead of downloading a key file, you can generate the key pair yourself, for example in your key vault or HSM, and register only the public key in the portal. The private key then never exists outside your system, and ClaimRev never sees it. See Client Applications → Using your own public key.

The key must be RSA, 2048 bits or larger, as a PEM public key (-----BEGIN PUBLIC KEY-----). With OpenSSL:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem

The sign-in is exactly the same as with a key file. Since there is no key file to read the values from, take them from the portal, which shows them when you register the key:

Assertion fieldValue
Header kidthe Key ID shown in the portal
iss and subthe User ID shown in the portal

Sign with your private key using RS256. The aud, iat, exp, token URL and scope are as in Build the assertion above.

Signing in with a client secret (Azure AD B2C)​

Send a POST to the environment's B2C token URL using application/x-www-form-urlencoded:

POST https://portalclaimrev.b2clogin.com/portalclaimrev.onmicrosoft.com/B2C_1_sign-in-service/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={YOUR_CLIENT_ID}
&client_secret={YOUR_CLIENT_SECRET}
&scope=https://portalclaimrev.onmicrosoft.com/portal/api/.default

Successful response:

{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600
}

(Substitute the staging token URL and scope when targeting the test environment.)

Include the token on every API call as a Bearer authorization header. B2C tokens need no extra header:

GET https://api.claimrev.com/api/UserProfile/v1/GetDefaultAccount
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi...

Secrets are valid for two years, and you rotate them yourself in the portal. Rotating issues a new secret while the old one keeps working, so you can deploy and verify the new secret before revoking the old one. Users on your account are reminded at sign-in once a secret is within 30 days of expiring. See Client Applications → Rotating a secret.

Your first call​

GET /api/UserProfile/v1/GetDefaultAccount is a useful first call with either method: it returns the account number associated with your token and confirms your sign-in is wired correctly end to end.

Token lifetime​

  • Read expires_in from the token response (seconds) and don't hard-code a lifetime; it can differ between ZITADEL and B2C and may change.
  • Neither method issues refresh tokens. Request a new token when the current one is close to expiring: build a fresh assertion (ZITADEL) or post the client secret again (B2C).
  • Cache the token across calls and reuse it. A common pattern is to request a new one once fewer than 5 minutes remain.

Errors​

HTTPWhat it means
400 from the ZITADEL token URLThe assertion was rejected (invalid_grant), usually: the key was revoked or has expired, the key belongs to the other environment, aud is not exactly the instance URL, iss/sub is not the key file's userId, kid is not its keyId, or your server clock is off. The error_description field gives the reason.
400 or 401 from the B2C token URLThe token request failed, usually a wrong client_id, client_secret or scope. The error and error_description fields give the reason; see Microsoft's B2C error reference.
401 from the APIThe token is missing, malformed or expired; request a new one. With a ZITADEL token, first check that every call sends x-auth-tenant: zitadel.
403 from the APIYour application is signed in but doesn't have permission for the endpoint or account you're calling. Check its roles and the account scoping, and contact ClaimRev support if you believe access should be granted.

Security reminders​

  • Never commit key files or secrets to source control. Use your secret manager or environment configuration.
  • Never log tokens, secrets, key files or assertions, even at debug level.
  • Tokens are bearer credentials: anyone holding a valid token can act as your application until it expires.
  • If you suspect a key or secret is compromised, add a new one, revoke the old one right away, and tell ClaimRev support. See If a key or secret is compromised.