Building a Payments Integration Layer That Keeps Venue Owners in Control
At checkout, “paid” is an unusually expensive word to get wrong. An order can exist, a payment sheet can be open, and a customer can have approved a wallet prompt; none of those facts means the venue should prepare the drinks.
Problem
The business requirement was simple to say and harder to implement cleanly:
- Venues own payouts.
- Venues control refunds.
- The platform may collect its own fee, but it should not hold funds.
- Staff need a clear status view when something is not connected or has failed.
That means the system has to track payment state without pretending that the SaaS is the merchant of record.
Constraints and risks
The obvious risks are webhook failure, expired payment connections, partial setup states, and ambiguous admin messaging. If a venue is half-connected, the UI needs to say so plainly instead of showing a green tick that does not match reality.
There is also a security concern: payment credentials and webhook secrets must stay server-side, and provider-specific details should not leak into the browser.
Design / Investigation
I treat the provider as an integration behind a clean interface:
type PaymentProvider interface {
CreateAccount(ctx context.Context, venueID string) (Connection, error)
RefreshStatus(ctx context.Context, venueID string) (Status, error)
IssueRefund(ctx context.Context, paymentID string, amount int64) error
HandleWebhook(ctx context.Context, body []byte, signature string) error
}
That keeps the business model separate from the provider implementation. The UI can ask "is this venue ready to take payments?" without knowing how the provider names its accounts or event types.
I also model the connection state explicitly:
| State | Meaning |
|---|---|
disconnected |
No payment provider connection exists |
pending |
Setup is not complete yet |
active |
Payments can be processed |
restricted |
The provider reports an issue |
expired |
The venue needs to reconnect |
Implementation approach
The integration layer does a few boring but important things well:
- Verifies webhook signatures before reading event payloads.
- Uses idempotency keys for retried calls.
- Persists state transitions before updating the UI.
- Reconciles provider state instead of assuming every webhook arrives in order.
Plain-English admin copy matters here. A staff member does not need provider terminology. They need to know whether payments are live, waiting, disconnected, or restricted.
Checklist I use before shipping a payments flow:
- Is the venue in control of payouts?
- Are refunds traceable and reversible?
- Can a failed webhook be replayed safely?
- Does the admin UI explain the failure in plain language?
- Are secrets and signatures server-side only?
What I would do differently
I would define the payment lifecycle earlier, before the UI accumulates assumptions about what "connected" means. I would also keep a better reconciliation report so support work is less guessy when provider state and local state drift apart.
The decision that changed
The current guest flow creates an order, has the server-side payments service create a Stripe PaymentIntent, and opens embedded wallet-first checkout with card fallback. Funds settle through Stripe Connect to the pub owner’s connected account. Orders persist provider, flow, reference, amount, status and failure information; the webhook is authoritative, with a scoped status lookup as a recovery signal.
I now treat a payment UI as a view of a state machine, not as evidence that money moved. The extra states make the code and screen less neat, but they give the venue and the guest a way to recover when the tidy version of events is not true.