Skip to main content

Deposit attempts — client integration guide

This page is a complete integration reference for depositAttempts: card retry / decline history on each payment. Copy the examples below into your backend (webhook handler + API polling).


1. What is depositAttempts?

Every time a customer tries to pay (card declined, insufficient funds, expired card, then success), the payment provider records one deposit attempt. We sync that list and expose it as:

"depositAttempts": [ ... ]

Use it to:

  • Show payment retry history in your admin or customer UI
  • Display decline reasons (errorMessage) to support
  • Know when a payment is still retrying vs finally paid

Important rules:

RuleDetail
Full snapshotEach webhook/API response sends the entire array. Replace your stored list; do not merge deltas.
OrderNewest attempt is usually first in the array.
Always presentField is always an array — use [] when there are no attempts yet.
Updates trigger webhooksYou receive transaction.status_changed when attempts change, even if payment status is still processing.

2. Setup (one time)

Webhook

  1. Merchant dashboard → Organization → API & Webhooks
  2. Set Endpoint URL (your HTTPS endpoint)
  3. Save Webhook secret (shown once) — used to verify X-Webhook-Signature
  4. Optional: Send test event to confirm your handler returns 2xx

API key (for GET payment)

Same dashboard → create an API key. Use it for server-to-server calls:

Authorization: Api-Key YOUR_API_KEY

3. Webhook — receive depositAttempts

Request headers

HeaderValue
Content-Typeapplication/json
X-Webhook-Eventtransaction.status_changed
X-Webhook-Delivery-IdUUID — unique per delivery
X-Webhook-TimestampUnix time (seconds)
X-Webhook-Signaturesha256=<hex_hmac>

Verify signature (required)

Sign the raw request body (before JSON parse) with HMAC-SHA256 and your webhook secret.

Node.js

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}

// Express example
app.post('/webhooks/payments', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

const payload = JSON.parse(req.body.toString('utf8'));
if (payload.event === 'transaction.status_changed') {
handleTransactionUpdate(payload);
}

res.status(200).send('ok');
});

Python (Flask)

import hmac
import hashlib
import json
from flask import Flask, request

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"

def verify_webhook(raw_body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected)

@app.post("/webhooks/payments")
def payments_webhook():
raw = request.get_data()
sig = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(raw, sig):
return {"error": "invalid signature"}, 401

payload = json.loads(raw.decode("utf-8"))
if payload.get("event") == "transaction.status_changed":
handle_transaction_update(payload)

return {"ok": True}, 200

Full webhook payload (transaction.status_changed)

{
"event": "transaction.status_changed",
"transaction_id": "TXabc123",
"order_id": "org99-1781653725-2c883fe9",
"amount": "22.54",
"gross_amount": "23.00",
"fee_percent": 1.5,
"status": "paid",
"currency": "USD",
"depositAttempts": [
{
"status": "succeeded",
"paymentId": "pay_SnYcIPQHukxYtFtIrfLpw",
"attemptedAt": "2026-06-16T23:56:01.145Z",
"errorMessage": null,
"paymentMethod": "card"
},
{
"status": "requires_payment_method",
"paymentId": "pay_SnYcIPQHukxYtFtIrfLpw",
"attemptedAt": "2026-06-16T23:55:04.411Z",
"errorMessage": "Your card has insufficient funds.",
"paymentMethod": "card"
},
{
"status": "requires_payment_method",
"paymentId": "pay_SnYcIPQHukxYtFtIrfLpw",
"attemptedAt": "2026-06-16T23:52:26.333Z",
"errorMessage": "Your card was declined.",
"paymentMethod": "card"
},
{
"status": "requires_payment_method",
"paymentId": "pay_SnYcIPQHukxYtFtIrfLpw",
"attemptedAt": "2026-06-16T23:50:40.743Z",
"errorMessage": "Your card has expired.",
"paymentMethod": "card"
}
],
"created_at": "2026-06-16T23:49:07.004Z",
"updated_at": "2026-06-16T23:56:01.490Z"
}

Webhook field reference

