Payload transforms

Reshape a webhook payload before HostWebhook delivers it — 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 a webhook. 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.

ℹA webhook has one transform, and it applies to its target URL. With no transform configured, the target URL 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

Most email providers accept JSON, so a template is all you need to turn an event into a request their API accepts. The provider's API URL, method and credentials go on an HTTP Action node, which renders its body with the same syntax as the templates below.

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}/messages—Form-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 <hello@yourdomain.com>",
  "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": "hello@yourdomain.com",
  "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": "hello@yourdomain.com", "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": "hello@yourdomain.com", "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 the Payload Transform card on the webhook detail page. 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