Guide to Marketo Webhooks: Features and Best Practices
Marketo webhooks let a marketing automation flow call your own services. When a person fills in a form, changes a field or hits a scoring threshold, a Smart Campaign can send their details to your CRM sync, enrichment service, Slack alerting or data pipeline as part of the flow. If you run campaigns on Adobe Marketo Engage, webhooks are how those flows reach code you control.
This guide covers how Marketo webhooks are defined and triggered, what the request contains, how to authenticate requests that Marketo does not sign, the limits that catch out new integrations, and the best practices for running a Marketo receiver in production.
What are Marketo webhooks?
Marketo webhooks work differently from event subscriptions on most platforms. A Marketo webhook is a request definition: a URL, an HTTP method, a body template and optional headers, created once by an admin. Nothing is sent until a marketer adds a Call Webhook step to a Smart Campaign's flow. The campaign's triggers decide when the webhook fires, and it fires once for each person who runs through the flow.
You write the request yourself. The body is your Payload Template, with tokens such as {{lead.Email Address}} replaced by the person's values at send time. Marketo adds no event type, envelope or metadata of its own, so whatever your receiver needs to know about the event, including which campaign sent it, has to be in your template.
Marketo does not sign webhook requests. The only way to prove a request came from your Marketo instance is a secret you put in a custom header, which your receiver checks.
Marketo webhook features
| Feature | Details |
|---|---|
| Configuration | Admin > Webhooks > New Webhook |
| Trigger | A Call Webhook flow step in a trigger Smart Campaign, once per person |
| HTTP method | The developer docs say GET or POST. The Create a Webhook page lists DELETE, GET, PATCH, POST and PUT |
| Body | Your Payload Template (JSON, XML or another format that allows double-quoted strings) with tokens substituted |
| Token encoding | JSON or Form/Url, set per webhook |
| Event types | None defined by Marketo. You choose what goes in the body |
| Authentication | None built in. Add a secret through Webhook Actions > Set Custom Header |
| Signature | None. No HMAC, signing secret or timestamp |
| Response window | 30 seconds |
| Response mappings | Write response fields back to the person, only on a 2xx response |
| Retries | Not documented |
Marketo publishes no source IP list for webhook calls, so the custom header is the credential.
Common events
Because Marketo has no event types, "events" are whatever triggers you put on the Smart Lists of the campaigns that call your webhook. Marketo invokes the webhook whenever a person enters the campaign's flow. A webhook that serves several campaigns cannot tell them apart unless the template says so, so put a fixed identifier in the body:
{
"event": "lead_created",
"campaign": {{campaign.name}},
"lead": {
"id": {{lead.Id}},
"email": {{lead.Email Address}},
"first_name": {{lead.First Name}},
"last_name": {{lead.Last Name}}
}
}
The event field here is your own convention, not a Marketo field. Use a separate webhook definition for each value, or add the field with a campaign-level token. Marketo encloses string-type tokens in double quotes for you, so do not wrap tokens in quotes yourself, or the result will not parse.
Lead and system tokens are always available. Trigger, campaign and program tokens are available in their own scopes, and you can use program-level My Tokens to pass a fixed ID for each program. Tokens also work in the URL and in custom headers.
Setting up Marketo webhooks
- Generate a long random secret, for example with
openssl rand -hex 32, and store it on your server asMARKETO_WEBHOOK_SECRET. - In Marketo, go to Admin > Webhooks and click New Webhook.
- Enter a name and your HTTPS endpoint URL, set Request Type to
POST, and write the Payload Template. Use Insert Token to add tokens. - Set Request Token Encoding to
JSONfor a JSON template, set Response Type, and click Create. - Select the webhook, choose Webhook Actions > Set Custom Header, and add a header such as
X-Webhook-Secretwith your secret as its value. - Open a trigger Smart Campaign. On the Smart List tab, add the trigger, and on the Flow tab, add a Call Webhook step that uses your webhook. Activate the campaign.
Marketo has no test button for webhooks. Use a test campaign whose Smart List only matches a test person, then trigger it.
Marketo sends the URL exactly as written: & stays & and %26 stays %26. If a token in the URL can contain special characters, put it in the body instead.
If the webhook points at Hookdeck, keep Request Type set to POST. Hookdeck's Marketo source type pre-selects POST as the allowed method.
Securing Marketo webhooks
There is no signature to verify. Check the custom header you set, compare it with your stored secret in constant time, and reject the request if it is missing or wrong.
const crypto = require('crypto');
const express = require('express');
const HEADER_NAME = 'x-webhook-secret'; // the header you set in Set Custom Header
function isValidSecret(headerValue, secret) {
if (typeof headerValue !== 'string' || !secret) return false;
const received = Buffer.from(headerValue, 'utf8');
const expected = Buffer.from(secret, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/marketo', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.MARKETO_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!isValidSecret(req.get(HEADER_NAME), secret)) {
return res.sendStatus(401);
}
let payload;
try {
payload = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400); // a token value broke the template
}
// Route on the event field your template sets. Persist, then process asynchronously.
res.status(200).json({ received: true });
});
The same check in Python:
import hmac
import os
def is_valid_secret(header_value, secret: str) -> bool:
if not header_value or not secret:
return False
return hmac.compare_digest(header_value.encode("utf-8"), secret.encode("utf-8"))
# In FastAPI:
# is_valid_secret(request.headers.get("x-webhook-secret"), os.environ["MARKETO_WEBHOOK_SECRET"])
A header secret proves the caller knows the secret. It does not protect the body from tampering, and it gives no replay protection, so serve the endpoint over HTTPS only and rotate the secret by editing the webhook if it leaks.
Make Marketo webhooks production-ready. Hookdeck Event Gateway answers Marketo straight away so slow processing never holds up a campaign, verifies your secret header and stops requests that fail before they reach your service, and durably queues every request with replay for anything that fails.
Marketo webhook limitations and pain points
No signature, only a header you add
The Problem: Anyone who finds your endpoint URL can send it requests that look like they came from Marketo.
Why It Happens: Marketo signs nothing. A webhook has no signing secret, and the only authentication available is a custom header whose name and value you choose.
Workarounds:
- Add a long random secret header to every webhook definition before you use it in a campaign.
- Compare it in constant time and fail closed if your server has no secret configured.
- Re-fetch the person from the Marketo REST API before taking any action that matters.
How Hookdeck Can Help: Hookdeck's Marketo source type asks for the header name and its exact value, and stops requests without them from reaching your service.
Slow endpoints slow down campaigns
The Problem: A campaign that calls your webhook takes hours to finish, or calls start timing out.
Why It Happens: Marketo calls the webhook once per person, waits up to 30 seconds for each response, and the campaign's execution waits on it. Marketo's own example: even at 50 ms per call, 100,000 calls take 1.5 hours.
Workarounds:
- Return as soon as you have stored the request, and do the work afterwards.
- Avoid calling other slow services inside the request.
- Narrow the Smart List so only people who need the webhook trigger it.
How Hookdeck Can Help: Hookdeck acknowledges each request at its edge and queues it, so Marketo gets a fast response however long your own processing takes. You can also set a max delivery rate on delivery to your service so a large batch campaign does not overwhelm it.
Response mappings need your service's response
The Problem: You use the webhook for enrichment and map response fields back onto the person, but the fields never update.
Why It Happens: Response mappings read the response body Marketo receives, and only apply on a 2xx. If anything sits between Marketo and your service and answers on its behalf, Marketo maps from that response instead of yours.
Workarounds:
- Keep enrichment webhooks, which need a synchronous answer, pointed straight at the service that produces it.
- Use a queue in front only for notification webhooks whose response Marketo does not need.
- For asynchronous enrichment, write results back with the Marketo REST API when your processing finishes.
How Hookdeck Can Help: Put Hookdeck in front of notification-style webhooks, where reliability matters more than the response body, and write enrichment results back through the REST API.
No event type, no delivery ID, no documented retries
The Problem: Your receiver cannot tell which campaign sent a request, cannot recognise a duplicate, and cannot rely on a failed call being sent again.
Why It Happens: The body is entirely your template. Marketo adds no event field, no request ID and no Content-Type that its docs commit to, and it does not describe retry behaviour.
Workarounds:
- Include a fixed
eventvalue,{{campaign.name}}and{{lead.Id}}in every template. - Build an idempotency key from those values and upsert on the person ID.
- Return
400for bodies that do not parse, so misconfigured templates show up quickly.
How Hookdeck Can Help: Hookdeck filters can route requests on your template's event field, deduplication can drop repeats based on the fields you choose, and every request is stored so you can replay it after a failure.
Best practices
Write templates that always parse
Set Request Token Encoding to JSON for JSON templates, never put quotes around tokens, and test the template with people whose fields contain quotes, ampersands and empty values. Reject bodies that do not parse with a 400.
Name the event in the body
Put a fixed event value and the campaign name in every template. A receiver that serves more than one campaign has nothing else to route on.
Use trigger campaigns only
Marketo's guidance is to use the Call Webhook flow step only in trigger campaigns. Follow it, and keep each Smart List narrow so only the people who need the call trigger it, because every call adds to the campaign's run time.
Make handlers idempotent
Marketo calls the webhook every time a person enters the flow, and it documents no delivery ID, so your receiver cannot assume one request per person. Upsert on lead.id instead of inserting. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Store the request, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Conclusion
A Marketo webhook sends whatever its Payload Template says, once per person, from a Call Webhook step in a trigger campaign. Marketo signs nothing, so a secret custom header is your only authentication, and your template has to carry the event name, campaign and person ID your receiver needs. Keep responses fast, because campaign execution waits on every call, and do not rely on Marketo to retry.
Hookdeck Event Gateway checks the secret header, answers Marketo straight away, and holds each request until your service has processed it, so a slow or failing receiver no longer drags a campaign down with it.
Get started with Hookdeck for free and handle Marketo webhooks reliably in minutes.