# Marketo Webhooks

Marketo doesn't have webhook subscriptions. A webhook is a definition you create
in **Admin → Webhooks** (URL, request type, payload template, encoding) and then
call from a Smart Campaign with the **Call Webhook** flow step. Marketo's docs
say to use that step in **trigger campaigns** only.

This differs from most providers in three ways:

1. **There is no signature.** Marketo doesn't sign the body. There's no HMAC,
  signing secret or timestamp header, so don't look for an `X-Marketo-Signature`.
  You add a secret header yourself.
2. **You write the payload.** The Payload Template is any text that works in an
  HTTP POST body, with tokens substituted per lead. There are no fixed event
  type strings, so you put an `event` name in the body yourself.
3. **You choose the request.** The method (GET or POST), token encoding (JSON or
  Form/Url) and headers are all set on the webhook.

## When to Use This Skill

* How do I receive Marketo webhooks?
* How do I verify Marketo webhook signatures? (You can't. Send a shared secret
  in a custom header instead.)
* How do I set up "Call Webhook" in a Marketo Smart Campaign?
* What payload does a Marketo webhook send? (Whatever your Payload Template says.)
* How do I tell a lead-created webhook from a lead-updated one?
* Why does my Marketo webhook time out? (Marketo waits at most 30 seconds.)

## Verification (core): a shared secret custom header

Open the webhook in **Admin → Webhooks**, choose **Webhook Actions → Set Custom
Header**, and add a Key and Value:

```
X-Webhook-Secret: 3f9c0e...a long random hex value...

```

The header name is your choice. Custom headers can contain tokens, but a fixed
random value is what you want here. Check it in constant time before you parse
the body:

```javascript
const crypto = require('crypto');

// NOT a Marketo signature. This is a secret YOU put in the webhook's custom headers.
function verifyMarketoSecret(headerValue) {
  const expected = process.env.MARKETO_WEBHOOK_SECRET;
  if (!expected) throw new Error('MARKETO_WEBHOOK_SECRET is not set'); // fail closed
  if (typeof headerValue !== 'string') return false;
  const a = Buffer.from(headerValue);
  const b = Buffer.from(expected);
  // Length check first: timingSafeEqual throws on a length mismatch.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express lowercases header names: req.get('X-Webhook-Secret') works too.

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/marketo-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/marketo-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/marketo-webhooks/examples/fastapi/).

Return `401` for a missing or wrong secret and `500` when the secret isn't
configured, so the endpoint never accepts everything by accident. The secret
travels in a header over HTTPS, so treat it as a bearer token and rotate it by
editing the webhook. See [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/marketo-webhooks/references/verification.md).

## The Payload: you define it

Set **Request Type** `POST` and **Request Token Encoding** `JSON`, then write a
Payload Template with a fixed `event` field for each webhook. Use **Insert
Token** to add tokens. Marketo adds double quotes around string tokens itself, so
don't wrap them in quotes:

```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}},
    "company": {{lead.Company Name}}
  }
}

```

Use the same body with `"event": "lead_updated"` for a second webhook.

* `event` is **this skill's convention**, not a Marketo field. Marketo defines
  no event-type strings for webhooks.
* Token names depend on your instance's fields. Pick them with **Insert Token**
  rather than typing them.
* A token value with special characters can break a JSON template, so keep
  **Request Token Encoding** on JSON and return `400` for bodies that don't parse.
* Treat the body as a notification. Re-fetch the lead from the Marketo REST API
  before you act on anything important.

## Events (this skill's `event` values)

| `event` | Smart Campaign trigger (example) | Fires when |
| --- | --- | --- |
| `lead_created` | Person is Created | A new lead enters the database and the campaign flow calls the webhook |
| `lead_updated` | Data Value Changes | A lead field changes and the campaign flow calls the webhook |

The trigger is whatever you put on the campaign's Smart List. If an `event`
value isn't one you handle, log it and return `200`. If the body has no `event`
field, the webhook is misconfigured, so return `400`.

## Delivery and Timeouts

* Marketo waits up to **30 seconds** for a response, then times out. Webhook
  execution time directly delays the campaign: even 50 ms per call means 100,000
  calls take about 1.5 hours. Respond fast and process later.
* **Response mappings** write response properties back to the lead, and only
  apply when you return a `2xx`. A plain `200` with `{"received": true}` is fine.
* Marketo's docs don't describe retries or a delivery ID, so dedupe on values you
  put in the body (e.g. `event` + `lead.id` + `campaign`) or upsert on `lead.id`.
* The URL is sent as written: `&` stays `&` and `%26` stays `%26`.

## Environment Variables

```bash
# REQUIRED. The value you put in the webhook's custom header. Generate with: openssl rand -hex 32
MARKETO_WEBHOOK_SECRET=
# OPTIONAL. The custom header name (case-insensitive). Default: X-Webhook-Secret
MARKETO_WEBHOOK_HEADER=X-Webhook-Secret

```

## Local Development

```bash
npx hookdeck-cli listen 3000 marketo --path /webhooks/marketo

```

No account required. The CLI creates a guest account on first run and gives you
a public HTTPS URL to paste into the webhook's URL field, plus a web UI for
inspecting requests.

In production, Hookdeck's **Marketo** source type checks the header for you. You
enter the header name and its exact value, and requests without them are
rejected. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/marketo-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/marketo-webhooks/references/overview.md) - How webhooks and Call Webhook work, properties, tokens, limits
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/marketo-webhooks/references/setup.md) - Step-by-step webhook and campaign setup, custom header, testing, Hookdeck source setup
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/marketo-webhooks/references/verification.md) - Why there is no signature, the shared-secret header pattern, gotchas