v2
Merchant Integration Reference  ·  spendlayerapm.com
Overview

How the integration works

Scope of v2. This reference covers the wallet-code checkout — a single payment per request. Recurring collection under a mandate is handled separately; talk to your integration contact if you need it.

Base URL. {{base_url}} throughout is a placeholder — your environment's base URL is issued with your API credentials.

Spendlayer lets you accept crypto payments where your customer pays from their own non-custodial wallet using a wallet code. You create a payment request from your server, send the customer to the checkout link we return, and track the outcome by polling the API (or via your webhook). The whole integration is just two calls — POST /walletCode to start a payment and GET /getStatus to follow it.

The payment flow
later 1 · Merchant calls POST /walletCode and redirects the customer to the returned link 2 · The customer opens the checkout and completes the payment 3 · When the wallet code is processed ↩ webhook wallet_code_processed → your server ✓ the customer is redirected to your successUrl 4 · Afterwards — on-chain completion ↩ webhook completed → your server the definitive paid signal — settle the order on this
Step by step
  1. Create a payment request. Call POST /walletCode with your referenceId, amount and cryptoCurrencyCode. You get back a checkout url and a paymentRequestId.
  2. Send the customer to the checkout. Redirect (or pop out) to the returned url. The customer completes the whole payment on that hosted checkout.
  3. Track the outcome. Poll GET /getStatus/:requestId (or list with GET /getRequests). The status moves created → processing → completed, or expired if the customer never finishes. See the Status Lifecycle.
  4. Get notified / redirected. The customer is returned to the successUrl / failureUrl you supplied, and if you set a webhookUrl we POST a callback when the payment status changes — so you don't have to poll at all.
Every request is authenticated with your API key and an HMAC signature on the body. Read Authentication & Signing next, then the per-endpoint reference below.
Authentication & Signing

Request Headers

Every request must carry two headers: an Authorization bearer token (your API key) and a Signature HMAC-SHA256 of the request body signed with your secret.

Header Value / Format Required Description
Authorization Bearer <api_key> required Your merchant API key.
Signature HMAC-SHA256 hex string required HMAC-SHA256 of the exact JSON-stringified request body, keyed with your API secret. See signing section below.
Content-Type application/json required Body must be JSON. Signature is computed over this JSON string.

How to Sign a Request

The Signature header is an HMAC-SHA256 hex digest. It must be computed over the exact JSON string that will be sent as the request body — field order and whitespace matter.

  1. Build your body object and serialize it with JSON.stringify(body). Do not pretty-print — use the compact (no extra spaces) default output.
  2. Compute HMAC-SHA256 using your API secret (different from the API key) as the key, over the UTF-8 bytes of the JSON string. Output as a lowercase hex string.
  3. Send the hex string verbatim in the Signature request header.
Signing Function — Node.js
JavaScript · Node.js
const crypto = require('crypto');

/**
 * Generate an HMAC-SHA256 hex signature over a JSON body.
 * @param {object} body   - The request body object (plain JS object, NOT yet stringified)
 * @param {string} secret - Your API secret
 * @returns {string}      - Lowercase hex signature to place in the `Signature` header
 */
function generateSignature(body, secret) {
  const jsonString = JSON.stringify(body); // compact — matches server expectation
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(jsonString);
  return hmac.digest('hex');
}

module.exports = { generateSignature };
The signature is computed over JSON.stringify(body) — the exact bytes sent over the wire. If you add spaces, reorder keys, or pretty-print, the signature will not match and the request will be rejected with 403 signature is invalid.
Endpoints
POST /api/v1/public/walletCode Create Wallet-Code Payment Request

Creates a new wallet code payment request for a customer. On success the response includes a unique widget URL the customer visits to enter their wallet code and complete payment. The request is idempotent per referenceId + merchant: if a non-completed request already exists for the same reference ID, the existing widget URL is returned.

