Blog

How to debug and format webhook payloads

How webhook payloads are structured, what Stripe and GitHub send, and how to format and inspect a payload when your handler misbehaves.

February 7, 2026

A customer paid, Stripe says the webhook was delivered, and yet your app still thinks the order is unpaid. Somewhere in a one-line, 3,000-character JSON body in your logs is the answer. The first step is making that body readable.

This guide covers how webhook payloads are put together, what the big providers send, and a checklist for when your handler misbehaves.

What webhooks are

A webhook is an HTTP request that a service sends to your server when something happens. The alternative is polling: asking the API "anything new?" every few seconds, which is slow and wasteful. With a webhook, you give the service a URL once, and it sends a POST request with the event data as soon as the event occurs.

When a customer pays through Stripe, Stripe sends your server the payment details. When someone pushes to a GitHub repository, GitHub sends the commits, branch and author. Payment processors, CRMs, CI/CD systems and chat platforms all integrate this way.

How a webhook payload is structured

The payload is the request body, usually JSON and occasionally XML or form-encoded data. Each provider has its own schema, but most include an event ID, an event type, a timestamp and the data for the event:

JSON
{
  "id": "evt_1234567890",
  "type": "payment.completed",
  "created": 1738886400,
  "data": {
    "object": {
      "id": "pay_abc123",
      "amount": 2999,
      "currency": "usd",
      "customer": "cus_xyz789",
      "status": "succeeded"
    }
  }
}

Real payloads are usually much bigger than this, often nested several levels deep, and they arrive minified: one long line with no spaces. Formatting them with indentation is the first step to reading them.

Common webhook providers

Stripe

Stripe sends events for payments, failed charges, subscription changes and invoices. Every event has a top-level type such as charge.succeeded or invoice.paid, a created Unix timestamp in seconds, and a data.object holding the full resource. With expanded customer, payment method and metadata objects, a single event can contain over a hundred fields.

GitHub

GitHub sends events for pushes, pull requests, issues, releases and deployments. The event name is in the X-GitHub-Event header, not the body. The body has an action field for events that have one, plus details about the repository, the sender and the change:

JSON
{
  "action": "opened",
  "pull_request": {
    "number": 42,
    "title": "Fix login bug",
    "user": {
      "login": "octocat",
      "id": 1
    },
    "head": {
      "ref": "fix/login-bug",
      "sha": "abc123def456"
    },
    "base": {
      "ref": "main"
    }
  },
  "repository": {
    "full_name": "octocat/hello-world"
  }
}

Other providers

Slack sends events for messages and interactive components, Shopify for orders, products and customers, and Twilio for incoming SMS and call status changes. Twilio sends form-encoded data (key=value&key2=value2) rather than JSON, so check the Content-Type header before you reach for JSON.parse.

How to use the webhook formatter

  1. Copy the raw body from your logs or the provider’s dashboard and paste it into the webhook payload formatter.
  2. Select Format and inspect. If the JSON is invalid, the tool shows the parse error so you can see where the body was cut off or corrupted.
  3. Read the summary: total keys, maximum nesting depth, number of arrays and number of nested objects. If the payload has a string field named event, type, event_type or action, its value is shown as the event type. A tag appears when field names suggest timestamps (timestamp, created_at, updated_at, date or time).
  4. Read or Copy the formatted output, indented with two spaces.

Everything runs in your browser, so payloads containing customer data aren't uploaded. To turn a Unix timestamp such as Stripe’s created into a date, use the timestamp converter.

Debugging webhook issues

When a handler misbehaves, look at the raw payload before you look at your code. It's tempting to assume the provider sent what the docs promised. Most of the time the bug is one of these:

  • Reading a field at the wrong nesting level.
  • Matching the wrong event type string.
  • Not handling optional fields that can be null.
  • Parsing timestamps incorrectly. Stripe sends Unix time in seconds, while JavaScript's new Date() expects milliseconds, which is how orders end up dated January 1970.

It also helps to compare payloads for different event types. A Stripe charge.succeeded event carries a Charge in data.object, while invoice.paid carries an Invoice, so a handler that serves both needs to branch on type. The JSON diff tool shows exactly which fields differ between two payloads.

Best practices for handling webhooks

  • Verify signatures. Most providers sign each request with a shared secret. Check the signature against the raw request body before trusting the payload.
  • Respond quickly. Return a 2xx status right away and do the real work in a background job. Providers retry when they don't get a response within a few seconds, and a slow handler can end up processing the same event several times.
  • Handle duplicates. The same event can be delivered more than once. Store event IDs and skip ones you've already processed.
  • Log raw payloads. Store the body before processing it, so you can replay and inspect it when something goes wrong.
  • Receive real webhooks locally. Tools such as ngrok or the Stripe CLI forward webhooks to your development machine.

Need data from a webpage as JSON?

PulpMiner turns any public page into structured JSON and can save it as a live API. See it work on a demo page, no sign-up.

Try it on a demo page