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.
-
Create a payment request. Call
POST /walletCodewith yourreferenceId,amountandcryptoCurrencyCode. You get back a checkouturland apaymentRequestId. -
Send the customer to the checkout. Redirect (or pop out) to the returned
url. The customer completes the whole payment on that hosted checkout. -
Track the outcome. Poll
GET /getStatus/:requestId(or list withGET /getRequests). The status movescreated → processing → completed, orexpiredif the customer never finishes. See the Status Lifecycle. -
Get notified / redirected. The customer is returned to the
successUrl/failureUrlyou supplied, and if you set awebhookUrlwe POST a callback when the payment status changes — so you don't have to poll at all.
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.
-
Build your body object and serialize it with
JSON.stringify(body). Do not pretty-print — use the compact (no extra spaces) default output. -
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.
-
Send the hex string verbatim in the
Signaturerequest header.
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 };
403 signature is invalid.
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.
| Header | Value | Required |
|---|---|---|
| Authorization | Bearer <api_key> |
required |
| Signature | HMAC-SHA256 hex of the request body JSON string | required |
| Content-Type | application/json |
required |
| Field | Type | Required | Description |
|---|---|---|---|
| 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"
|
{
"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 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', } } );
{
"success": true,
"url": "{{base_url}}/wallet-code/eyJhbGciOi...",
"paymentRequestId": "req_abc123xyz",
"message": "Wallet code payment request created successfully",
"statusCode": 200
}
{
"success": false,
"message": "Request already exists!",
"url": "{{base_url}}/wallet-code/eyJhbGciOi..."
}
{
"success": false,
"message": "Missing required fields: referenceId, amount, cryptoCurrencyCode, merchantId",
"statusCode": 400
}
{
"status": 403,
"message": "signature is invalid"
// signature errors: "signature is missing" | "signature is invalid" | "signature verification failed"
// auth/role errors: "Forbidden"
}
| Field | Type | Description |
|---|---|---|
| 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. |
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.
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.
Signature header is not required for this endpoint (no body to sign). Only the Authorization header is needed.
| Header | Value | Required |
|---|---|---|
| Authorization | Bearer <api_key> |
required |
| Parameter | Type | Required | Description |
|---|---|---|---|
| requestId | string | required |
The paymentRequestId returned by POST /walletCode.
e.g. /getStatus/req_abc123xyz
|
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}`, } } );
{
"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)
}
{
"success": false,
"message": "Request not found"
// or: "Merchant not found"
}
{
"status": 403,
"message": "Forbidden"
}
status is one of created, processing, completed, expired. See the Status Lifecycle section for what each means and how a request transitions between them.
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.
Signature header is not required. Only the Authorization header is needed.
| Header | Value | Required |
|---|---|---|
| Authorization | Bearer <api_key> |
required |
| Parameter | Type | Required | Description |
|---|---|---|---|
| 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
|
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}` }, });
{
"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 }
}
{
"status": 403,
"message": "Forbidden"
}
| Field | Type | Description |
|---|---|---|
| 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. |
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.
Signature header is not required. Only the Authorization header is needed.
| Header | Value | Required |
|---|---|---|
| Authorization | Bearer <api_key> |
required |
| Parameter | Type | Required | Description |
|---|---|---|---|
| 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
|
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}` } } );
{
"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"
}
}
{
"success": false,
"message": "Request not found"
// or: "Merchant not found"
}
{
"status": 403,
"message": "Forbidden"
}
request)| Field | Type | Description |
|---|---|---|
| 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. |
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.
created │ customer opens the link and enters their wallet code ▼ processing │ payment authorised; awaiting on-chain settlement ▼ completed // 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
| Status | Meaning |
|---|---|
| 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. |
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.
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. |
| Header | Value |
|---|---|
| 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. |
wallet_code_processed{
"success": true,
"status": "wallet_code_processed",
"_id": "RQ1a2b3c4d5e", // your paymentRequestId
"prId": "PR1a2b3c4d5e", // internal ID
"referenceId": "ORDER-20240312-0042"
}
completed{
"success": true,
"status": "completed",
"_id": "RQ1a2b3c4d5e", // your paymentRequestId
"prId": "PR1a2b3c4d5e", // internal ID
"referenceId": "ORDER-20240312-0042"
}
| Field | Type | Description |
|---|---|---|
| success | boolean | Always true for these events. |
| status | string | The event: wallet_code_processed or completed. |
| _id | string | Your paymentRequestId (RQ…) — the same value returned by POST /walletCode. |
| prId | string | Internal payment request ID (PR…). |
| referenceId | string | Your reference ID for the request. |
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); });
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.
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); })();