Browse Templates/Payment Gateway

Payment Gateway

A robust payment gateway database schema for building payment processing platforms like Stripe or PayPal. Includes merchant onboarding and management, multi-method payment processing (cards, bank transfers, digital wallets), detailed transaction tracking with idempotency keys, refund workflow management, and webhook event logging for asynchronous notifications. Ideal for fintech companies, payment aggregators, or e-commerce payment infrastructure.

PostgreSQL10 tablesFinance
financepaymentsgatewaymerchantsrefundswebhooks
Created with ERD Studio

About this schema

transactions.idempotency_key is the most important column here. A payment request that times out will be retried, and without a key that the gateway can recognise, the retry charges the customer a second time. Storing it makes the second attempt return the first result instead of creating a new charge.

api_keys carries a mode column, which is what separates test traffic from live. The same merchant holds keys for both, and every transaction made with a test key must be invisible to settlements — the alternative is a sandbox that quietly contaminates real payouts.

Webhook delivery is stored rather than attempted and forgotten. webhook_events keeps the payload, the http_status and the attempts count, because a merchant whose endpoint was down for an hour needs those events replayed, and a support question about a missing notification needs an answer.

Key relationships

merchants → transactions → refunds / disputes
Everything downstream of a payment attaches to the transaction, never to the merchant directly, so each one names the charge it concerns.
transactions → payment_methods
The stored method holds a token and last_four — enough to charge again and to display, and nothing that is worth stealing.
merchants → webhooks → webhook_events
The registration and each delivery attempt. Keeping the attempts means a retry is possible and a failure is explainable.
merchants → settlements
Payouts are per period with fee_amount recorded beside the gross, so what the merchant received and what was withheld are both auditable.
merchants → api_keys
Only a hash and a key_prefix are stored. The prefix is what lets a merchant identify a key in a list they can no longer read in full.
merchants → merchant_settings
Configuration split from identity, so webhook_secret and payout_schedule change without touching the merchant record.

Design decisions

Idempotency is enforced in the database

Checking for a duplicate in application code leaves a race between two concurrent retries. A unique constraint on the merchant and idempotency_key makes the second insert fail, and the handler returns the original transaction instead. That turns a double charge from a bug that is hard to reproduce into an outcome the database makes impossible.

Live and test share one set of tables

A separate sandbox database is cleaner and diverges from production within weeks. One schema with mode on the api_key means test traffic exercises exactly the same code path. The burden moves to every query that touches money — settlements, reporting, dashboards must filter correctly, and a single missed filter puts test charges into a real payout.

Webhook events are rows, not fire-and-forget

An HTTP POST that times out is indistinguishable from one that was never sent, and the merchant's reconciliation depends on receiving every event. Storing payload, http_status, attempts and status makes delivery a queue with retries and gives support a record. The cost is a table that grows with every event and needs its own retention policy.

Settlements are rows, not a computed balance

What a merchant is owed could be summed from transactions minus refunds and fees. Once money has actually been sent, that sum is no longer the answer — a later refund or fee change would retroactively alter a completed payout. The settlement row records what was actually transferred for a period, which is the figure the bank statement will match.

Tables in this template

merchantsMerchants
7 cols
merchant_settingsMerchant Settings
7 cols
payment_methodsPayment Methods
8 cols
transactionsTransactions
10 cols
refundsRefunds
6 cols
disputesDisputes
8 cols
webhooksWebhooks
6 cols
webhook_eventsWebhook Events
8 cols
settlementsSettlements
10 cols
api_keysAPI Keys
9 cols

Frequently asked

How does idempotency actually prevent a double charge?
The merchant sends a key with the request. A unique constraint on merchant_id and idempotency_key means a retry cannot insert a second row, so the gateway returns the existing transaction and its original result.
Why store a token instead of card details?
Because the token is useless outside this gateway's relationship with the processor, while a card number is useful to anyone. last_four exists only so the customer recognises which card they picked.
What is key_prefix for?
So a merchant can tell their keys apart in a list. The full key is shown once and only a hash is kept, which means the prefix is the only thing left to identify a key you want to revoke.
Can a transaction be refunded more than once?
Yes — refunds is a separate table, so partial refunds accumulate. The gateway must check that the total refunded does not exceed the transaction amount, since the schema does not enforce it.
How are disputes different from refunds?
A refund is the merchant returning money voluntarily. A dispute is the cardholder's bank reclaiming it, which is why that table carries evidence and a due_date — there is a deadline to respond, and missing it decides the outcome.