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:
| Rule | Detail |
|---|---|
| Full snapshot | Each webhook/API response sends the entire array. Replace your stored list; do not merge deltas. |
| Order | Newest attempt is usually first in the array. |
| Always present | Field is always an array — use [] when there are no attempts yet. |
| Updates trigger webhooks | You receive transaction.status_changed when attempts change, even if payment status is still processing. |
2. Setup (one time)
Webhook
- Merchant dashboard → Organization → API & Webhooks
- Set Endpoint URL (your HTTPS endpoint)
- Save Webhook secret (shown once) — used to verify
X-Webhook-Signature - 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
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Event | transaction.status_changed |
X-Webhook-Delivery-Id | UUID — unique per delivery |
X-Webhook-Timestamp | Unix time (seconds) |
X-Webhook-Signature | sha256=<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
| Field | Type | Description |
|---|---|---|
event | string | Always transaction.status_changed for payments |
transaction_id | string | null | Public ID — use with GET /api/transactions/<id>/. null if checkout canceled before payment |
order_id | string | Same as checkout_id from create payment — match your order in DB |
amount | string | Net amount (what you receive after fees) |
gross_amount | string | Total charged to customer |
fee_percent | number | Optional — your platform fee % |
status | string | paid | failed | pending | processing | canceled | refund_pending | refunded |
currency | string | USD | EUR |
depositAttempts | array | Full attempt history — see below |
created_at | string | ISO 8601 |
updated_at | string | ISO 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)
| Field | Description |
|---|---|
id | Public payment ID (TX…) |
amount | Gross total charged (TTC) |
net_amount | Net to merchant after fees |
status | Payment status |
depositAttempts | Same array shape as webhook |
payer_email | Customer email when available |
products | [{ "id", "name" }] |
created_at / updated_at | ISO 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:
| Field | Type | Description |
|---|---|---|
status | string | succeeded, requires_payment_method, failed, etc. |
paymentId | string | Provider payment id (e.g. pay_…) |
attemptedAt | string | ISO 8601 timestamp of this attempt |
errorMessage | string | null | Human-readable decline reason; null on success |
paymentMethod | string | e.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 data | Our field |
|---|---|
| Checkout / order you created | order_id |
| Payment id for API calls | transaction_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
errorMessagefor failed attempts - When
status === 'paid', expect at least one attempt withstatus === 'succeeded'
8. Manual resend & testing
| Action | Where |
|---|---|
Resend webhook with latest depositAttempts | Dashboard → Payments → open payment → Resend webhook |
| Test endpoint + signature | Dashboard → Organization → Webhooks → Send test event |
Respond with HTTP 2xx from your webhook URL or delivery may retry.
9. Checklist
- Webhook URL + secret configured
- Handler verifies
X-Webhook-Signatureon raw body - Handler reads
depositAttemptson everytransaction.status_changed - You replace stored attempts (not append)
- You match payments by
transaction_idand/ororder_id - GET
/api/transactions/<id>/used for backfill / reconcile - UI shows
errorMessage+attemptedAtfor failed tries
10. Related docs
- Webhooks — all events and headers
- Get payment by id — payment API
- Create payment — start checkout