How to Transform Webhook Payloads Without Writing Code

Last updated: February 2026 · 8 min read

In this guide

The Problem: Webhook Payloads Never Match

You're integrating with a third-party service — GitHub, Stripe, Shopify, whatever — and they send webhooks. Great. Except the payload looks nothing like what your application expects.

GitHub sends deeply nested JSON with repository.owner.login. Stripe wraps everything inside a data.object envelope. Shopify uses snake_case when your API expects camelCase. The field you need is buried three levels deep, and there are 40 fields you don't care about.

The typical fix? Write a middleware endpoint. A small server that receives the webhook, reshapes the JSON, and forwards it to your actual application. It's maybe 30 lines of code — but then you need to deploy it, keep it running, handle retries when your app is down, and maintain it when the upstream schema changes.

That's what Hookpipe replaces. It's a webhook transformer that sits between the sender and your app. You define transform rules as JSON configuration — field mapping, filtering, renaming, static values — and Hookpipe handles the rest. No code to deploy, no server to maintain.

How Hookpipe's Webhook Transformer Works

The flow is simple:

  1. Create a hook with a destination URL (where your app lives) and a transformConfig (the rules).
  2. Point the external service at your Hookpipe receive URL: https://hookpipe.app/hooks/YOUR_HOOK_ID.
  3. Hookpipe receives the webhook, stores the original payload, applies your transforms, and forwards the result to your destination.

If your destination is down, Hookpipe retries up to 3 times with exponential backoff. Every payload is stored for 7 days, so you can inspect what arrived and replay failed deliveries.

Let's walk through each transform capability with real examples.

Field Mapping with Dot Notation

Field mapping is the most common webhook payload mapping operation. You tell Hookpipe which fields to extract from the incoming payload and what to name them in the output. Dot notation lets you reach into nested objects.

Say GitHub sends you this when someone opens a pull request:

{
  "action": "opened",
  "number": 42,
  "pull_request": {
    "title": "Fix login bug",
    "user": {
      "login": "alice"
    },
    "head": {
      "ref": "fix/login-bug"
    }
  },
  "repository": {
    "full_name": "acme/webapp",
    "private": false
  }
}

But your app just needs the PR title, author, branch, and repo name. Define a fieldMapping:

"fieldMapping": {
  "title": "pull_request.title",
  "author": "pull_request.user.login",
  "branch": "pull_request.head.ref",
  "repo": "repository.full_name"
}

Your app receives:

❌ Raw payload (16 fields, nested)

{ "action": "opened",
  "number": 42,
  "pull_request": {
    "title": "...",
    "user": { "login": "..." },
    ... } }

✅ Transformed (4 fields, flat)

{
  "title": "Fix login bug",
  "author": "alice",
  "branch": "fix/login-bug",
  "repo": "acme/webapp"
}

Clean, flat, and exactly what your app expects. No middleware, no code.

Filtering Payloads by Content

Not every webhook matters. GitHub sends events for opened, closed, labeled, assigned, and a dozen other actions. If you only care about opened pull requests on production repos, you can filter everything else out.

"filters": [
  { "field": "action", "operator": "equals", "value": "opened" },
  { "field": "repository.full_name", "operator": "contains", "value": "prod" }
]

When a payload doesn't match all filter conditions, Hookpipe returns HTTP 200 to the sender (so it doesn't retry) but doesn't forward anything to your app. Your destination only sees the events that matter.

Supported operators:

Filters work on nested fields using the same dot notation: pull_request.user.login, data.object.amount, etc.

Renaming Fields

Sometimes the payload is almost right — the data is there, but the field names don't match your schema. Maybe the upstream sends user_email and your app expects email. Instead of writing a mapping from scratch, use renames:

"renames": {
  "user_email": "email",
  "created_at": "timestamp",
  "full_name": "name"
}

Renames are applied after field mapping, so you can combine them: first extract the fields you want, then rename to match your app's conventions.

Adding Static Values

Sometimes your app needs context that isn't in the webhook. Which service sent this? What environment is it from? Add static values by including literal strings in your fieldMapping — just use a non-dot-notation key with a constant value:

"fieldMapping": {
  "title": "pull_request.title",
  "author": "pull_request.user.login",
  "source": "github",
  "env": "production"
}

Every payload forwarded through this hook will include source and env as static values. This is useful for routing — if your app receives webhooks from multiple services, the source field tells your handler which one it's dealing with.

Full Example: Stripe → Your App

Let's put it all together with a realistic example. Stripe sends you payment events. You only want successful charges over $10, and your app expects a simplified format.

Step 1: Create your API key

curl -X POST https://hookpipe.app/api/auth/keys \
  -H "Content-Type: application/json" \
  -d '{"name": "my-key"}'

Save the API key from the response — it's only shown once.

Step 2: Create the hook with transforms

curl -X POST https://hookpipe.app/api/hooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "stripe-charges",
    "destinationUrl": "https://your-app.com/payments",
    "transformConfig": {
      "fieldMapping": {
        "charge_id": "data.object.id",
        "amount_cents": "data.object.amount",
        "currency": "data.object.currency",
        "customer_email": "data.object.billing_details.email",
        "description": "data.object.description",
        "source": "stripe",
        "env": "production"
      },
      "filters": [
        { "field": "type", "operator": "equals", "value": "charge.succeeded" },
        { "field": "data.object.amount", "operator": "greaterThan", "value": "1000" }
      ],
      "renames": {
        "customer_email": "email"
      }
    }
  }'

Step 3: Test it

Simulate a Stripe charge event:

curl -X POST https://hookpipe.app/hooks/YOUR_HOOK_ID \
  -H "Content-Type: application/json" \
  -d '{
    "type": "charge.succeeded",
    "data": {
      "object": {
        "id": "ch_1abc123",
        "amount": 4999,
        "currency": "usd",
        "billing_details": {
          "email": "customer@example.com"
        },
        "description": "Pro plan subscription"
      }
    }
  }'

Your app at https://your-app.com/payments receives:

{
  "charge_id": "ch_1abc123",
  "amount_cents": 4999,
  "currency": "usd",
  "email": "customer@example.com",
  "description": "Pro plan subscription",
  "source": "stripe",
  "env": "production"
}

Five nested levels flattened to a clean object. Failed charges and charges under $10 are silently dropped. The source and env fields are injected automatically. Your app doesn't need to know anything about Stripe's payload format.

Inspect and Replay

Every webhook received by Hookpipe is stored for 7 days. You can inspect both the raw incoming payload and the transformed output:

# List recent payloads
curl https://hookpipe.app/api/hooks/YOUR_HOOK_ID/payloads \
  -H "Authorization: Bearer YOUR_API_KEY"

# Get a specific payload with full details
curl https://hookpipe.app/api/hooks/YOUR_HOOK_ID/payloads/PAYLOAD_ID \
  -H "Authorization: Bearer YOUR_API_KEY"

If a delivery failed — maybe your app was deploying and returned a 503 — you can replay it:

curl -X POST https://hookpipe.app/api/hooks/YOUR_HOOK_ID/payloads/PAYLOAD_ID/replay \
  -H "Authorization: Bearer YOUR_API_KEY"

The replayed request includes an X-Replay: true header so your app can distinguish replays from original deliveries. No data is lost, and you don't have to ask the upstream service to resend anything.

When to Use a Webhook Transformer

Hookpipe is a good fit when:

Start Transforming Webhooks

Create your first hook in under a minute. Free tier includes unlimited hooks, the full transform engine, and 7-day payload retention.

Read the Quick Start Open Dashboard