Request Headers
HeaderValueRequired
Authorization Bearer <api_key> required
Signature HMAC-SHA256 hex of the request body JSON string required
Content-Type application/json required
Request Body
FieldTypeRequiredDescription
referenceId string required Your unique identifier for this transaction. Used to prevent duplicate requests.
e.g. "ORDER-20240312-0042"
amount string required Payment amount in the specified cryptocurrency. Must be a positive number greater than 0, passed as a string.
e.g. "50.00"
cryptoCurrencyCode string required The cryptocurrency code for the payment.
e.g. "USDC", "EURC"
successUrl string optional URL the customer is redirected to after a successful payment. Falls back to the merchant's default success URL if not provided.
e.g. "https://yourshop.com/success"
failureUrl string optional URL the customer is redirected to after a failed or cancelled payment.
e.g. "https://yourshop.com/failed"
webhookUrl string optional URL that will receive a POST callback when the payment status changes. Overrides the merchant's default webhook URL.
e.g. "https://yourshop.com/webhooks/payment"
Example Request Body
JSON
{
  "referenceId":        "ORDER-20240312-0042",
  "amount":             "50.00",
  "cryptoCurrencyCode": "USDC",
  "successUrl":         "https://yourshop.com/success",
  "failureUrl":         "https://yourshop.com/failed",
  "webhookUrl":         "https://yourshop.com/webhooks/payment"
}
Example Request — Node.js (axios)
JavaScript · Node.js
const axios  = require('axios');
const crypto = require('crypto');

const API_KEY    = 'your_api_key';
const API_SECRET = 'your_api_secret';
const BASE_URL   = '{{base_url}}/api/v1/public';

function sign(body, secret) {
  return crypto.createHmac('sha256', secret)
    .update(JSON.stringify(body))
    .digest('hex');
}

const body = {
  referenceId:        'ORDER-20240312-0042',
  amount:             '50.00',
  cryptoCurrencyCode: 'USDC',
  successUrl:         'https://yourshop.com/success',
  failureUrl:         'https://yourshop.com/failed',
  webhookUrl:         'https://yourshop.com/webhooks/payment',
};

const response = await axios.post(
  `${BASE_URL}/walletCode`,
  body,
  {
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Signature':     sign(body, API_SECRET),
      'Content-Type':  'application/json',
    }
  }
);
Responses
200 Success
JSON · 200 OK — request created
{
  "success":          true,
  "url":              "{{base_url}}/wallet-code/eyJhbGciOi...",
  "paymentRequestId": "req_abc123xyz",
  "message":          "Wallet code payment request created successfully",
  "statusCode":       200
}
400 Duplicate
JSON · 200 — duplicate (non-completed) referenceId
{
  "success": false,
  "message": "Request already exists!",
  "url":     "{{base_url}}/wallet-code/eyJhbGciOi..."
}
400 Validation
JSON · 200 — missing required fields
{
  "success":    false,
  "message":    "Missing required fields: referenceId, amount, cryptoCurrencyCode, merchantId",
  "statusCode": 400
}
403 Auth
JSON · 403 — invalid key or signature
{
  "status":  403,
  "message": "signature is invalid"
  // signature errors: "signature is missing" | "signature is invalid" | "signature verification failed"
  // auth/role errors: "Forbidden"
}
Response Fields
FieldTypeDescription
success boolean Whether the request was processed successfully.
url string The widget URL to send to the customer. Contains a signed JWT token in the path. Redirect or embed this URL for the customer to complete payment.
paymentRequestId string Internal ID of the created payment request. Use this with getStatus to poll for status updates.
message string Human-readable status message or error description.
The url contains a JWT-signed token embedded in the path (/wallet-code/<token>). The token encodes the requestId and merchantId and will expire, so send the customer to it promptly. No decoding is required on your side — simply redirect the customer to this URL.
GET /api/v1/public/getStatus/:requestId Get Payment Request Status

Retrieves the current status of a payment request by its ID. The merchant must be the owner of the request — the API key's merchant identity is verified against the request. Use this endpoint to poll for payment completion or to display status to the customer.

This is a GET request — there is no request body. The Signature header is not required for this endpoint (no body to sign). Only the Authorization header is needed.
Request Headers
HeaderValueRequired
Authorization Bearer <api_key> required
URL Parameters
ParameterTypeRequiredDescription
requestId string required The paymentRequestId returned by POST /walletCode.
e.g. /getStatus/req_abc123xyz
Example Request — Node.js (axios)
JavaScript · Node.js
const axios  = require('axios');

const API_KEY        = 'your_api_key';
const BASE_URL       = '{{base_url}}/api/v1/public';
const paymentReqId   = 'req_abc123xyz';

