Back to documentation

HTTP integration examples

XferAPI is a hosted JSON API. Use the HTTP client already present in your backend; no embedded engine or public SDK is required.

Base URL

https://api.xferapi.com/v1

Authentication

Bearer xfer_sk_<key_id>_<secret>

Content type

application/json

Payments and business events

Call XferAPI after your provider confirms the event

XferAPI does not authorize card or fiat payments. Keep Stripe, Alipay, WeChat Pay, or another provider in its existing role. After your backend verifies a payment, renewal, refund, reward, or usage event, submit one idempotent XferAPI transfer to move the corresponding customer-defined units.

1

Provider confirms

Verify the signed webhook or authoritative server response.

2

Persist intent

Save one transfer ID, scene, and complete payload with the business event.

3

Create transfer

Issue, spend, split, or reverse units through the ledger-scoped API.

4

Inspect result

Use the response code, request ID, Transfer, Records, and Console history.

Read a balance with curl
curl "https://api.xferapi.com/v1/accounts/member_42" \
  -H "Authorization: Bearer $XFERAPI_KEY"

Production-safe write pattern

Create a transfer without guessing after a timeout

Save the business transfer ID and complete payload before the first request. Business outcomes use HTTP 200, while protocol and infrastructure failures may use a non-2xx status; always parse a valid JSON envelope and branch on its string code. Only ok is success. A timeout, connection failure, invalid or missing envelope, internal_error, service_unavailable, or transfer_state_conflict is uncertain. Resend the exact original ID, scene, and payload directly; do not generate a new ID or require a read request before retrying.

rate_limit_exceeded, quota_exceeded, and billing_suspended reject the new attempt before it executes. After backoff or after the Workspace condition is resolved, resend the same original request. For POST /v1/transfers, the terminal transaction failure codes are exactly insufficient_balance and execution-time amount_out_of_range. That ID-and-scene pair can never later succeed; fix the business condition, then start a new attempt with a new transfer_id. Handle transfer_already_reversed separately because the pair is already closed, and idempotency_conflict because the pair belongs to another payload; either case requires a new ID for the request you are trying to submit.

Persist identity

Store one transfer ID with the business operation before calling XferAPI.

Keep integers exact

Send every amount as a positive decimal JSON string.

Retry uncertainty

Resend the exact original request directly; a read is not a prerequisite.

Retain request ID

Log X-Request-ID with your own operation ID, without logging the API key.

Server-side JavaScript transfer
const transferId = 'order_123' // Persist with your order before the first request.
const scene = 'spend'
const payload = {
  transfer_id: transferId,
  scene,
  background_completion: true,
  from_accounts: [
    { account_id: 'buyer_42', asset: 'points', action: 'spend', amount: '100' }
  ],
  to_accounts: [
    { account_id: 'seller_7', asset: 'points', action: 'spend', amount: '90' },
    { account_id: '-2', asset: 'points', action: 'spend', amount: '10' }
  ]
}

const UNCERTAIN_CODES = new Set([
  'internal_error',
  'service_unavailable',
  'transfer_state_conflict'
])
const RETRY_AFTER_CONDITION_CODES = new Set([
  'rate_limit_exceeded',
  'quota_exceeded',
  'billing_suspended'
])
const TERMINAL_TRANSFER_CODES = new Set([
  'insufficient_balance',
  'amount_out_of_range'
])

class XferAPIError extends Error {
  constructor(message, details) {
    super(message)
    Object.assign(this, details)
  }
}

async function callXferAPI(path, options = {}) {
  let response
  try {
    response = await fetch(`https://api.xferapi.com/v1${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${process.env.XFERAPI_KEY}`,
        'Content-Type': 'application/json',
        ...options.headers
      },
      signal: AbortSignal.timeout(40_000)
    })
  } catch (cause) {
    throw new XferAPIError('XferAPI response is uncertain', { cause, uncertain: true })
  }

  const requestId = response.headers.get('x-request-id')
  const body = await response.json().catch(() => null)
  if (requestId) console.info('XferAPI response request ID:', requestId)
  if (!body || typeof body !== 'object' ||
      typeof body.code !== 'string' || !('data' in body)) {
    throw new XferAPIError('XferAPI returned an invalid transport response', {
      httpStatus: response.status,
      code: body?.code,
      requestId,
      uncertain: true
    })
  }
  if (body.code !== 'ok') {
    throw new XferAPIError(body.message || body.code, {
      httpStatus: response.status,
      code: body.code,
      requestId,
      uncertain: UNCERTAIN_CODES.has(body.code),
      retryAfterCondition: RETRY_AFTER_CONDITION_CODES.has(body.code),
      terminalTransfer: TERMINAL_TRANSFER_CODES.has(body.code),
      pairClosed: body.code === 'transfer_already_reversed',
      payloadConflict: body.code === 'idempotency_conflict'
    })
  }
  if (!response.ok) {
    throw new XferAPIError('XferAPI returned ok with a non-success HTTP status', {
      httpStatus: response.status,
      code: body.code,
      requestId,
      uncertain: true
    })
  }
  return { data: body.data, requestId }
}

