Implement a backend client

Build a server-side adapter with contract-derived requests, durable idempotency and settlement tracking.

Use this walkthrough alongside Build your integration. It shows how to turn the API contract into a backend adapter; the coverage map provides code recipes and API Reference links for all public operations.

Copy the request helper

The JavaScript helper below uses Node.js 22 or later and built-in APIs only. Copy this block into a backend .mjs file, then append the workflow functions from this page. No repository checkout, client download or operation catalogue is required. It is an implementation example, not an official SDK.

Set QFP_BASE_URL to your assigned HTTPS gateway origin without /api, and QFP_ACCESS_TOKEN to a current bearer token supplied through your secret manager. The helper supplies API-Version, applies a request timeout and preserves JSON versus text responses. It sends one request per call and does not obtain tokens, retry writes, validate every schema field or decide whether a transaction has settled. Keep those responsibilities in your adapter.

const required = name => {
  const value = process.env[name];
  if (!value) throw new Error(`Set ${name} before running this example`);
  return value;
};
const baseUrl = required('QFP_BASE_URL').replace(/\/$/, '');
if (!/^https:\/\//.test(baseUrl)) throw new Error('Use your assigned HTTPS gateway');
const token = required('QFP_ACCESS_TOKEN');
const segment = value => encodeURIComponent(value);
async function request(method, path, body, key) {
  const headers = { Authorization: `Bearer ${token}`, 'API-Version': '1.0.0', Accept: 'application/json' };
  if (key) headers['Idempotency-Key'] = key;
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  const response = await fetch(baseUrl + path, {
    method, headers, body: body === undefined ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000), redirect: 'error',
  });
  const text = await response.text();
  const data = text && response.headers.get('content-type')?.includes('json') ? JSON.parse(text) : text || null;
  if (!response.ok) {
    const error = new Error(`HTTP ${response.status}`);
    Object.assign(error, { status: response.status, data, headers: response.headers });
    throw error;
  }
  return { status: response.status, data };
}
const identity = await request('GET', '/api/users/me');
console.log(identity.status, identity.data);

API connection: GET /api/users/me. See Authenticate for user authentication and Authentication for machine clients. Confirm machine credential issuance and token endpoints during onboarding. Reading your profile does not prove entitlement to every operation.

Provision before transacting

Confirm the asset's contract is ACTIVE, register the customer, then register its account. Persist a separate idempotency key for each write before sending it. The following functions accept those already-persisted keys; they deliberately do not create a new key every time a function is called.

async function registerCustomer(customerRef, persistedKey) {
  if (!persistedKey) throw new Error('Load the persisted customer key');
  return request('POST', '/api/customers', { customerRef }, persistedKey);
}
async function registerAccount(customerRef, accountRef, assetId, persistedKey) {
  if (!persistedKey) throw new Error('Load the persisted account key');
  return request('POST', '/api/accounts', { customerRef, accountRef, assetId }, persistedKey);
}
async function readAccount(accountRef) {
  return request('GET', '/api/accounts/' + segment(accountRef));
}

API connections: POST /api/customers, POST /api/accounts and GET /api/accounts/{accountRef}. Read Create a customer and an account for the full provisioning sequence. Account registration returns 202 with PENDING; wait for ACTIVE before submitting token operations. Verify custody, signing keys and contract roles separately with the platform operator.

Submit an instruction once

The YAML provides examples for mint, burn, transfer, lock, release and cancel. This mint uses its exact body structure, with your account, asset and decimal amount substituted. It performs a write when called; run it only against the intended environment and account.

async function submitMint(instruction) {
  if (!instruction.idempotencyKey) throw new Error('Load the original persisted key');
  return request('POST', '/api/transactions', {
    txType: 'MINT', assetId: instruction.assetId,
    accountRef: instruction.accountRef,
    amount: instruction.amount, // Decimal string, such as "10.50".
  }, instruction.idempotencyKey);
  // Persist the returned transactionId against this original instruction.
}

API connection: POST /api/transactions. Read Issue tokens, Amounts, currencies and precision and Idempotency and retries.

A new accepted submission returns 202; an idempotent replay can return 200 with the current operation. Neither status alone proves settlement. Transaction submissions reuse the original intent and ignore a changed body under the same key. To retry an uncertain submission, retain its original key and body; to submit a genuinely new intent, persist a different key.

Transaction code recipes include every source request example and potential response. A lock's transactionId identifies the operation; its params.lockId identifies the lock. Release and cancel use txType and params.lockId, rather than another mint-shaped body. Account freeze/unfreeze and contract deployment have their own endpoints.

Track the outcome separately

For a short sandbox exercise, the following bounded poll reads an operation until it reaches a terminal state. In production, prefer signed events plus read-based recovery. A local timeout means the outcome is still unknown; it must not be recorded as a platform failure or trigger a fresh submission.

import { setTimeout as delay } from 'node:timers/promises';

