Skip to main content

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:

VariableValue
EXN_API_BASE_URLThe HTTPS origin provided for your Partnership API access, without a path, query or fragment. Required; no default host is used.
EXN_API_KEYYour active API key with Partnership read access.
EXN_PRIVATE_KEY_FILEPath 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_MSA 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.