Problem Statement
Outbound webhooks in DevTrack dispatch a structured payload of type WebhookPayload (matching the shape { event: string, timestamp: string, data: Record<string, unknown> }). When a webhook delivery fails, this complete payload structure is logged in the webhook_deliveries table under the payload column.
When a developer clicks "Retry Delivery" in the webhooks dashboard, the retry route handler /api/webhooks/custom/[id]/deliveries/[deliveryId]/retry/route.ts reads this logged payload from the database and forwards it directly as the third parameter (data) to dispatchWebhook. Because dispatchWebhook automatically wraps its data argument in a new WebhookPayload shell, the final HTTP POST request is sent with a corrupt, nested structure:
{
"event": "goal.completed",
"timestamp": "2026-07-04T10:00:00.000Z",
"data": {
"event": "goal.completed",
"timestamp": "2026-07-03T12:00:00.000Z",
"data": {
// Original data is nested here
}
}
}
This violates the API contract and breaks integrations that parse the webhook payload structure.
tch.
Proposed Solution
Extract and forward the nested data block of the original payload during redelivery:
- In the retry route handler
src/app/api/webhooks/custom/[id]/deliveries/[deliveryId]/retry/route.ts, parse the delivery.payload database column.
- Check if the parsed payload follows the standard
WebhookPayload structure (i.e. contains a top-level data key).
- If it does, extract
payloadData.data and pass that extracted dictionary to dispatchWebhook instead of the full root object:
// Extract original inner data to prevent double-nesting on dispatch
const originalData = (payloadData && typeof payloadData === "object" && "data" in payloadData)
? (payloadData.data as Record<string, unknown>)
: payloadData;
const result2 = await dispatchWebhook(id, delivery.event, originalData);
- This ensures that when
dispatchWebhook constructs the new payload, it wraps only the original event fields.
Feature Area
API / Backend
Alternatives Considered
- Modifying
dispatchWebhook: We considered modifying the core helper in src/lib/webhooks.ts to detect if the incoming data is already wrapped in a WebhookPayload. However, dispatchWebhook is called from various live events (goals, streaks, achievements) that always supply raw dictionaries. Adding check-logic there introduces unnecessary complexity to a critical path. Normalizing the input at the retry route boundary is cleaner and safer.
Acceptance Criteria
- Outbound webhook retries dispatch a payload structure that matches the original dispatch payload format (no nested
event/timestamp keys inside the data object).
- Unit tests exist covering retry payload formatting, ensuring that the payload remains properly un-nested during a simulated retry dispatch.
Additional Context
No response
Problem Statement
Outbound webhooks in DevTrack dispatch a structured payload of type
WebhookPayload(matching the shape{ event: string, timestamp: string, data: Record<string, unknown> }). When a webhook delivery fails, this complete payload structure is logged in thewebhook_deliveriestable under thepayloadcolumn.When a developer clicks "Retry Delivery" in the webhooks dashboard, the retry route handler
/api/webhooks/custom/[id]/deliveries/[deliveryId]/retry/route.tsreads this logged payload from the database and forwards it directly as the third parameter (data) todispatchWebhook. BecausedispatchWebhookautomatically wraps itsdataargument in a newWebhookPayloadshell, the final HTTP POST request is sent with a corrupt, nested structure:{ "event": "goal.completed", "timestamp": "2026-07-04T10:00:00.000Z", "data": { "event": "goal.completed", "timestamp": "2026-07-03T12:00:00.000Z", "data": { // Original data is nested here } } }This violates the API contract and breaks integrations that parse the webhook payload structure.
tch.
Proposed Solution
Extract and forward the nested
datablock of the original payload during redelivery:src/app/api/webhooks/custom/[id]/deliveries/[deliveryId]/retry/route.ts, parse thedelivery.payloaddatabase column.WebhookPayloadstructure (i.e. contains a top-leveldatakey).payloadData.dataand pass that extracted dictionary todispatchWebhookinstead of the full root object:dispatchWebhookconstructs the new payload, it wraps only the original event fields.Feature Area
API / Backend
Alternatives Considered
dispatchWebhook: We considered modifying the core helper insrc/lib/webhooks.tsto detect if the incomingdatais already wrapped in aWebhookPayload. However,dispatchWebhookis called from various live events (goals, streaks, achievements) that always supply raw dictionaries. Adding check-logic there introduces unnecessary complexity to a critical path. Normalizing the input at the retry route boundary is cleaner and safer.Acceptance Criteria
event/timestampkeys inside thedataobject).Additional Context
No response