async function waitForOutcome(transactionId, { attempts = 20, intervalMs = 3000 } = {}) {
  const terminal = new Set(['CONFIRMED', 'FAILED', 'CANCELLED']);
  for (let attempt = 0; attempt < attempts; attempt += 1) {
    const { data } = await request('GET', '/api/transactions/' + segment(transactionId));
    if (terminal.has(data.state)) return { complete: true, transaction: data };
    if (attempt + 1 < attempts) await delay(intervalMs);
  }
  return { complete: false, transactionId };
}

API connection: GET /api/transactions/{transactionId}. See Operations and their lifecycle for intermediate states, and Build your own approval inbox for operations awaiting approval. Approval is not confirmation of blockchain settlement.

Your event receiver should verify the signature against the raw request bytes, durably store the event before acknowledging it, deduplicate by event identity and apply changes idempotently. Keep slow business processing out of the acknowledgement path. Follow Receive your first webhook, Webhooks and Delivery history and recovery. Confirm the signing-secret encoding and rotation procedure for your deployment before implementing the verifier.

Read settled balances

async function readBalance(accountRef) {
  const { data } = await request('GET', '/api/accounts/' + segment(accountRef) + '/balances');
  return data;
}

API connection: GET /api/accounts/{accountRef}/balances. Use Read balances and Reconcile against your core banking to interpret the result. Read the contract's distinction between settled, locked and available amounts; do not infer a balance change from an accepted instruction.

Use PUT, PATCH and DELETE deliberately

These examples illustrate contract shapes for administrative changes. They are not required for a normal mint. Check the caller's permissions and use identifiers belonging to your tenant.

// PUT replaces the entire permission set; supply the complete intended set.
async function replaceRolePermissions(tenantId, roleId, intendedPermissions) {
  return request('PUT', '/api/tenants/' + segment(tenantId) + '/roles/' + segment(roleId) + '/permissions',
    { permissions: intendedPermissions });
}
async function pauseWebhook(webhookId, persistedKey) {
  if (!persistedKey) throw new Error('Load the persisted status-change key');
  return request('PATCH', '/api/webhooks/' + segment(webhookId), { status: 'INACTIVE' }, persistedKey);
}
async function deleteWebhook(webhookId) {
  return request('DELETE', '/api/webhooks/' + segment(webhookId)); // 204 has no body.
}

API connections: PUT /api/tenants/{id}/roles/{roleId}/permissions, PATCH /api/webhooks/{id} and DELETE /api/webhooks/{id}. Removing or pausing a webhook stops that integration's event delivery. Consult Roles and access and Webhooks before changing configuration.

For script imports, construct FormData from the fields documented in the Scripts recipes. Use a separate multipart request with the same authentication and version headers. Let fetch supply the multipart boundary; do not manually set Content-Type: multipart/form-data. Source recipes identify operations without a JSON example instead of inventing a body.

Handle failures without duplicating instructions

try {
  await readBalance(process.env.QFP_ACCOUNT_REF);
} catch (error) {
  if (error.status) {
    // Record sanitised diagnostics in your backend, not customer-facing logs.
    console.error({ status: error.status });
    // error.data holds the returned body; error.headers includes Retry-After if supplied.
  }
  throw error;
}
SituationAdapter action
Invalid input or schema rejectionCorrect the request using the endpoint's documented error. Avoid retrying the same invalid input.
Authentication or entitlement failureResolve the token/permission issue. Refresh through the configured identity provider; do not invent a refresh endpoint.
Rate limit or temporary service failureApply your bounded backoff policy and honour Retry-After when supplied. Preserve the write's idempotency key and body.
Timeout or dropped connection after submissionRetain an unknown outcome, recover the operation using the same intent and key, then reconcile.
FAILED or CANCELLED operationStore the terminal outcome and available reason evidence. Decide the next business action separately.

The helper sends one request per call and performs no automatic write retry. Use Errors and Rate limits for platform guidance; endpoint response lists remain authoritative for their individual contracts.

Persist the integration's evidence

RecordWhy your adapter retains it
Business instruction and idempotency keyRecover an uncertain write without creating a second intent.
Customer and account referencesJoin token operations to your bank's records.
Transaction ID and latest observed stateResume outcome tracking after a restart.
Lock ID, where applicableAddress a later release/cancel without confusing it with the submission ID.
Received event identity and processing resultDeduplicate deliveries and recover interrupted processing.
Confirmed transaction evidence and reconciliation resultExplain settlement and investigate outstanding breaks.

Before production, exercise provisioning, approval, confirmation, rejection, lock expiry, duplicate delivery, timeout recovery and reconciliation with the operator. Review Security and key management, Audit and evidence and Integration readiness. Deployment-specific configuration requires operator confirmation; a generated example is not proof that the backend implements every declared capability.

Follow a Recipe

Step through the code in JavaScript or Python, inspect illustrative responses, and open the connected endpoint schemas.


Did this page help you?