FieldTypeDescription
eventstringAlways transaction.status_changed for payments
transaction_idstring | nullPublic ID — use with GET /api/transactions/<id>/. null if checkout canceled before payment
order_idstringSame as checkout_id from create payment — match your order in DB
amountstringNet amount (what you receive after fees)
gross_amountstringTotal charged to customer
fee_percentnumberOptional — your platform fee %
statusstringpaid | failed | pending | processing | canceled | refund_pending | refunded
currencystringUSD | EUR
depositAttemptsarrayFull attempt history — see below
created_atstringISO 8601
updated_atstringISO 8601

Webhook handler — save depositAttempts

function handleTransactionUpdate(payload) {
const key = payload.transaction_id || payload.order_id;
if (!key) return;

// Replace entire attempt list (not append)
const attempts = Array.isArray(payload.depositAttempts) ? payload.depositAttempts : [];

db.payments.upsert({
where: { externalId: key },
data: {
transactionId: payload.transaction_id,
orderId: payload.order_id,
status: payload.status,
netAmount: payload.amount,
grossAmount: payload.gross_amount,
currency: payload.currency,
depositAttempts: attempts, // store JSON array as-is
updatedAt: payload.updated_at,
},
});

// Optional: notify UI / support when latest attempt failed
const latest = attempts[0];
if (latest && latest.status !== 'succeeded' && latest.errorMessage) {
notifyDecline(payload.order_id, latest.errorMessage);
}
}
def handle_transaction_update(payload: dict) -> None:
key = payload.get("transaction_id") or payload.get("order_id")
if not key:
return

attempts = payload.get("depositAttempts") or []
if not isinstance(attempts, list):
attempts = []

db.upsert_payment(
external_id=key,
transaction_id=payload.get("transaction_id"),
order_id=payload.get("order_id"),
status=payload.get("status"),
net_amount=payload.get("amount"),
gross_amount=payload.get("gross_amount"),
currency=payload.get("currency"),
deposit_attempts=attempts, # replace full list
updated_at=payload.get("updated_at"),
)

latest = attempts[0] if attempts else None
if latest and latest.get("status") != "succeeded" and latest.get("errorMessage"):
notify_decline(payload.get("order_id"), latest["errorMessage"])

Canceled checkout (no transaction_id)

{
"event": "transaction.status_changed",
"transaction_id": null,
"order_id": "org1-1234567890-abc123",
"amount": "99.00",
"gross_amount": "99.00",
"status": "canceled",
"currency": "USD",
"depositAttempts": [],
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:05:00Z"
}

Match on order_id only.


4. GET payment — fetch depositAttempts

Use this to backfill, reconcile, or poll when you already have transaction_id from a webhook or checkout flow.

Request

GET /api/transactions/TXabc123/
Authorization: Api-Key YOUR_API_KEY

<id> can be public ID (TXabc123) or numeric internal id.

cURL

curl -sS -X GET "https://api.example.com/api/transactions/TXabc123/" \
-H "Authorization: Api-Key YOUR_API_KEY"

Full response (200)

{
"id": "TXabc123",
"amount": "23.00",
"net_amount": "22.54",
"status": "paid",
"currency": "USD",
"payer_email": "customer@example.com",
"products": [
{ "id": "prod_xyz", "name": "Pack starter saudi" }
],
"depositAttempts": [
{
"status": "succeeded",
"paymentId": "pay_SnYcIPQHukxYtFtIrfLpw",
"attemptedAt": "2026-06-16T23:56:01.145Z",
"errorMessage": null,
"paymentMethod": "card"
},
{
"status": "requires_payment_method",
"paymentId": "pay_SnYcIPQHukxYtFtIrfLpw",
"attemptedAt": "2026-06-16T23:55:04.411Z",
"errorMessage": "Your card has insufficient funds.",
"paymentMethod": "card"
}
],
"fee_percent": 1.5,
"amount_breakdown": {
"gross_ttc": "23.00",
"currency": "USD",
"net_usd": "22.54"
},
"created_at": "2026-06-16T23:49:07.004Z",
"updated_at": "2026-06-16T23:56:01.490Z"
}

GET field reference (payment + attempts)

