# 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:

```json
{
  "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

1. Generate a long random secret, for example with `openssl rand -hex 32`, and store it on your server as `MARKETO_WEBHOOK_SECRET`.
2. In Marketo, go to **Admin > Webhooks** and click **New Webhook**.
3. Enter a name and your HTTPS endpoint URL, set **Request Type** to `POST`, and write the **Payload Template**. Use **Insert Token** to add tokens.
4. Set **Request Token Encoding** to `JSON` for a JSON template, set **Response Type**, and click **Create**.
5. Select the webhook, choose **Webhook Actions > Set Custom Header**, and add a header such as `X-Webhook-Secret` with your secret as its value.
6. 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.

```javascript
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:

```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](/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](/docs/destinations#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 `event` value, `{{campaign.name}}` and `{{lead.Id}}` in every template.
* Build an idempotency key from those values and upsert on the person ID.
* Return `400` for bodies that do not parse, so misconfigured templates show up quickly.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can route requests on your template's `event` field, [deduplication](/docs/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](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Store the request, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Marketo webhooks reliably in minutes.