Skip to main content

Client Certificates (mTLS)

AttestID only accepts an API key when the request also presents a client certificate that AttestID issued to your company. This is called mutual TLS (mTLS): AttestID proves who it is to you, and your server proves who it is to AttestID.

This page covers everything you need: getting a certificate, using it from your language, testing it, and fixing problems.

Why this matters​

An API key on its own is a password, and passwords leak (in logs, in a pasted config, in a former employee's notes). With mTLS, a leaked API key is useless on its own: the attacker would also need your certificate's private key, which never leaves your servers.

The rules​

  • Required for every API-key request, in both test and live. There's no way to switch it off.
  • Only your backend needs it. Browser and mobile apps use SDK tokens, which don't need a certificate.
  • One certificate covers your whole company. It works with all your API keys, test and live.
  • The certificate must belong to the same company as the API key. A valid certificate from another company is rejected.
  • Valid for 365 days. Renew it before it expires (see Renewing).

Step 1: Create your private key and CSR​

Run these on the server that will call AttestID (or wherever you keep secrets). The private key is created there and never leaves it.

openssl ecparam -name prime256v1 -genkey -noout -out client.key
openssl req -new -key client.key -subj "/CN=attestid-client" -out client.csr
chmod 600 client.key

This creates two files:

FileWhat it isShare it?
client.keyYour private keyNever. Not with AttestID, not in Git, not in a frontend or mobile app
client.csrA certificate signing request: your public key, asking to be certifiedYes, you paste it into the dashboard

ECDSA P-256 keys (as above) are recommended. RSA keys of 2048 bits or more also work. The name after /CN= doesn't matter: AttestID always sets the certificate's subject to your company ID.

Step 2: Issue the certificate in the dashboard​

  1. Log in to the AttestID dashboard as the owner, an admin or a developer.
  2. Open Client Certificates → Issue Certificate.
  3. Paste the full contents of client.csr, including the -----BEGIN CERTIFICATE REQUEST----- and -----END CERTIFICATE REQUEST----- lines. Optionally, add a label such as "production server".
  4. Click Issue Certificate, then click the client.crt download button. Your browser saves it as attestid-client.crt.

The dialog offers a second download, attestid-client-ca.pem. That's AttestID's certificate authority, which you only need if your HTTP client asks for the full chain. Don't use it as your client certificate: it won't match your key.

The certificate is only shown once. If you lose it, issue a new one from the same CSR and revoke the unused one.

Put the certificate next to your key​

Your backend needs both files on the same server, under names your code points at. We use client.crt and client.key in this guide.

If you downloaded the certificate on your laptop, copy it to the server as a file named client.crt:

scp ~/Downloads/attestid-client.crt you@your-server:/path/to/client.crt

Or skip the transfer: use the copy button next to the certificate in the dialog (or open the downloaded file and copy everything from -----BEGIN CERTIFICATE----- to -----END CERTIFICATE-----), and paste it into a new client.crt on the server.

Then check the file, on the server:

ls -l client.crt # starts with -rw (a file), about 700 bytes
head -1 client.crt # -----BEGIN CERTIFICATE-----

# The certificate must belong to your key. These two lines must print the same value:
openssl x509 -in client.crt -noout -pubkey | openssl sha256
openssl pkey -in client.key -pubout | openssl sha256

You now have the two files your backend needs: client.crt and client.key.

Step 3: Test the connection​

Before writing any code, check that the certificate works with curl. This calls GET /v1/account/me, which returns your company's details:

curl https://api.attestid.brimsage.co.ls/v1/account/me \
--cert client.crt --key client.key \
-H "X-API-Key: $ATTESTID_API_KEY" \
-H "X-Access-Key-Id: $ATTESTID_ACCESS_KEY_ID"

It works if you get 200 and JSON with your company_name.

It doesn't work if you get 401 with "A valid client certificate is required…". See Troubleshooting.


Step 4: Use the certificate in your code​

Every language follows the same pattern: load client.crt and client.key, attach them to your HTTP client, and send the API-key headers as usual. Create the client once and reuse it.

Node.js​

Node's built-in fetch can't send a client certificate, so use undici (npm install undici):

import { readFileSync } from 'node:fs';
import { Agent, fetch } from 'undici';

const mtls = new Agent({
connect: {
cert: readFileSync('client.crt'),
key: readFileSync('client.key'),
},
});

const res = await fetch('https://api.attestid.brimsage.co.ls/v1/kyc/session', {
method: 'POST',
dispatcher: mtls,
headers: {
'X-API-Key': process.env.ATTESTID_API_KEY!,
'X-Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId }),
});

Using @attestid/sdk on the server? Pass the same thing as its fetch option:

const fetchWithCert = (url: string, init?: RequestInit) =>
fetch(url, { ...init, dispatcher: mtls } as never) as unknown as Promise<Response>;

const session = await createSdkSession(apiUrl, apiKey, accessKeyId, userId, { fetch: fetchWithCert });

Python​

With requests:

import os
import requests

response = requests.post(
"https://api.attestid.brimsage.co.ls/v1/kyc/session",
headers={
"X-API-Key": os.environ["ATTESTID_API_KEY"],
"X-Access-Key-Id": os.environ["ATTESTID_ACCESS_KEY_ID"],
},
json={"user_id": user_id},
cert=("client.crt", "client.key"),
timeout=10,
)
response.raise_for_status()
sdk_token = response.json()["sdk_token"]

With httpx, pass cert=("client.crt", "client.key") to httpx.Client(...).

PHP​

With cURL:

$ch = curl_init('https://api.attestid.brimsage.co.ls/v1/kyc/session');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSLCERT => '/path/to/client.crt',
CURLOPT_SSLKEY => '/path/to/client.key',
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('ATTESTID_API_KEY'),
'X-Access-Key-Id: ' . getenv('ATTESTID_ACCESS_KEY_ID'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['user_id' => $userId]),
]);
$body = curl_exec($ch);

