Custodian
Authentication

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

  1. Log in to the Xellar Dashboard (opens in a new tab).
  2. Create a new App (or open an existing one).
  3. Copy the App ID — a UUID that identifies your app.
  4. 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:

HeaderDescription
X-App-IDYour app UUID
X-TimestampCurrent Unix time in seconds
X-SignatureBase64 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/DELETE requests 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

CodeMeaning
401Missing headers, expired timestamp, unknown app, or invalid signature
500Failed to read the request body

All error responses use the shape { "error": "message" }.