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
testandlive. 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,
testandlive. - 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:
| File | What it is | Share it? |
|---|---|---|
client.key | Your private key | Never. Not with AttestID, not in Git, not in a frontend or mobile app |
client.csr | A certificate signing request: your public key, asking to be certified | Yes, 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
- Log in to the AttestID dashboard as the owner, an admin or a developer.
- Open Client Certificates → Issue Certificate.
- 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". - 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.keywith 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,*.crtand*.p12to.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:
- Create a new key and CSR (Step 1). Use new file names, such as
client-2027.key. - Issue a new certificate in the dashboard (Step 2). Both certificates now work.
- Deploy the new files to every server, and test with
curl(Step 3). - 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 see | Cause | How to check | Fix |
|---|---|---|---|
401 "A valid client certificate is required…" | No certificate was sent | Does the curl test (Step 3) work? If curl works but your code doesn't, your code isn't attaching it | Follow the example for your language above |
401 "A valid client certificate is required…" | A proxy strips the certificate | curl works from your laptop but not from the server | Exclude api.attestid.brimsage.co.ls from HTTPS inspection |
401 "A valid client certificate is required…" | Certificate revoked | Check its status under Client Certificates | Issue a new one |
401 "A valid client certificate is required…" | Certificate from a different company account | The API key and certificate must come from the same account | Issue a certificate from the account that owns the key |
400 "The SSL certificate error" | Certificate expired | openssl x509 -noout -enddate -in client.crt | Renew 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 dashboard | Save the certificate the dashboard issued |
openssl says "Could not find certificate from client.crt", or your code fails with EISDIR | client.crt is a folder, not a file. scp to a path that's already a folder puts the file inside it | ls -ld client.crt starts with d | Move 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 it | You saved attestid-client-ca.pem (AttestID's CA) instead of your certificate | openssl x509 -noout -subject -in client.crt shows AttestID Client CA | Use 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 match | openssl x509 -noout -pubkey -in client.crt | openssl sha256 and openssl pkey -pubout -in client.key | openssl sha256 must print the same value | Use 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.