Webhook Payload Formats: JSON, Form Data, and XML
JSON: The Most Common Format
The vast majority of modern webhook providers send payloads as JSON with the Content-Type: application/json header. JSON is human-readable, widely supported, and maps naturally to JavaScript objects, Python dictionaries, and similar data structures in every programming language.
A typical JSON webhook payload looks like this:
{
"event": "order.created",
"timestamp": "2026-01-15T10:30:00Z",
"data": {
"id": "ord_123",
"customer": {
"email": "user@example.com"
},
"items": [
{ "name": "Widget", "quantity": 2, "price": 1299 }
],
"total": 2598
}
}Parsing JSON in most frameworks is automatic. Express.js uses express.json() middleware, Django REST Framework uses JSONParser, and most languages have built-in JSON parsers. The key consideration for webhook handling is accessing the raw body for signature verification before parsing.
Form-Encoded Data
Some providers, notably Twilio, Slack (for slash commands and interactive components), and some legacy payment systems, send webhooks as application/x-www-form-urlencoded data. This is the same format used by HTML form submissions.
Form-encoded data is a flat list of key-value pairs separated by &, with values URL-encoded:
From=%2B15551234567&To=%2B15559876543&Body=Hello+World&MessageSid=SM1234567890In Express.js, parse form-encoded data with express.urlencoded({ extended: true }):
app.post('/webhook/twilio',
express.urlencoded({ extended: true }),
(req, res) => {
const { From, To, Body, MessageSid } = req.body;
console.log(`SMS from ${From}: ${Body}`);
res.type('text/xml').send('<Response><Message>Got it!</Message></Response>');
}
);Form-encoded data is inherently flat — it does not support nested objects or arrays natively. Some providers work around this by including a JSON string as one of the form values (like Slack's payload parameter for interactive components). In the ReqPour dashboard, form-encoded data is displayed as a parsed key-value table for easy reading.
XML Payloads
XML webhooks are less common today but still used by some enterprise systems, SOAP-based services, and legacy integrations. Twilio also expects XML (TwiML) responses to its webhooks, even though the incoming data is form-encoded.
An XML webhook payload might look like this:
<?xml version="1.0" encoding="UTF-8"?>
<notification>
<event-type>payment-completed</event-type>
<transaction>
<id>txn_789</id>
<amount>25.00</amount>
<currency>USD</currency>
</transaction>
</notification>Parse XML in Node.js using libraries like xml2js or fast-xml-parser:
const { XMLParser } = require('fast-xml-parser');
const parser = new XMLParser();app.post('/webhook/xml', express.text({ type: 'application/xml' }), (req, res) => { const data = parser.parse(req.body); console.log(data.notification.transaction.id); res.status(200).send('OK'); } ); ```
When working with XML webhooks, the ReqPour dashboard shows the raw XML in a readable format, making it easier to understand the structure before writing your parser.
Handling Multiple Formats
If your application receives webhooks from multiple providers, you may need to handle different formats on different endpoints. The cleanest approach is to use separate routes with the appropriate parsing middleware for each:
// JSON webhooks (Stripe, GitHub, etc.)
app.post('/webhooks/stripe', express.json(), handleStripe);// Form-encoded webhooks (Twilio, Slack commands) app.post('/webhooks/twilio', express.urlencoded({ extended: true }), handleTwilio );
// XML webhooks (legacy systems) app.post('/webhooks/legacy', express.text({ type: 'application/xml' }), handleLegacy ); ```
Alternatively, if you need a single endpoint that handles multiple formats, check the Content-Type header and parse accordingly. Be careful with signature verification — different formats require different raw body handling.
ReqPour's dashboard handles all these formats. JSON payloads are displayed with syntax highlighting and collapsible sections. Form data is shown as a key-value table. XML is displayed in its raw form. This makes it easy to inspect webhooks from any provider regardless of format.
Raw Body Access for Signatures
Regardless of the payload format, webhook signature verification requires the raw, unparsed request body. Most frameworks parse the body into a native data structure (object, dictionary, etc.) automatically, and re-serializing this parsed data may produce different bytes than the original.
In Express.js, capture the raw body alongside the parsed body:
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));In Next.js App Router, use request.text() to get the raw body before parsing:
export async function POST(request: Request) {
const rawBody = await request.text();
const body = JSON.parse(rawBody);// Use rawBody for signature verification // Use body for processing } ```
During development, the ReqPour dashboard shows both the raw and parsed views of each request, which helps you understand exactly what bytes your handler receives and verify that your raw body capture is working correctly.
Related
Get started with ReqPour
Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.