Payload transforms

Reshape a webhook payload before it reaches an additional target — convert a raw Stripe JSON into a Discord embed, or a GitHub push into a Slack message, without writing any code.

Overview

A payload transform is a JSON string template attached to an additional target. When an event arrives, HostWebhook evaluates the template, replacing every {{expression}} with the corresponding value from the event, and POSTs the result to the target URL.

Transforms only apply to additional targets. The primary target URL always receives the original payload untouched.

Available context

Inside any {{...}} expression you have access to three objects:

ObjectDescriptionExample
payloadThe full parsed JSON body of the incoming webhook{{payload.type}}
headersInbound request headers (lowercase keys){{headers.content-type}}
metaHostWebhook metadata about the event{{meta.eventId}}

meta fields

FieldValue
meta.eventIdThe HostWebhook event ID
meta.receivedAtISO 8601 timestamp when the event was received

Syntax reference

Simple path interpolation

Use dot-notation to access any field in the payload. Objects are serialized as JSON; missing fields resolve to an empty string.

// Input payload:
{ "type": "payment_intent.succeeded", "data": { "object": { "amount": 5000 } } }

// Transform:
{ "text": "Payment: {{payload.type}} for {{payload.data.object.amount}}" }

// Result:
{ "text": "Payment: payment_intent.succeeded for 5000" }

Null coalescing ??

Use {{path ?? fallback}} to provide a default value when the field is null or undefined.

// Field is null:
{ "customer": null }

// Transform:
{ "text": "Customer: {{payload.customer ?? 'Guest'}}" }

// Result:
{ "text": "Customer: Guest" }

Ternary expressions

Use {{condition ? 'yes' : 'no'}} for conditional values. The truthy branch is taken when the condition path is non-empty, non-null, and not false.

// Truthy check:
{ "color": "{{payload.active ? 5763719 : 15548997}}" }

// Branches can also be field paths (no quotes):
{ "name": "{{payload.nickname ? payload.nickname : payload.email}}" }

Comparison operators

Compare a path against a literal value using standard operators:

OperatorExample
=== / =={{payload.status === "active" ? "Yes" : "No"}}
!== / !={{payload.reason !== null ? payload.reason : "none"}}
> / <{{payload.amount > 1000 ? 'Large' : 'Small'}}
>= / <={{payload.retries >= 3 ? 'Too many' : 'OK'}}

Comparands can be: null, undefined, true, false, numbers, or quoted strings.


Full examples

Stripe payment → Discord embed

{
  "embeds": [{
    "title": "💳 Payment Received",
    "color": 5763719,
    "fields": [
      {
        "name": "Amount",
        "value": "{{payload.data.object.amount_received ?? 0}} {{payload.data.object.currency}}",
        "inline": true
      },
      {
        "name": "Status",
        "value": "{{payload.data.object.status}}",
        "inline": true
      },
      {
        "name": "Customer",
        "value": "{{payload.data.object.customer ?? 'Guest'}}",
        "inline": false
      }
    ],
    "timestamp": "{{meta.receivedAt}}"
  }]
}

Stripe subscription → Discord (conditional color)

{
  "embeds": [{
    "title": "{{payload.type === 'customer.subscription.created' ? '🟢 New Subscription' : '🔴 Subscription Canceled'}}",
    "color": "{{payload.type === 'customer.subscription.created' ? 5763719 : 15548997}}",
    "fields": [
      { "name": "Plan", "value": "{{payload.data.object.items.data[0].price.nickname ?? 'Unknown'}}", "inline": true },
      { "name": "Status", "value": "{{payload.data.object.status}}", "inline": true }
    ],
    "timestamp": "{{meta.receivedAt}}"
  }]
}

GitHub push → Slack

{
  "text": "🚀 *{{payload.pusher.name}}* pushed {{payload.commits.length ?? 0}} commit(s) to `{{payload.ref}}` in *{{payload.repository.full_name}}*"
}

GitHub PR → Discord (opened / merged / closed)

{
  "embeds": [{
    "title": "{{payload.action === 'opened' ? '🔀 PR Opened' : payload.pull_request.merged ? '✅ PR Merged' : '❌ PR Closed'}}: {{payload.pull_request.title}}",
    "color": "{{payload.action === 'opened' ? 3447003 : payload.pull_request.merged ? 5763719 : 15548997}}",
    "fields": [
      { "name": "Author", "value": "{{payload.pull_request.user.login}}", "inline": true },
      { "name": "Repo", "value": "{{payload.repository.full_name}}", "inline": true }
    ]
  }]
}

