Skip to content
Tapwire

Working with JSON webhook payloads

Content types, event envelopes, large numbers, dates, missing fields and the parsing mistakes that break receivers.

7 min read · Updated 24 October 2026

Most webhook providers send JSON. It is readable, every language can parse it, and it maps naturally to objects. It also has a handful of sharp edges that only show up with real traffic: numbers that silently lose precision, dates in three different formats, fields that are sometimes missing and sometimes null. This guide covers the ones that break receivers most often.

Check the content type first

A JSON body should arrive with Content-Type: application/json. Some providers use a more specific type such as application/vnd.example+json; the +json suffix tells you it is still JSON. If your framework only parses bodies whose type is exactly application/json, those deliveries reach your handler as an empty object or a raw string, and the bug looks like the provider sent nothing.

The reverse also happens: a provider sends form-encoded data (payload=%7B%22id%22…) where the JSON is a URL-encoded string inside a form field. If parsing fails, look at the raw body before assuming the JSON is malformed.

JSON exchanged between systems must be UTF-8 encoded. A charset parameter is unnecessary, and a byte order mark at the start of the body is not allowed by the specification, although some parsers tolerate it.

Envelopes and flat events

Providers usually follow one of two shapes.

// Envelope: metadata outside, the object inside
{
  "id": "evt_01JB8Z4K2M",
  "type": "customer.updated",
  "created": 1729778527,
  "data": { "object": { "id": "cus_42", "email": "ada@example.com" } }
}

// Flat: the object is the payload, the event type is in a header
{ "id": "cus_42", "email": "ada@example.com", "updated_at": "2026-10-24T14:02:07Z" }

Envelopes are easier to route and deduplicate because the event ID and type are always in the same place. With flat payloads, check the headers for the event type and delivery ID; they are rarely in the body.

Numbers: the precision trap

JSON itself puts no limit on the size of numbers, but JavaScript and many other runtimes parse them into 64-bit floating-point values. Integers above 9,007,199,254,740,991 (253 − 1) cannot be represented exactly, so a large ID silently changes:

JSON.parse('{"id": 12345678901234567890}').id
// 12345678901234567000

Well-designed APIs send large identifiers as strings for exactly this reason. If yours does not, parse with a library that supports big integers, or treat the raw text as the source of truth. When Tapwire formats JSON it only re-indents the text, so the digits you see are the digits that were sent.

Money is the other classic case. 10.50 parsed as a float is fine for display but not for arithmetic. Most payment providers send amounts as integers in the smallest currency unit (for example 4900 for 49.00) so you never have to round.

Dates and times

You will meet at least three formats, sometimes from the same provider:

  • ISO 8601 strings: "2026-10-24T14:02:07Z" or with an offset, "2026-10-24T19:32:07+05:30".
  • Unix timestamps in seconds: 1729778527.
  • Unix timestamps in milliseconds: 1729778527000.

Seconds and milliseconds differ by a factor of 1,000, and confusing them gives dates in 1970 or tens of thousands of years in the future. Check the magnitude: a current timestamp in seconds has 10 digits, in milliseconds 13.

Missing, null and empty

{"phone": null}, {"phone": ""} and {} can mean three different things: the value was cleared, the value is an empty string, or the field was not included in this event. Some providers omit unchanged fields in update events. Write handlers that distinguish "absent" from "null" when it matters, and never assume a field is present because it was present in the documentation example.

Be tolerant of new fields

Providers add fields over time without considering it a breaking change. If your parser rejects unknown properties, a routine provider update takes your integration down. Validate the fields you depend on and ignore the rest. Pin an API version where the provider supports it, so that structural changes arrive only when you choose.

Keep the raw body

Signature verification is computed over the exact bytes that were sent. If your framework parses the JSON and you re-serialize it, whitespace, key order and number formatting can change and the signature no longer matches. Capture the raw body before parsing, verify the signature on it, and only then parse. See verifying signatures for the details.

Common parsing mistakes

SymptomLikely cause
Handler receives an empty objectContent type not recognized by the body parser
A field is a string containing JSONThe provider double-encoded the value; parse it a second time
IDs do not match the provider dashboardLarge integers lost precision during parsing
Signature fails only for some eventsBody was re-serialized before verification; non-ASCII characters often trigger it
Parser error at position 0Body is form-encoded, empty, or starts with a byte order mark

The fastest way to rule all of these out is to look at the raw request. Point the provider at a URL from the inspector, compare the raw and formatted views, and check the content type header before you write any parsing code.