API, webhooks and backend Technical case study Published July 14, 2026 8 min read

Case study: Laravel and Django payment backend with webhooks and refunds

How to design a payment backend that survives repeated events, network failures and manual operations without delivering an order twice.

Case study: Laravel and Django payment backend with webhooks and refunds

Article focus

Case study: Laravel and Django payment backend with webhooks and refundsAPI, webhooks and backendLaravel payment gateway ArmeniaDjango payment integration Armeniapayment webhook

Starting point: one contract between the order and provider

Backend must not pass an arbitrary browser amount to a provider or change an order without a verifiable payment state.

Order API -> payment attempt -> provider checkout
Laravel or Django holds internal order state separately from an external payment state.

The server validates the order, amount, currency and allowed action before it creates a payment attempt with an internal id and idempotency key. Only then does it create a safe redirect or checkout URL.

Internal order id, payment attempt id and provider payment id are stored together. This makes retries safe and lets support and accounting trace operations without guessing.

  • Do not trust a final payment amount from the frontend.
  • Keep order, payment attempt and refund as separate entities.
  • Lock currency and rounding rules on backend.
  • Return the same create-payment result for the same idempotency key.

Implementation: state machine and idempotent webhook handlers

A webhook can arrive more than once, later than return URL or out of order, so business state changes only through allowed transitions.

Pending -> paid or failed -> refund review
Every event is verified, retained and evaluated against an allowed transition table.

Backend validates signature, amount, currency, correlation id and event source, then checks whether the received transition is allowed. A late failed callback cannot replace paid, and a repeated paid event cannot deliver the order twice.

A Laravel job or Django worker can perform secondary actions asynchronously: notifications, CRM sync, fiscal workflow or access delivery. The critical webhook response stays fast even when a secondary system is unavailable.

  • Store raw event and normalized verification result.
  • Separate webhook intake from heavy background actions.
  • Use retry policy and manual review for unresolved mismatches.
  • Do not execute irreversible actions before verified paid.

After launch: refunds, observability and reconciliation

The team needs a ledger for payment attempts, refunds, retries and differences from a provider report.

Payment ledger -> webhook log -> reconciliation report
Each operation is discoverable by internal id, external id and correlation id without exposing sensitive data.

A refund starts by validating the original payment, available amount and operator role. After the request is sent, backend creates a separate refund record and confirms the final result with the same safe status mechanism.

Structured logs, queue metrics and daily provider-report reconciliation expose not only API errors but also payments that succeeded at a provider yet did not reach an internal system.

  • Do not log card data or secrets.
  • Let operators search by order id, payment id and correlation id.
  • Separate initiated from confirmed refunds.
  • Track webhook retries and manual-review tasks separately.

FAQ

Do we need a payment attempt if an order already exists?

Yes. Order and payment attempt have different lifecycles: one order can have several attempts, a cancellation or a refund.

How should a repeated webhook be handled?

Validate its signature and event id, retain it for audit, then apply only an allowed idempotent state transition.

Can CRM sync run inside a webhook?

Prefer accepting and validating the event quickly, then run CRM sync through a durable queue with retry and error logging.