Back to Blog
Technology

Stop Phantom Deposits: A Practical Idempotency Blueprint for Brokerage Webhooks

Priya DesaiPriya Desai
April 19, 20267 min read10 views
Stop Phantom Deposits: A Practical Idempotency Blueprint for Brokerage Webhooks

Webhooks are the backbone of modern brokerage payment integrations—but they’re also a common source of “phantom deposits” and expensive double-credits. Retries, out-of-order delivery, duplicate events, and partial failures are normal behavior in webhook systems.

This post lays out a practical, implementation-ready approach to designing idempotent deposit flows for brokers and prop firms: how to structure your ledger, how to process events safely, and how to prove what happened when ops or compliance asks.

Why deposit webhooks double-credit in real life

Most double-credits aren’t caused by “bad code” so much as normal distributed-systems behavior meeting an under-specified deposit flow.

Common patterns that create duplicate credits:

  • Gateway retries: provider times out waiting for your 200 OK, so it resends the same event.
  • Multiple event types: e.g., payment_authorized, payment_captured, payment_succeeded—your system credits on more than one.
  • Out-of-order delivery: “succeeded” arrives before “pending,” or “chargeback” arrives late.
  • Split-brain processing: two app instances consume the same webhook concurrently.
  • Manual ops actions: support replays a webhook or “reprocesses” a deposit without guardrails.

In brokerage payments, the blast radius is larger than typical e-commerce: you’re not just shipping a product; you’re changing trading equity, which impacts margin, risk exposure, and potentially regulatory reporting. Always check local regulations and align your controls with your compliance program.

The non-negotiables: ledger-first design and a deposit state machine

If you want idempotency to be reliable, you need two foundational building blocks:

1) A ledger-first deposit modelTreat every deposit as a ledger entry (or a small set of entries) with immutable facts:

  • deposit_id (internal)
  • provider + provider_transaction_id
  • client_id + trading_account_id
  • amount, currency
  • status (state machine)
  • created_at, updated_at

Avoid “just update balance” as your primary record. Balance should be derived from ledger movements or, at minimum, be updated only via controlled balance operations with a corresponding ledger/audit record.

2) A strict deposit state machineDefine the only allowed transitions, for example:

  • createdpendingsucceeded
  • pendingfailed
  • succeededrefunded / chargeback

Then enforce it in code. Webhook handlers should attempt a transition; if the transition is invalid (e.g., already succeeded), the handler should become a no-op.

This is the core of “idempotent deposit flows”: same input event, same final state, no extra side effects.

Idempotency keys: what to key on (and what not to)

Idempotency is only as good as the key you choose.

Best practice: key on the provider’s immutable transaction identifier.

  • Example unique constraint: (provider, provider_transaction_id)

If your provider doesn’t give a stable transaction ID, derive one carefully:

  • Prefer a provider event ID that is guaranteed unique per transaction lifecycle.
  • If you must hash, include stable fields (provider, merchant account, amount, currency, client reference) and document the assumptions.

What not to key on:

  • Timestamp (duplicates can share timestamps; retries can arrive later)
  • Client ID alone (clients can deposit multiple times)
  • Amount alone (same amount repeats frequently)

Implementation detail that matters: store an idempotency record (or use the deposit table itself) that makes it impossible to insert the same (provider, provider_transaction_id) twice.

A safe webhook processing pattern (with concurrency and retries)

A robust webhook handler is less about “parsing JSON” and more about transaction boundaries.

A practical pattern that works well for brokerage deposits:

  1. Verify authenticity
  • Validate signature/HMAC, timestamps, replay windows.
  • Reject or quarantine invalid payloads.
  1. Persist the raw event first (in an “inbox” table)
  • Store provider_event_id, provider_transaction_id, payload, received time.
  • Add a unique constraint on provider_event_id if available.
  1. Acquire a lock per transaction
  • DB row lock (SELECT ... FOR UPDATE) on the deposit row, or
  • Distributed lock keyed by (provider, provider_transaction_id).
  1. Upsert the deposit
  • Insert if missing; otherwise load existing.
  • Enforce unique constraint on (provider, provider_transaction_id).
  1. Apply state transition
  • Map provider status/event → your state machine.
  • If transition is invalid or already applied, return 200 OK (idempotent no-op).
  1. Credit exactly once
  • Only credit on the single state transition into succeeded.
  • Record a credited_at, credit_operation_id, or ledger_entry_id.
  • Guard with an additional constraint like deposit_id unique in your ledger movements.
  1. Acknowledge quickly; process asynchronously when needed
  • If provider timeouts are common, consider: store + enqueue + 200 OK, then process in a worker.

This design makes retries safe: the second webhook hits the unique constraints and state-machine rules and becomes a no-op.

Preventing double-credits when posting to MT4/MT5 or an internal wallet

In broker systems, the “credit” step often means:

  • Posting a balance operation to a trading platform (e.g., MT5 Manager API), or
  • Crediting an internal wallet that later syncs to the trading platform.

Either way, treat the credit as an external side effect and make it idempotent too.

Controls that help in practice:

  • Write-ahead intent: before calling the platform, create a credit_attempt record tied to deposit_id.
  • Exactly-once guard: enforce UNIQUE(deposit_id) in the table that stores the platform credit operation.
  • Outbox pattern: commit deposit state + outbox message in one DB transaction; a worker delivers the platform credit. If delivery retries, the unique guard prevents duplicates.
  • Reconciliation hook: if the platform call returns “unknown” (timeout), mark as credit_pending and reconcile by querying platform history before retrying.

Operationally, this is where many teams get burned: a timeout is treated as a failure, the job retries, and the platform actually processed the first request. Your system must assume timeouts are ambiguous, not failures.

Reconciliation and audit: how ops proves “what happened”

Even with good idempotency, you need controls for the messy cases: chargebacks, partial captures, provider disputes, and manual adjustments.

A lightweight reconciliation setup for brokerage payments:

  • Daily provider report match: sum of succeeded deposits by currency vs provider settlement reports.
  • Exception queues:
    • succeeded in provider, not credited internally
    • credited internally, not succeeded in provider
    • chargeback/refund events without a matching original deposit
  • Immutable event timeline: keep raw webhook payloads and transition logs (who/what/when).

From a compliance perspective, ensure you can produce:

  • The original client deposit request (UI/API)
  • The provider transaction reference
  • The webhook events received
  • The internal ledger entry / platform credit reference

Check local regulations on record retention, audit logging, and financial reporting, and align with your AML/KYC and transaction monitoring policies.

A practical checklist: idempotent deposit flows you can ship

Use this as a release checklist for your payments + CRM/backoffice team:

  • Unique constraint on (provider, provider_transaction_id)
  • Raw webhook “inbox” table with dedupe on provider_event_id
  • Deposit state machine with enforced transitions
  • Credit happens only on transition into succeeded
  • Exactly-once guard for credit (UNIQUE(deposit_id) in credit/ledger table)
  • Timeout handling treats platform/provider calls as ambiguous
  • Outbox + worker for side effects (platform credit, notifications)
  • Replay tooling that is safe by design (replays become no-ops)
  • Reconciliation jobs + exception queues
  • Audit trail: event timeline + operator actions

The Bottom Line

Webhook retries and duplicates are expected—double-credits are not. The fix is a ledger-first deposit model, a strict state machine, and idempotency enforced at the database level.

Design your flow so every webhook can be processed multiple times without changing the outcome, and make the “credit” step exactly-once with clear audit evidence.

If you’re building or hardening your brokerage payments stack, Brokeret can help you implement these controls cleanly across CRM, payments, and platform integrations—start here: /get-started.

Share:TwitterLinkedIn