Authentication
The Custodian API authenticates requests with an ED25519 signature. You need two things from the Xellar Panel (opens in a new tab): an App ID and a private key.
These reads are public and do not require a signature:
GET /api/v1/tokens— allowed tokens and networksGET /api/v1/wallets/to-listenandGET /api/v1/wallets/listen/:address/status— listen wallet
Step 1: Get Your Key Pair
- Log in to the Xellar Panel (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 — standard base64 of a 64-byte ED25519 private key (the format returned when an app is created in the API). The public key kept on the server is 32 bytes, also base64-encoded.
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.
Signature playground
Use this tool to generate the same headers your server would produce. Enter the exact raw body string you send on the wire (including spacing). To debug a mismatch with your code, enable Use custom timestamp and paste the same Unix seconds value you use locally — X-Signature should match byte-for-byte.
Signing runs only in your browser. Your private key is not sent to Xellar or stored anywhere.
Runs entirely in your browser. Use a fixed timestamp to match your own signer line-by-line (same App ID, timestamp, and raw body bytes).
Example (Node.js)
const crypto = require('crypto');
// Panel private key: base64 of 64-byte ed25519.PrivateKey (not PKCS#8).
function ed25519KeyFromPanel(privateKeyB64) {
const raw = Buffer.from(privateKeyB64, 'base64');
if (raw.length !== 64) {
throw new Error('Expected 64-byte ED25519 private key from Panel');
}
const seed = raw.subarray(0, 32);
const pkcs8Prefix = Buffer.from('302e020100300506032b657004220420', 'hex');
const pkcs8 = Buffer.concat([pkcs8Prefix, seed]);
return crypto.createPrivateKey({ key: pkcs8, format: 'der', type: 'pkcs8' });
}
function signRequest(privateKeyB64, appId, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const message = `${appId}\n${timestamp}\n${body}`;
const keyObject = ed25519KeyFromPanel(privateKeyB64);
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" }.