A complete webhook delivery inspection and redelivery system that allows merchants to:
- Query the full history of webhook delivery attempts for any payment
- Manually re-send failed or pending webhook deliveries
- Verify the exact payload and delivery status of each attempt
Added three new functions:
WebhookDeliverystruct (serializable) to represent a delivery recordlist_webhook_deliveries()— queries all deliveries for a payment, ordered newest firstget_webhook_delivery()— fetches a specific delivery by ID- Helper function
row_to_webhook_delivery()to safely map database rows
Rationale: Kept the struct definition near the query functions for tight cohesion and easy maintenance.
Registered two new routes in the router:
GET /payments/:id/webhooksPOST /payments/:id/webhooks/:delivery_id/redeliver
Rationale: Routes are the contract between clients and handlers; keeping them centralized in the router makes the API surface clear.
Added two new handler functions:
list_webhooks()— retrieves delivery history with validationredeliver_webhook()— resends a delivery with fresh signature computation
Key decisions:
- Both handlers verify the payment exists first (fail-fast error handling)
redeliver_webhook()verifies the delivery belongs to the payment (prevents cross-payment access)- Re-signing uses the exact original payload bytes (preserves authenticity)
- Attempt count is incremented on every re-send attempt (audit trail)
- Returns
502 Bad Gatewaywhen the webhook delivery fails (semantically correct — downstream issue)
Added five comprehensive tests:
test_list_webhooks_not_found()— validates 404 for missing paymenttest_list_webhooks_empty()— validates empty list responsetest_redeliver_webhook_not_found()— validates 404 for missing payment during redelivertest_redeliver_delivery_not_found()— validates 404 for missing deliverytest_webhook_delivery_isolation()— validates deliveries are properly scoped to payments (security-critical)
Rationale: Isolation test is particularly important — it prevents subtle bugs where a merchant could inspect another merchant's webhooks.
Response includes payment_id at top level to give clients context about which payment the deliveries belong to.
The original payload is stored and re-sent as-is during redeliver. This means:
- Signature remains valid (re-computed using the original bytes)
- Merchant sees the exact event that was originally sent
- No re-serialization artifacts (decimal precision changes, etc.)
404 Not Found— resource doesn't exist or doesn't belong to the requester502 Bad Gateway— delivery failed (merchant's endpoint returned error or was unreachable)200 OK— redelivery succeeded
All error responses use the standard { "error": "message" } format for consistency with existing endpoints.
The isolation test validates that deliveries are properly scoped to payments. Once auth lands (#9), add merchant_id checks to prevent cross-tenant access. The structure is ready for this.
Tests follow the existing pattern:
- Use
test_server()to get an in-memory SQLite instance - Create test data via API calls
- Verify responses with
axum_testassertions - The isolation test manually inserts a delivery to test edge cases
The test suite covers:
- Happy path: empty list, successful operations
- Error paths: 404s for missing resources
- Security: cross-payment access prevention
- Merchant Scoping — Add
merchant_idfilter to prevent cross-tenant leaks - Response Logging — Store HTTP response code/body for debugging
- Pagination — Add limit/offset to list endpoint for high-volume payments
- Filtering — Allow filtering by status or date range
- Bulk Redelivery — Redelivery all failed deliveries for a payment in one call
- Delivery history is queryable per payment (
GET /payments/:id/webhooks) - Manual redelivery re-sends the original signed body (
POST /payments/:id/webhooks/:delivery_id/redeliver) - Records a new attempt on redeliver (attempts counter incremented)
- Endpoints return standard error shape for unknown IDs
- Tests cover list + redelivery + isolation