const response = await axios.get(
  `${BASE_URL}/getStatus/${paymentReqId}`,
  {
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
    }
  }
);
Responses
200 Success
JSON · 200 OK
{
  "success": true,
  "status":  "completed",       // see Status Lifecycle section
  "_id":     "req_abc123xyz",
  "prId":    "PR1a2b3c4d5e",        // internal payment request ID
  "referenceId": "ORDER-20240312-0042",
  "txId":    "0xabc123..."         // blockchain transaction ID (null if not yet paid)
}
400 Not found
JSON · 200 — request not found or wrong merchant
{
  "success": false,
  "message": "Request not found"
  // or: "Merchant not found"
}
403 Auth
JSON · 403
{
  "status":  403,
  "message": "Forbidden"
}
Status Values

status is one of created, processing, completed, expired. See the Status Lifecycle section for what each means and how a request transitions between them.

GET /api/v1/public/getRequests List Payment Requests

Returns a paginated list of your payment requests, newest first. Always scoped to the merchant identified by the API key — you only ever see your own requests. Every query parameter is optional.

This is a GET request — there is no request body, so the Signature header is not required. Only the Authorization header is needed.
Request Headers
HeaderValueRequired
Authorization Bearer <api_key> required
Query Parameters
ParameterTypeRequiredDescription
status string optional Filter by status. A single value or a comma-separated list; unrecognised values are silently ignored.
e.g. "completed"  or  "processing,completed"
Valid values for the wallet code flow: created, processing, completed, expired. See the Status Lifecycle section.
from string optional Only include requests created on or after this date (inclusive), matched against createdAt. ISO-8601; an unparseable value is ignored.
e.g. "2026-01-01"  or  "2026-01-01T00:00:00Z"
to string optional Only include requests created on or before this date (inclusive), matched against createdAt. ISO-8601; an unparseable value is ignored.
e.g. "2026-06-30"
page integer optional 1-based page number. Defaults to 1; values below 1 (or non-numeric) are coerced to 1.
e.g. 2
limit integer optional Page size. Defaults to 50, capped at a maximum of 500; values below 1 (or non-numeric) fall back to the default.
e.g. 100
Example Request — Node.js (axios)
JavaScript · Node.js
const axios  = require('axios');

const API_KEY  = 'your_api_key';
const BASE_URL = '{{base_url}}/api/v1/public';