With Guzzle: new Client(['cert' => '/path/to/client.crt', 'ssl_key' => '/path/to/client.key']).

Java​

Java reads certificates from a keystore, so first bundle the two files into one (choose your own password):

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name attestid

Then (Java 11+):

char[] password = System.getenv("ATTESTID_P12_PASSWORD").toCharArray();

KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream("client.p12")) {
keyStore.load(in, password);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);

HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build();

HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.attestid.brimsage.co.ls/v1/kyc/session"))
.header("X-API-Key", System.getenv("ATTESTID_API_KEY"))
.header("X-Access-Key-Id", System.getenv("ATTESTID_ACCESS_KEY_ID"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"user_id\":\"" + userId + "\"}"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

In Spring, give the same SSLContext to your RestClient / WebClient HTTP connector. Treat client.p12 and its password as secrets.

C# / .NET​

.NET 5 or later:

var cert = X509Certificate2.CreateFromPemFile("client.crt", "client.key");
// On Windows, re-import so the key can be used for TLS:
cert = new X509Certificate2(cert.Export(X509ContentType.Pkcs12));

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
var http = new HttpClient(handler); // create once and reuse

var request = new HttpRequestMessage(HttpMethod.Post, "https://api.attestid.brimsage.co.ls/v1/kyc/session")
{
Content = JsonContent.Create(new { user_id = userId }),
};
request.Headers.Add("X-API-Key", Environment.GetEnvironmentVariable("ATTESTID_API_KEY"));
request.Headers.Add("X-Access-Key-Id", Environment.GetEnvironmentVariable("ATTESTID_ACCESS_KEY_ID"));
var response = await http.SendAsync(request);

Go​

cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
if err != nil {
log.Fatal(err)
}
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{cert}}},
}

body, _ := json.Marshal(map[string]string{"user_id": userID})
req, _ := http.NewRequest("POST", "https://api.attestid.brimsage.co.ls/v1/kyc/session", bytes.NewReader(body))
req.Header.Set("X-API-Key", os.Getenv("ATTESTID_API_KEY"))
req.Header.Set("X-Access-Key-Id", os.Getenv("ATTESTID_ACCESS_KEY_ID"))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)

