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.
Available context
Inside any {{...}} expression you have access to three objects:
| Object | Description | Example |
|---|---|---|
| payload | The full parsed JSON body of the incoming webhook | {{payload.type}} |
| headers | Inbound request headers (lowercase keys) | {{headers.content-type}} |
| meta | HostWebhook metadata about the event | {{meta.eventId}} |
meta fields
| Field | Value |
|---|---|
| meta.eventId | The HostWebhook event ID |
| meta.receivedAt | ISO 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:
| Operator | Example |
|---|---|
| === / == | {{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.
| Provider | Target URL | Auth header | Native support |
|---|---|---|---|
| Resend | https://api.resend.com/emails | Authorization: Bearer re_... | JSON |
| Postmark | https://api.postmarkapp.com/email | X-Postmark-Server-Token: <key> | JSON |
| SendGrid | https://api.sendgrid.com/v3/mail/send | Authorization: Bearer SG... | JSON |
| Brevo | https://api.brevo.com/v3/smtp/email | api-key: <key> | JSON |
| Mailgun | https://api.mailgun.net/v3/{domain}/messages | — | Form-encoded |
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.
{
"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>"
}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.
{
"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.
{
"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.
{
"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>"
}{ 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
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