Webhook Processing System
A 1-hour pairing session — build a small end-to-end system without unnecessary complexity. We care about how you reason, not how fast you finish.
What you will build
A Webhook Processing System for payment-related events — three components working together end-to-end.
The three deliverables
-
1. HTTP endpoint
POST /webhooks/payment— accepts JSON webhooks describing payment lifecycle updates. Validates the payload and handles duplicate deliveries correctly. -
2. Payments store
One record per logical payment (payment_id), updated as new events arrive. The same notification may arrive more than once — your service must handle this correctly. -
3. Minimal web UI
One page showing the list of payments with filtering and summary information.
Stack
- Any language, framework, and AI tools you normally use
- Vanilla HTML/JS is fine for the UI
- No real payment provider integration
- No user login required
Business context
A payments provider notifies your app when something changes — e.g. a charge succeeds or fails. The same HTTP notification may arrive more than once (retries, timeouts, duplicate delivery). Your service must handle this correctly.
Before you write a single line of code
Answer in 2–3 sentences: What is your data model, and how will you handle duplicate deliveries of the same notification?
How we evaluate this
This is a pairing session, not a speed test. Work incrementally: before writing code, walk us through your plan. At each step, explain what you are building and why — data model choices, edge cases, trade-offs. We care about how you reason more than how fast you finish.
Payment Events & Payload
Each webhook body describes one notification about a payment. The same notification may be POSTed again with the same event_id.
Required payload attributes
| Attribute | Type | Description |
|---|---|---|
event_id |
string | Unique notification ID. Retries reuse the same event_id. |
payment_id |
string | Logical payment. All webhooks for the same charge share this id. |
event |
string | Type of payment event — must be one of the allowed values. |
amount |
number | Monetary amount in minor currency units/cents (e.g. 25000 = $250.00). Must be ≥ 0 integer. |
currency |
string | ISO 4217 code, uppercase (e.g. USD, EUR). |
user_id |
string | Customer identifier in your system (opaque string). |
timestamp |
number | When the provider emitted the event: Unix time in seconds. |
Allowed values for event
| Value | Meaning |
|---|---|
payment.pending |
Payment initiated; not yet settled. |
payment.completed |
Payment successfully captured. |
payment.failed |
Payment did not complete. |
payment.refunded |
A refund was applied. |
Example payloads
Completed payment{
"event_id": "evt_7f3a9c2b",
"payment_id": "pay_1001",
"event": "payment.completed",
"amount": 25000,
"currency": "USD",
"user_id": "user_881",
"timestamp": 1710000000
}
Earlier event — same payment{
"event_id": "evt_1a2b3c4d",
"payment_id": "pay_1001",
"event": "payment.pending",
"amount": 25000,
"currency": "USD",
"user_id": "user_881",
"timestamp": 1709999000
}
Failed payment{
"event_id": "evt_8b21d4e1",
"payment_id": "pay_1002",
"event": "payment.failed",
"amount": 9900,
"currency": "EUR",
"user_id": "user_442",
"timestamp": 1710003600
}
API & Persistence
Two endpoints to implement. The idempotency requirement is the core challenge — figure out the right behavior before writing any code.
POST /webhooks/payment
- Body: JSON matching the payload spec
- On success: return a JSON response with a 2xx status
- On invalid payload: return an appropriate 4xx response
The key question
The system must handle the case where the same notification is delivered more than once. Figure out the right behavior — there is a correct answer, and it has consequences for your schema design.
GET /payments
- Response: JSON array with one object per
payment_id, showing the current state of each payment - The array represents the latest known state — not the full history
Data model considerations
- What does your
paymentstable / collection look like? - How do you detect a duplicate
event_id? - What happens when two events arrive for the same
payment_id? - Out-of-order delivery: a
payment.failedarrives beforepayment.pending— what do you store?
Work at a sustainable pace
- Prioritize a working vertical slice over polish
- Use any language, framework, and AI tools you normally use
- We care that you can integrate pieces, validate behavior, and explain your choices
AI as a pairing tool
Use AI to accelerate implementation of ideas you already understand — not as a replacement for reasoning. If you can't explain why you wrote something, it will come up in the pairing discussion.
Frontend — Mandatory
One page that fetches GET /payments and displays the list. Stack is your choice — vanilla HTML/JS is fine. Beyond the basics, it must include these five features.
Required features
-
Status filter
Allow the user to view all payments or filter by a specific status. Your choice of UI — tabs, buttons, dropdown. -
Summary bar
Show total payment count per status and total amount grouped by currency. E.g. "3 Completed · $750.00 USD". -
Formatted amounts
Display currency values properly —$250.00 USD, not250. -
Relative timestamps
Show when each payment was last updated as a relative time (e.g. "2 min ago") that updates live without a page reload. -
Refresh indicator
Make it clear to the user when data was last fetched and when a fetch is in progress.
Auto-refresh
Auto-refresh interval is your call — be ready to justify it. There is no single right answer; what matters is that you understand the trade-off between freshness and request load.
What "working" means
- Sending a
POST /webhooks/paymentand then refreshing the UI shows the new payment - Sending the same
event_idtwice does not create a duplicate entry in the list - The status filter actually changes what is displayed
- The summary bar reflects the filtered or unfiltered total (your choice — be consistent)
Prioritize function over form
A plain unstyled page that works correctly beats a polished UI that crashes on the first duplicate webhook. Polish is a bonus, not a requirement.
Optional Bonus & Non-goals
Scope clearly defined — so you know exactly where not to spend time.
Optional — if time remains
Webhook audit log
Store each accepted webhook delivery as a database record associated with its payment_id. If implemented, expose it via:
GET /payments/:id/events
You decide the schema and what fields are worth keeping. This is the only optional deliverable.
Why the audit log is optional
The three mandatory deliverables already test the most important behaviors: endpoint design, idempotency logic, and UI integration. The audit log adds complexity without changing the core challenge — so it's only worth building if everything else is solid and working.
Explicit non-goals
- No real payment provider integration
- No user login or authentication
- No queues, workers, or microservices
- No full history of status changes per payment (unless you implement the optional audit log)
- No deployment — running locally is fine
- No fancy UI styling or animations
Time management
A working vertical slice beats an ambitious system with broken pieces. If you're 40 minutes in and the POST endpoint isn't working yet, stop adding features and make what you have solid.
How We Evaluate
This is a pairing session, not an exam. We are watching how you think, not just what you produce.
What we look for
-
Incremental approach
You work step by step — skeleton first, then behavior, then edge cases. You don't disappear for 20 minutes and come back with a complete system. -
Explaining before building
Before writing any code, you walk us through your plan: data model, idempotency strategy, API shape, and UI approach. We ask questions; you engage with them. -
Validating behavior
You test as you go — sending test payloads, verifying responses, checking the UI reflects what the API returns. You don't just write and hope. -
Explainability of choices
Every decision has a reason. Column name, HTTP status code, filter behavior — if we ask why, you can answer.
The data model question
Answer before writing code
"What is your data model, and how will you handle duplicate deliveries of the same notification?"
There is a correct answer. Think it through before reaching for the keyboard.
What does not matter
- How polished the UI looks
- Whether you finish all features in time
- Which language or framework you chose
- Whether you used AI — we expect you to
Strong signal
- Explains trade-offs unprompted
- Tests edge cases proactively
- Adjusts plan when stuck
- Asks clarifying questions
Weak signal
- Silent for long stretches
- Can't explain what AI wrote
- Adds features before core works
- Never validates behavior