Storing the certificate and key safely​

  • Keep client.key with the same care as your API key: in a secrets manager (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, Doppler…) or a file readable only by your app (chmod 600).
  • Add *.key, *.csr, *.crt and *.p12 to .gitignore.
  • Never put the key in a frontend, a mobile app, a Docker image you publish, or a support ticket.

Platforms without files (Vercel, AWS Lambda, Heroku…)​

Store the two files as environment variables, base64-encoded so the line breaks survive:

base64 -w0 client.crt # → ATTESTID_CLIENT_CERT_B64
base64 -w0 client.key # → ATTESTID_CLIENT_KEY_B64

(On macOS, use base64 -i client.crt.) Then decode them in your code:

const mtls = new Agent({
connect: {
cert: Buffer.from(process.env.ATTESTID_CLIENT_CERT_B64!, 'base64'),
key: Buffer.from(process.env.ATTESTID_CLIENT_KEY_B64!, 'base64'),
},
});

Proxies and firewalls​

The certificate has to travel directly from your server to AttestID. If your company routes outgoing traffic through a proxy that inspects HTTPS traffic (common in corporate networks), it removes the certificate and every request fails with 401. Ask your network team to exclude api.attestid.brimsage.co.ls from inspection.


Renewing a certificate​

Certificates last 365 days, and the dashboard shows each one's Valid Until date. Set a reminder about a month before. To renew without downtime:

  1. Create a new key and CSR (Step 1). Use new file names, such as client-2027.key.
  2. Issue a new certificate in the dashboard (Step 2). Both certificates now work.
  3. Deploy the new files to every server, and test with curl (Step 3).
  4. Revoke the old certificate in the dashboard.

Revoking a certificate​

If a private key might have been exposed, revoke its certificate straight away in Client Certificates. It stops working on the next request. Issue a replacement from a new key.

You can always revoke your last certificate. Issuing a new one goes through the dashboard, not the API, so you can't lock yourself out.


Troubleshooting​

Certificate problems show up in one of three ways:

What you seeCauseHow to checkFix
401 "A valid client certificate is required…"No certificate was sentDoes the curl test (Step 3) work? If curl works but your code doesn't, your code isn't attaching itFollow the example for your language above
401 "A valid client certificate is required…"A proxy strips the certificatecurl works from your laptop but not from the serverExclude api.attestid.brimsage.co.ls from HTTPS inspection
401 "A valid client certificate is required…"Certificate revokedCheck its status under Client CertificatesIssue a new one
401 "A valid client certificate is required…"Certificate from a different company accountThe API key and certificate must come from the same accountIssue a certificate from the account that owns the key
400 "The SSL certificate error"Certificate expiredopenssl x509 -noout -enddate -in client.crtRenew it
400 "The SSL certificate error"Not a certificate AttestID issued (for example a self-signed one, or the CSR file)The file must start with -----BEGIN CERTIFICATE----- and come from the dashboardSave the certificate the dashboard issued
openssl says "Could not find certificate from client.crt", or your code fails with EISDIRclient.crt is a folder, not a file. scp to a path that's already a folder puts the file inside itls -ld client.crt starts with dMove the file out (mv client.crt/attestid-client.crt ./tmp.crt && rmdir client.crt && mv tmp.crt client.crt)
Certificate and key don't match, though you just issued itYou saved attestid-client-ca.pem (AttestID's CA) instead of your certificateopenssl x509 -noout -subject -in client.crt shows AttestID Client CAUse the client.crt download (attestid-client.crt)
An error in your own code before anything is sent, such as "key values mismatch"Certificate and key don't matchopenssl x509 -noout -pubkey -in client.crt | openssl sha256 and openssl pkey -pubout -in client.key | openssl sha256 must print the same valueUse the key that made the CSR, or issue a new certificate

If curl works and your code still fails, compare the URL, headers and certificate files the two are using. If you're stuck, contact support@brimsage.co.ls with the certificate's fingerprint from the dashboard. Never send the private key.