Code Examples
Use the signed read below to establish a working connection. Workflow response examples use fictitious identifiers. API Configuration and Limits contains the current documented settings to use for request scheduling and request sizes.
Before running the example
To run this example, use Node.js 22 or later. It uses built-in Node.js APIs and requires no third-party packages.
Set these environment variables in your backend environment; do not put secrets in browser code:
| Variable | Value |
|---|---|
| EXN_API_BASE_URL | The HTTPS origin provided for your Partnership API access, without a path, query or fragment. Required; no default host is used. |
| EXN_API_KEY | Your active API key with Partnership read access. |
| EXN_PRIVATE_KEY_FILE | Path to a PEM-encoded Ed25519 private key on your machine. This is the example's local input format, not a claim about the format exported by Key Management. |
| EXN_REQUEST_TIMEOUT_MS | A positive integer request deadline chosen for your application, in milliseconds. It is not an API SLA or recommended polling interval. |
Download first-request.mjs, supply the environment variables, then run:
node first-request.mjs
Signed read and error parsing
The program sends only GET /v1/partnership/partner/default-link. It creates a fresh timestamp and Ed25519 signature, hashes an empty body, and leaves the reserved signed idempotency_key empty. It sends no idempotency header and makes no automatic retry.
import { realpathSync } from 'node:fs';
import {createHash, createPrivateKey, sign} from 'node:crypto';
import {readFile} from 'node:fs/promises';
import {pathToFileURL} from 'node:url';
export function signedHeaders(apiKey, privateKeyPem, method, path, body = Buffer.alloc(0)) {
const key = createPrivateKey(privateKeyPem);
if (key.asymmetricKeyType !== 'ed25519') throw new Error('An Ed25519 private key is required');
const timestamp = Date.now();
const payload = Buffer.from(JSON.stringify({
api_key: apiKey, idempotency_key: '', timestamp, sign_version: 1,
method, path, body_hash: createHash('sha256').update(body).digest('base64url'),
}));
return {
'EXN-API-KEY': apiKey, 'EXN-TIMESTAMP': String(timestamp), 'EXN-SIGN-VERSION': '1',
'EXN-DATA': payload.toString('base64url'),
'EXN-SIGN': sign(null, payload, key).toString('base64url'),
};
}
export function parseError(status, body, retryAfter) {
if (body && typeof body.code === 'string' && typeof body.message === 'string') {
return {status, envelope: 'partnership', code: body.code,
diagnostic: body.message, details: body.details, retryAfter};
}
if (body && typeof body.code === 'number' && typeof body.error_message === 'string') {
return {status, envelope: 'request-processing', code: body.code,
name: body.error_message, diagnostic: body.description,
retryAfter, retryAfterTimestamp: body.retry_after};
}
return {status, envelope: 'unrecognized', retryAfter};
}
export async function getDefaultLink({baseUrl, apiKey, privateKeyPem, signal, transport = fetch}) {
let base;
try { base = new URL(baseUrl); } catch {
throw new Error('Set EXN_API_BASE_URL to your Partnership API HTTPS origin');
}
if (base.protocol !== 'https:' || base.username || base.password ||
base.pathname !== '/' || base.search || base.hash || base.hostname.endsWith('.invalid')) {
throw new Error('EXN_API_BASE_URL must be your Partnership API HTTPS origin without credentials, a path, query or fragment');
}
const path = '/v1/partnership/partner/default-link';
let response;
try {
response = await transport(base.origin + path, {
method: 'GET', headers: signedHeaders(apiKey, privateKeyPem, 'GET', path),
signal, redirect: 'error',
});
} catch (cause) {
throw new Error('No response received; no automatic retry was sent.', {cause});
}
const text = await response.text();
let body;
try { body = JSON.parse(text); } catch {
throw new Error(`HTTP ${response.status}: response was not JSON; no automatic retry was sent.`);
}
if (!response.ok) {
const error = new Error(`Request failed with HTTP ${response.status}`);
error.apiError = parseError(response.status, body, response.headers.get('Retry-After'));
throw error;
}
if (response.status !== 200 || !body ||
!['full_default_link', 'domain', 'code', 'path', 'url'].every(k => typeof body[k] === 'string')) {
throw new Error('Unexpected success response shape');
}
return body;
}
async function main() {
const baseUrl = process.env.EXN_API_BASE_URL;
const apiKey = process.env.EXN_API_KEY;
const keyFile = process.env.EXN_PRIVATE_KEY_FILE;
const timeoutMs = Number(process.env.EXN_REQUEST_TIMEOUT_MS);
if (!baseUrl || !apiKey || !keyFile || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
throw new Error('Set EXN_API_BASE_URL, EXN_API_KEY, EXN_PRIVATE_KEY_FILE and a positive EXN_REQUEST_TIMEOUT_MS');
}
const privateKeyPem = await readFile(keyFile, 'utf8');
const result = await getDefaultLink({baseUrl, apiKey, privateKeyPem, signal: AbortSignal.timeout(timeoutMs)});
console.log(JSON.stringify(result, null, 2));
}
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
main().catch(error => {
console.error(error.apiError ? JSON.stringify(error.apiError, null, 2) : error.message);
process.exitCode = 1;
});
}
What to expect
On success the program prints the five required default-link fields as JSON. On a non-2xx response it distinguishes string-code Partnership errors from numeric request-processing errors. Diagnostic text is available for investigation; handling decisions must use the HTTP status and machine-readable code.
A connection failure, request deadline or invalid response stops the program without retrying. This example does not test write outcomes, polling or monetary arithmetic. Use decimal-aware processing before calculating with money in reporting integrations.