Authentication
The Custodian API authenticates every request with an ED25519 signature. You need two things from the Xellar Dashboard: an App ID and a private key.
Step 1: Get Your Key Pair
- Log in to the Xellar Dashboard (opens in a new tab).
- Create a new App (or open an existing one).
- Copy the App ID — a UUID that identifies your app.
- Copy the Private Key — a base64 ED25519 private key shown when the app is created.
Important
- The private key is shown only once and is never stored by Xellar — only your public key is kept on the server.
- Keep the private key secret. Anyone with it can sign requests as your app.
- The matching public key is stored on the server and used to verify your signatures.
Step 2: Sign Your Requests
Each signed request must include these headers:
| Header | Description |
|---|---|
X-App-ID | Your app UUID |
X-Timestamp | Current Unix time in seconds |
X-Signature | Base64 ED25519 signature of the message below |
Message to Sign
The signature is computed over this exact message (newline-separated):
<app_id>\n<timestamp>\n<raw_request_body>- For
GET/DELETErequests with no body, the body part is an empty string. - For
POST/PUT/PATCH, the body is the exact raw JSON bytes you send — whitespace matters, so sign and send the same bytes. - The timestamp must be within ±20 minutes of server time, or the request is rejected.
Example (Node.js)
const crypto = require('crypto');
// privateKeyB64: base64 ED25519 private key from the dashboard
// appId: your App ID (UUID)
function signRequest(privateKeyB64, appId, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const message = `${appId}\n${timestamp}\n${body}`;
const keyObject = crypto.createPrivateKey({
key: Buffer.from(privateKeyB64, 'base64'),
format: 'der',
type: 'pkcs8',
});
const signature = crypto.sign(null, Buffer.from(message), keyObject);
return {
'X-App-ID': appId,
'X-Timestamp': timestamp,
'X-Signature': signature.toString('base64'),
};
}
// Example: signed POST
const body = JSON.stringify({ subID: 'user-123' });
const headers = {
'Content-Type': 'application/json',
...signRequest(process.env.XELLAR_PRIVATE_KEY, process.env.XELLAR_APP_ID, body),
};
fetch('https://custodian-api-dev.xellar.co/api/v1/account/create', {
method: 'POST',
headers,
body, // must be the same bytes that were signed
});Error Responses
| Code | Meaning |
|---|---|
| 401 | Missing headers, expired timestamp, unknown app, or invalid signature |
| 500 | Failed to read the request body |
All error responses use the shape { "error": "message" }.