FieldDescription
idPublic payment ID (TX…)
amountGross total charged (TTC)
net_amountNet to merchant after fees
statusPayment status
depositAttemptsSame array shape as webhook
payer_emailCustomer email when available
products[{ "id", "name" }]
created_at / updated_atISO 8601

Fetch and sync (Node.js)

async function syncPaymentAttempts(transactionId) {
const res = await fetch(
`https://api.example.com/api/transactions/${transactionId}/`,
{ headers: { Authorization: `Api-Key ${process.env.API_KEY}` } }
);
if (!res.ok) throw new Error(`GET payment failed: ${res.status}`);

const payment = await res.json();
const attempts = payment.depositAttempts ?? [];

await db.payments.update({
where: { transactionId: payment.id },
data: {
status: payment.status,
depositAttempts: attempts,
updatedAt: payment.updated_at,
},
});

return { payment, attempts };
}

Fetch and sync (Python)

import requests

def sync_payment_attempts(transaction_id: str) -> dict:
url = f"https://api.example.com/api/transactions/{transaction_id}/"
headers = {"Authorization": f"Api-Key {API_KEY}"}
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
payment = resp.json()
attempts = payment.get("depositAttempts") or []

db.update_payment(
transaction_id=payment["id"],
status=payment["status"],
deposit_attempts=attempts,
updated_at=payment.get("updated_at"),
)
return {"payment": payment, "attempts": attempts}

5. depositAttempts[] item schema

Each object in the array:

FieldTypeDescription
statusstringsucceeded, requires_payment_method, failed, etc.
paymentIdstringProvider payment id (e.g. pay_…)
attemptedAtstringISO 8601 timestamp of this attempt
errorMessagestring | nullHuman-readable decline reason; null on success
paymentMethodstringe.g. card, CARD

TypeScript types (optional)

type DepositAttempt = {
status: string;
paymentId: string;
attemptedAt: string;
errorMessage: string | null;
paymentMethod: string;
};

type TransactionWebhookPayload = {
event: 'transaction.status_changed';
transaction_id: string | null;
order_id?: string;
amount: string;
gross_amount: string;
fee_percent?: number;
status: string;
currency: string;
depositAttempts: DepositAttempt[];
created_at: string | null;
updated_at: string | null;
};

type PaymentApiResponse = {
id: string;
amount: string;
net_amount: string;
status: string;
currency: string;
payer_email: string;
products: { id: string; name: string }[];
depositAttempts: DepositAttempt[];
created_at: string;
updated_at: string;
};

6. End-to-end flow

1. POST /api/checkout/create/     → store checkout_id (order_id)
2. Customer pays / retries card
3. Your webhook receives → transaction.status_changed + depositAttempts[]
4. You save depositAttempts → replace full array on each delivery
5. Optional: GET /transactions/ → same depositAttempts if you need to reconcile
6. Payment status = paid → latest attempt usually status = succeeded

Correlation keys

Your dataOur field
Checkout / order you createdorder_id
Payment id for API callstransaction_id (TX…)

7. UI suggestions

function renderAttempts(attempts) {
return attempts.map((a) => ({
at: a.attemptedAt,
ok: a.status === 'succeeded',
label: a.errorMessage || a.status,
method: a.paymentMethod,
}));
}
  • Show newest first (array order from API)
  • Highlight errorMessage for failed attempts
  • When status === 'paid', expect at least one attempt with status === 'succeeded'

8. Manual resend & testing

ActionWhere
Resend webhook with latest depositAttemptsDashboard → Payments → open payment → Resend webhook
Test endpoint + signatureDashboard → Organization → WebhooksSend test event

Respond with HTTP 2xx from your webhook URL or delivery may retry.


9. Checklist

  • Webhook URL + secret configured
  • Handler verifies X-Webhook-Signature on raw body
  • Handler reads depositAttempts on every transaction.status_changed
  • You replace stored attempts (not append)
  • You match payments by transaction_id and/or order_id
  • GET /api/transactions/<id>/ used for backfill / reconcile
  • UI shows errorMessage + attemptedAt for failed tries