Turn inbound emails into calls to your own API...
Wire an email address straight to your API: mail arrives, Webhook Relay parses it to JSON, a function reshapes it into your API's request, and the bucket's output POSTs it โ all server-side, no agent to run.
Sender โโโถ <id>@in.webhookrelay-mail.com โโโถ bucket โโโถ function โโโถ POST https://api.example.com/โฆ
(email) (inbound address) (parsed JSON) (reshape) (your API's shape)
Use the Webhook Relay MCP server first when it is connected. This workflow is mostly account configuration: create the bucket, create an email input, create and test the transform function, create the output, and inspect delivery logs.
Use the relay CLI only when MCP is not connected but the CLI is authenticated
and working, or when you need to run an internal forwarding agent for a private
destination.
With MCP, create the bucket, then create an email input on it with
create_email_input. Keep the returned inbound address; that is the address to
hand out or point mail at. If the destination is public, create the bucket with
the destination URL or add a public output with the available MCP output tool.
CLI fallback:
relay email create --bucket email-to-api
# โ prints the address, e.g. <id>@in.webhookrelay-mail.com (hand it out / point mail at it)
--filter-from <addr> restricts who may send; other mail is dropped.
The function receives the parsed email as r.body (the JSON from the
email-parsing-api skill) and rewrites the request into what your API expects.
See examples/email-to-api.js โ it maps an email into a create-ticket call:
// email-to-api.js โ parsed email โ your API's request.
let email
try {
email = JSON.parse(r.body)
} catch (e) {
r.setResponseStatus(400); r.setResponseBody("invalid email payload"); r.stopForwarding()
}
if (email) {
// Optional: only act on expected mail; drop the rest.
// if (!/support@yourco\.com/.test(email.recipient)) { r.stopForwarding() }
const ticket = {
subject: email.subject || "(no subject)",
requester: email.from,
requester_name: email.from_name || email.from,
body: email.text || email.html || "",
received_at: email.date,
external_id: email.message_id, // de-dupe key
}
r.setBody(JSON.stringify(ticket))
r.setMethod("POST")
r.setHeader("Content-Type", "application/json")
const token = cfg.get("API_TOKEN") // keep secrets in function config
if (token) { r.setHeader("Authorization", "Bearer " + token) }
}
Key function API: r.body (incoming parsed email), JSON.parse / JSON.stringify,
r.setBody, r.setHeader / r.deleteHeader, r.setMethod, r.setPath /
r.setRawQuery, cfg.get("KEY") for secrets, and r.stopForwarding() to drop a
message. Full reference and more helpers: the webhook-transformations skill and
https://webhookrelay.com/docs/webhooks/functions/manipulating-json.md
Prefer MCP create_function and execute with a parsed email payload from the
email-parsing-api skill. Iterate until the transformed request matches the
API contract.
CLI fallback: relay function test runs the function against sample requests โ
use a parsed email as the input body. See examples/spec.yaml:
relay function test -f examples/spec.yaml -v
With MCP, create the function, configure any required secret values through the dashboard or available function-config tooling, then attach the function to the public output. If the bucket/output does not exist yet, pass the function ID when creating the output or bucket.
CLI fallback:
# Deploy the function
relay function create --name email-to-api --driver js --source examples/email-to-api.js
# Store the secret it reads via cfg.get("API_TOKEN")
relay function config set email-to-api API_TOKEN=xxxxx
# Point the bucket's output at YOUR API, running the function on the way out
relay output create api --bucket email-to-api --type public \
--destination https://api.example.com/tickets \
--function email-to-api
The function reads its API token via cfg.get("API_TOKEN"); set it with
relay function config set (above) โ or in the
dashboard (Functions โ your function โ Config) โ
so it isn't hard-coded in source. List or remove values with
relay function config ls|rm <function>.
--type public means Webhook Relay delivers server-side โ nothing to keep
running. To reach a service with no public IP (localhost / private LAN), use an
internal output and a running agent instead (see webhook-forwarding-internal).
Email the address from step 1, then check delivery with MCP
list_webhook_logs / get_webhook_log or in the Webhook Relay dashboard logs
(request in, transformed request out, and your API's response). To eyeball
exactly what gets sent, temporarily point the output at https://bin.webhookrelay.com
and inspect the captured request.
JSON.parse in try/catch and return a 400 +
stopForwarding() on bad input, so malformed mail never hits your API.--filter-from on the address and/or check
email.recipient / email.subject in the function, calling
r.stopForwarding() to ignore mail you don't want.cfg.get(...) โ set them with
relay function config set <function> KEY=VALUE (or in the dashboard), never
in source.email.attachments[]; drop or cap them with
input policy if you don't need them.Webhook Relay docs (these .md URLs render as plain markdown for easy reading):