const response = await axios.get(`${BASE_URL}/getRequests`, {
  params:  { status: 'completed', from: '2026-01-01', page: 1, limit: 50 },
  headers: { 'Authorization': `Bearer ${API_KEY}` },
});
Responses
200 Success
JSON · 200 OK
{
  "success": true,
  "data": [
    {
      "referenceId": "ORDER-20240312-0042",
      "prId":        "PR1a2b3c4d5e",   // internal payment request ID
      "date":        "2026-06-01T12:34:56.000Z",
      "status":      "completed",
      "amount":      50.00,
      "currency":    "USDC"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 123, "totalPages": 3 }
}
403 Auth
JSON · 403
{
  "status":  403,
  "message": "Forbidden"
}
Response Fields
FieldTypeDescription
data array The page of payment requests, sorted by creation date descending (newest first).
data[].referenceId string Your reference ID for the request (null if none was supplied).
data[].prId string Internal payment request ID (PR…).
data[].date string ISO-8601 creation timestamp.
data[].status string Current status (one of the valid status values listed above).
data[].amount number The crypto amount for the request.
data[].currency string The crypto currency code matching amount.
pagination object { page, limit, total, totalPages } — the current page, page size, total matching requests, and total page count.
GET /api/v1/public/getRequest/:requestId Get a Single Payment Request

Retrieves the full merchant-visible detail of a single payment request, including its status history. Scoped to the merchant identified by the API key — you can only fetch your own requests.

This is a GET request — there is no request body, so the Signature header is not required. Only the Authorization header is needed.
Request Headers
HeaderValueRequired
Authorization Bearer <api_key> required
URL Parameters
ParameterTypeRequiredDescription
requestId string required Identifies the request. Accepts the paymentRequestId returned by POST /walletCode (RQ…), the internal ID (PR…), or your own referenceId.
e.g. /getRequest/RQ1a2b3c4d5e
Example Request — Node.js (axios)
JavaScript · Node.js
const axios  = require('axios');

const API_KEY  = 'your_api_key';
const BASE_URL = '{{base_url}}/api/v1/public';

const response = await axios.get(
  `${BASE_URL}/getRequest/RQ1a2b3c4d5e`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
Responses
200 Success
JSON · 200 OK
{
  "success": true,
  "request": {
    "prId":               "PR1a2b3c4d5e",   // internal ID
    "paymentRequestId":   "RQ1a2b3c4d5e",   // widget request ID (poll with this)
    "referenceId":        "ORDER-20240312-0042",
    "status":             "completed",
    "statusLog": [
      { "time": "2026-06-01T12:30:00.000Z", "status": "created" },
      { "time": "2026-06-01T12:33:00.000Z", "status": "processing" },
      { "time": "2026-06-01T12:34:56.000Z", "status": "completed" }
    ],
    "cryptoAmount":       50,
    "cryptoCurrencyCode": "USDC",
    "walletAddress":      "0x…",
    "chain":              "polygon",
    "txId":               "0x…",
    "approvalTxId":       null,
    "successUrl":         "https://yourshop.com/success",
    "failureUrl":         "https://yourshop.com/failed",
    "webhookUrl":         "https://yourshop.com/webhooks/payment",
    "createdAt":          "2026-06-01T12:30:00.000Z",
    "updatedAt":          "2026-06-01T12:34:56.000Z"
  }
}
400 Not found
JSON · 200 — not found or wrong merchant
{
  "success": false,
  "message": "Request not found"
  // or: "Merchant not found"
}
403 Auth
JSON · 403
{
  "status":  403,
  "message": "Forbidden"
}
Response Fields (request)
FieldTypeDescription
prId string Internal payment request ID (PR…).
paymentRequestId string Widget request ID (RQ…) — the value to poll getStatus with.
referenceId string Your reference ID (null if none).
status string Current status.
statusLog array Ordered status history; each entry is { time, status } (internal acquirer/error detail is stripped).
cryptoAmount / cryptoCurrencyCode number / string The crypto amount and currency for the request.
walletAddress / chain string The on-chain wallet address and chain involved (null until assigned).
txId / approvalTxId string Settlement transaction hash and (where applicable) the approval transaction hash (null until present).
successUrl / failureUrl / webhookUrl string The redirect and webhook URLs recorded for the request (null if none).
createdAt / updatedAt string ISO-8601 creation and last-update timestamps.
Status Lifecycle

Wallet Code Request Statuses

Both GET /getStatus and GET /getRequests report a request's current status. For the wallet code flow a request only ever moves through the four states below — these are the complete set of values you need to handle.

Lifecycle
created
   │  customer opens the link and enters their wallet codeprocessing
   │  payment authorised; awaiting on-chain settlementcompleted     // funds settled to the merchant wallet on-chain — TERMINAL


// If the customer never completes within the expiry window, the expiry
// job moves the request to a terminal expired state instead:
created / processing   ──(expiry job)──▶   expired     // TERMINAL
StatusMeaning
created Request created; awaiting customer action (entering their wallet code and paying).
processing Payment has been authorised; awaiting on-chain settlement.
completed Terminal. Funds have settled to the merchant wallet on-chain. txId is populated.
expired Terminal. The request was not completed within the expiry window and was expired automatically.
These endpoints report only the four statuses above. The customer-facing checkout tracks additional, more granular states internally that are never returned here — don't rely on any status outside the four listed.
Webhooks

Server-to-server callbacks

Instead of polling, you can receive a callback whenever a payment progresses. Set a webhookUrl per request on POST /walletCode (it overrides your account default). We send an HTTP POST with a JSON body to that URL for each event below.

Each event must be enabled for your account. If you're not receiving callbacks, ask your integration contact to switch the events on for your merchant profile.
Events
Event (status)When it fires
wallet_code_processed The customer's wallet code has been accepted and the payment authorised. Sent before on-chain settlement — treat it as "in progress", not "paid".
completed Funds have settled on-chain to the merchant wallet. This is the definitive "paid" signal — fulfil the order on this event.
Request Headers (sent to your endpoint)
HeaderValue
Content-Type application/json
Authorization Bearer <your_api_key>
signature HMAC-SHA256 hex of the raw JSON body, keyed with your API secret — same scheme as request signing. Verify this before trusting the payload.
Payload — wallet_code_processed
JSON · POST to your webhookUrl
{
  "success":     true,
  "status":      "wallet_code_processed",
  "_id":         "RQ1a2b3c4d5e",           // your paymentRequestId
  "prId":        "PR1a2b3c4d5e",           // internal ID
  "referenceId": "ORDER-20240312-0042"
}
Payload — completed
JSON · POST to your webhookUrl
{
  "success":     true,
  "status":      "completed",
  "_id":         "RQ1a2b3c4d5e",           // your paymentRequestId
  "prId":        "PR1a2b3c4d5e",           // internal ID
  "referenceId": "ORDER-20240312-0042"
}
Payload Fields
FieldTypeDescription
successbooleanAlways true for these events.
statusstringThe event: wallet_code_processed or completed.
_idstringYour paymentRequestId (RQ…) — the same value returned by POST /walletCode.
prIdstringInternal payment request ID (PR…).
referenceIdstringYour reference ID for the request.
Verifying & acknowledging
JavaScript · Node.js (Express)
const crypto = require('crypto');
const API_SECRET = 'your_api_secret';

app.post('/webhooks/payment', express.json(), (req, res) => {
  // 1. Verify the signature over the exact JSON body, keyed with your secret
  const expected = crypto.createHmac('sha256', API_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');
  if (req.get('signature') !== expected) return res.sendStatus(401);

  // 2. Act on the event
  const { status, referenceId } = req.body;
  if (status === 'completed') {
    // funds settled — fulfil the order for referenceId
  }

  // 3. Acknowledge with HTTP 200 — anything else is treated as a failure and retried
  res.sendStatus(200);
});
Delivery: respond with exactly HTTP 200 to acknowledge. Any other status code (including other 2xx such as 201/204), an error, or no response is treated as a failed delivery and retried up to 5 times. Events may arrive more than once and aren't strictly ordered, so make your handler idempotent (key off referenceId / _id) and rely on completed as the final paid signal.
Complete Integration Example
JavaScript · Node.js — full flow
const axios  = require('axios');
const crypto = require('crypto');

const API_KEY    = 'your_api_key';
const API_SECRET = 'your_api_secret';
const BASE_URL   = '{{base_url}}/api/v1/public';

// ── Sign helper ───────────────────────────────────────────────────────
function sign(body, secret) {
  return crypto.createHmac('sha256', secret)
    .update(JSON.stringify(body))
    .digest('hex');
}

// ── STEP 1: Create a wallet-code payment request ──────────────────────
async function createWalletCodeRequest() {
  const body = {
    referenceId:        'ORDER-20240312-0042',
    amount:             '50.00',
    cryptoCurrencyCode: 'USDC',
    successUrl:         'https://yourshop.com/success',
    failureUrl:         'https://yourshop.com/failed',
    webhookUrl:         'https://yourshop.com/webhooks/payment',
  };

  const { data } = await axios.post(`${BASE_URL}/walletCode`, body, {
    headers: {
      Authorization:  `Bearer ${API_KEY}`,
      Signature:      sign(body, API_SECRET),
      'Content-Type': 'application/json',
    },
  });

  if (!data.success) throw new Error(data.message);

  console.log('Widget URL → redirect customer to:', data.url);
  console.log('Payment request ID:', data.paymentRequestId);
  return data.paymentRequestId;
}

// ── STEP 2: Poll for status until completed / expired ─────────────────
async function pollStatus(paymentRequestId) {
  const TERMINAL = ['completed', 'expired'];
  let attempts = 0;

  while (attempts++ < 60) {
    const { data } = await axios.get(
      `${BASE_URL}/getStatus/${paymentRequestId}`,
      { headers: { Authorization: `Bearer ${API_KEY}` } }
    );

    console.log('Current status:', data.status);

    if (TERMINAL.includes(data.status)) {
      return data;
    }

    // Wait 10 seconds before next poll
    await new Promise(r => setTimeout(r, 10_000));
  }

  throw new Error('Timed out waiting for payment');
}

// ── Run ───────────────────────────────────────────────────────────────
(async () => {
  const id     = await createWalletCodeRequest();
  const result = await pollStatus(id);
  console.log('Final result:', result);
})();