const create = () => callXferAPI('/transfers', {
  method: 'POST',
  body: JSON.stringify(payload)
})

async function submitSafely() {
  try {
    return await create()
  } catch (error) {
    if (!error.uncertain) throw error

    // Do not create a new ID or make a read request first. Resend the exact
    // original ID, scene, and payload; XferAPI continues the same operation.
    return create()
  }
}

const result = await submitSafely()
console.info('XferAPI request ID:', result.requestId)
Python transfer
import os
import requests

TRANSFER_ID = "order_123"  # Persist with your order before the first request.
SCENE = "spend"
PAYLOAD = {
    "transfer_id": TRANSFER_ID,
    "scene": SCENE,
    "background_completion": True,
    "from_accounts": [
        {"account_id": "buyer_42", "asset": "points", "action": "spend", "amount": "100"}
    ],
    "to_accounts": [
        {"account_id": "seller_7", "asset": "points", "action": "spend", "amount": "90"},
        {"account_id": "-2", "asset": "points", "action": "spend", "amount": "10"},
    ],
}

UNCERTAIN_CODES = {
    "internal_error",
    "service_unavailable",
    "transfer_state_conflict",
}
RETRY_AFTER_CONDITION_CODES = {
    "rate_limit_exceeded",
    "quota_exceeded",
    "billing_suspended",
}
TERMINAL_TRANSFER_CODES = {
    "insufficient_balance",
    "amount_out_of_range",
}

class XferAPIError(Exception):
    def __init__(
        self,
        message,
        *,
        status=None,
        code=None,
        request_id=None,
        uncertain=False,
        retry_after_condition=False,
        terminal_transfer=False,
        pair_closed=False,
        payload_conflict=False,
    ):
        super().__init__(message)
        self.status = status
        self.code = code
        self.request_id = request_id
        self.uncertain = uncertain
        self.retry_after_condition = retry_after_condition
        self.terminal_transfer = terminal_transfer
        self.pair_closed = pair_closed
        self.payload_conflict = payload_conflict

def call_xferapi(method, path, *, json=None):
    try:
        response = requests.request(
            method,
            f"https://api.xferapi.com/v1{path}",
            headers={"Authorization": f"Bearer {os.environ['XFERAPI_KEY']}"},
            json=json,
            timeout=40,
        )
    except requests.RequestException as error:
        raise XferAPIError("XferAPI response is uncertain", uncertain=True) from error

    request_id = response.headers.get("X-Request-ID")
    try:
        body = response.json()
    except ValueError:
        body = None
    if request_id:
        print("XferAPI response request ID:", request_id)
    if (
        not isinstance(body, dict)
        or not isinstance(body.get("code"), str)
        or "data" not in body
    ):
        raise XferAPIError(
            "XferAPI returned an invalid transport response",
            status=response.status_code,
            code=body.get("code") if isinstance(body, dict) else None,
            request_id=request_id,
            uncertain=True,
        )
    if body["code"] != "ok":
        raise XferAPIError(
            body.get("message") or body["code"],
            status=response.status_code,
            code=body["code"],
            request_id=request_id,
            uncertain=body["code"] in UNCERTAIN_CODES,
            retry_after_condition=body["code"] in RETRY_AFTER_CONDITION_CODES,
            terminal_transfer=body["code"] in TERMINAL_TRANSFER_CODES,
            pair_closed=body["code"] == "transfer_already_reversed",
            payload_conflict=body["code"] == "idempotency_conflict",
        )
    if not response.ok:
        raise XferAPIError(
            "XferAPI returned ok with a non-success HTTP status",
            status=response.status_code,
            code=body["code"],
            request_id=request_id,
            uncertain=True,
        )
    return body["data"], request_id

def create_transfer():
    return call_xferapi("POST", "/transfers", json=PAYLOAD)

def submit_safely():
    try:
        return create_transfer()
    except XferAPIError as error:
        if not error.uncertain:
            raise

        # Do not create a new ID or make a read request first. Resend the exact
        # original ID, scene, and payload; XferAPI continues the same operation.
        return create_transfer()

data, request_id = submit_safely()
print("XferAPI request ID:", request_id)

Opt in to an ordinary account overdraft deliberately

Ordinary source accounts require sufficient available balance by default. Add allow_overdraft: true to a from_accounts item only when your product permits that account to become negative, and keep the value unchanged on every retry. Source items for the same ordinary account and asset must agree. Platform accounts always allow overdraft; the field is ignored on destination items.

Keep credentials and exact integers on the server

Do not ship a service key in a browser or mobile app. Amounts use decimal JSON strings across the complete signed 64-bit range; JavaScript backends can parse returned amounts with BigInt without losing precision.

Distinguish a retry from a new attempt

Uncertain results and rate_limit_exceeded, quota_exceeded, or billing_suspended keep the original ID, scene, and exact payload. After insufficient_balance or execution-time amount_out_of_range, that pair can never succeed; the corrected attempt must use a new transfer_id.

Use the API reference for the complete request model, status values, and error contract.