Email provider integrations

Additional targets can call email provider APIs directly — no code required. Most modern providers accept JSON, so a payload transform is all you need. Configure the provider's API URL and authentication in the target's Delivery Headers field, then use the transform to shape the body.

ProviderTarget URLAuth headerNative support
Resendhttps://api.resend.com/emailsAuthorization: Bearer re_...JSON
Postmarkhttps://api.postmarkapp.com/emailX-Postmark-Server-Token: <key>JSON
SendGridhttps://api.sendgrid.com/v3/mail/sendAuthorization: Bearer SG...JSON
Brevohttps://api.brevo.com/v3/smtp/emailapi-key: <key>JSON
Mailgunhttps://api.mailgun.net/v3/{domain}/messagesForm-encoded
Set the Delivery Headers on the target (not the payload transform) for authentication and Content-Type. The payload transform only shapes the JSON body.

Resend

Set the target URL to https://api.resend.com/emails and add Authorization: Bearer re_... in Delivery Headers. Include the from field in the transform using a sender address from your verified Resend domain.

jsonPayload transform — Resend
{
  "from": "Your App <[email protected]>",
  "to": ["{{payload.user.email}}"],
  "subject": "{{payload.user.name ?? 'There'}}, your account is ready",
  "html": "<h1>Welcome, {{payload.user.name ?? 'there'}}!</h1><p>Your account has been created.</p>"
}
The from address must use a domain you have verified in your Resend account. Resend will reject any sender from an unverified domain.

Postmark

Set the target URL to https://api.postmarkapp.com/email and add X-Postmark-Server-Token: <your-server-token> in Delivery Headers. Postmark uses PascalCase field names.

jsonPayload transform — Postmark
{
  "From": "[email protected]",
  "To": "{{payload.user.email}}",
  "Subject": "Welcome, {{payload.user.name ?? 'there'}}!",
  "HtmlBody": "<h1>Welcome!</h1><p>Hi {{payload.user.name ?? 'there'}}, your account is active.</p>",
  "MessageStream": "outbound"
}

SendGrid

Set the target URL to https://api.sendgrid.com/v3/mail/send and add Authorization: Bearer SG... in Delivery Headers. SendGrid has a nested structure for recipients and content.

jsonPayload transform — SendGrid
{
  "personalizations": [
    {
      "to": [{ "email": "{{payload.user.email}}", "name": "{{payload.user.name ?? ''}}" }],
      "subject": "Welcome, {{payload.user.name ?? 'there'}}!"
    }
  ],
  "from": { "email": "[email protected]", "name": "Your App" },
  "content": [
    {
      "type": "text/html",
      "value": "<h1>Welcome!</h1><p>Hi {{payload.user.name ?? 'there'}}, your account is active.</p>"
    }
  ]
}

Brevo (Sendinblue)

Set the target URL to https://api.brevo.com/v3/smtp/email and add api-key: <your-api-key> in Delivery Headers.

jsonPayload transform — Brevo
{
  "sender": { "email": "[email protected]", "name": "Your App" },
  "to": [{ "email": "{{payload.user.email}}", "name": "{{payload.user.name ?? ''}}" }],
  "subject": "Welcome, {{payload.user.name ?? 'there'}}!",
  "htmlContent": "<h1>Welcome!</h1><p>Hi {{payload.user.name ?? 'there'}}, your account is active.</p>"
}
These examples assume your app sends an event with { user: { email, name } }. Adjust the {{payload.*}} paths to match your actual payload structure.

Using the transform editor

The transform editor is available when adding or editing an additional target on any webhook. It shows:

  • Invalid badge — the JSON is malformed (syntax error)
  • Beautify button — formats the JSON for easier reading
If the transform has a syntax error at runtime, HostWebhook falls back to delivering the original raw payload. The delivery attempt in the event log will show a note about the transform error.

Tips

  • Always wrap string fallbacks in quotes: {{payload.name ?? 'Unknown'}}
  • Branch values without quotes are resolved as paths: {{payload.x !== null ? payload.x : payload.y}}
  • Objects and arrays are serialized as JSON strings when embedded in a larger string, or kept as-is when they are the entire value
  • The transform must be valid JSON — use the Beautify button and check the Invalid badge when editing