$npx -y skills add hookdeck/webhook-skills --skill salesforce-webhooksReceive and verify Salesforce Outbound Messages (the native "webhook" for Flow/Workflow). Use when setting up a Salesforce Outbound Message listener, parsing the SOAP/XML notification, validating the OrganizationId, returning the required Ack response, or handling Account, Contac
| 1 | # Salesforce Webhooks (Outbound Messages) |
| 2 | |
| 3 | Salesforce has no classic HMAC-signed webhook. The closest native push is the |
| 4 | **Outbound Message**, configured on a **Flow** or **Workflow Rule**, which |
| 5 | POSTs a **SOAP/XML** envelope to your HTTPS endpoint when a record changes. |
| 6 | |
| 7 | There is **no signature header**. You authenticate the message by: |
| 8 | |
| 9 | 1. **Validating `<OrganizationId>`** in the SOAP body against your known 18-char Salesforce org id. |
| 10 | 2. **Enforcing HTTPS** and restricting inbound traffic to **Salesforce IP ranges**. |
| 11 | 3. Optionally **mutual TLS** (Salesforce can present a client certificate). |
| 12 | |
| 13 | Your endpoint **must return a SOAP `<Ack>true</Ack>` envelope** with HTTP `200`, |
| 14 | or Salesforce retries the message for up to **24 hours**. |
| 15 | |
| 16 | ## When to Use This Skill |
| 17 | |
| 18 | - How do I receive Salesforce webhooks / Outbound Messages? |
| 19 | - How do I parse the Salesforce Outbound Message SOAP/XML body? |
| 20 | - How do I validate the OrganizationId on a Salesforce Outbound Message? |
| 21 | - What Ack response does a Salesforce Outbound Message listener return? |
| 22 | - How do I handle Account, Opportunity, Contact, Lead, or Case record changes? |
| 23 | - Should I use Outbound Messages, Platform Events, or Change Data Capture? |
| 24 | |
| 25 | ## Verification (core) |
| 26 | |
| 27 | Outbound Messages are unsigned. "Verification" = parse the SOAP body, match |
| 28 | `<OrganizationId>` (timing-safe) against your org id, then return the Ack. Use |
| 29 | the **raw** request body — Salesforce sends `Content-Type: text/xml`, so parse |
| 30 | XML, not JSON. |
| 31 | |
| 32 | ```javascript |
| 33 | const { XMLParser } = require('fast-xml-parser'); |
| 34 | const crypto = require('crypto'); |
| 35 | |
| 36 | // removeNSPrefix strips soapenv:/sf: prefixes so we can read plain element names. |
| 37 | const parser = new XMLParser({ ignoreAttributes: false, removeNSPrefix: true }); |
| 38 | |
| 39 | // rawXml = raw HTTP body string; expectedOrgId = your 18-char Salesforce org id. |
| 40 | function verifyOutboundMessage(rawXml, expectedOrgId) { |
| 41 | const msg = parser.parse(rawXml)?.Envelope?.Body?.notifications; |
| 42 | const orgId = String(msg?.OrganizationId ?? ''); |
| 43 | const a = Buffer.from(orgId), b = Buffer.from(expectedOrgId); |
| 44 | if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { |
| 45 | throw new Error('OrganizationId mismatch — reject with 401'); |
| 46 | } |
| 47 | return msg; // dispatch [].concat(msg.Notification) then return the Ack envelope below |
| 48 | } |
| 49 | ``` |
| 50 | |
| 51 | The **Ack envelope** every successful listener must return (`Content-Type: text/xml`, HTTP 200): |
| 52 | |
| 53 | ```xml |
| 54 | <?xml version="1.0" encoding="UTF-8"?> |
| 55 | <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> |
| 56 | <soapenv:Body> |
| 57 | <notificationsResponse xmlns="http://soap.sforce.com/2005/09/outbound"> |
| 58 | <Ack>true</Ack> |
| 59 | </notificationsResponse> |
| 60 | </soapenv:Body> |
| 61 | </soapenv:Envelope> |
| 62 | ``` |
| 63 | |
| 64 | > **For complete handlers with SOAP parsing, event dispatch, the Ack response, and tests**, see: |
| 65 | > - [examples/express/](examples/express/) |
| 66 | > - [examples/nextjs/](examples/nextjs/) |
| 67 | > - [examples/fastapi/](examples/fastapi/) |
| 68 | |
| 69 | ## Common Event Types |
| 70 | |
| 71 | Outbound Messages don't carry an event-name string. The "event" is the **sObject |
| 72 | type** the Flow/Workflow Rule is built on, plus the create/update action. Each |
| 73 | `<Notification>` contains one `<sObject>` whose `xsi:type` attribute is the type: |
| 74 | |
| 75 | | sObject (`xsi:type`) | Triggered When | Common Use Cases | |
| 76 | |----------------------|----------------|------------------| |
| 77 | | `Account` | Account created/updated | Sync CRM accounts, provision customers | |
| 78 | | `Contact` | Contact created/updated | Sync contacts, update mailing lists | |
| 79 | | `Lead` | Lead created/updated | Route leads, trigger enrichment | |
| 80 | | `Opportunity` | Opportunity created/updated/stage change | Update forecasts, notify sales | |
| 81 | | `Case` | Case created/updated | Sync support tickets, alert on escalation | |
| 82 | |
| 83 | A single message can include **up to 100 `<Notification>` elements**. Messages may |
| 84 | arrive **out of order** and **more than once** — handle idempotently on the |
| 85 | `<Notification>` `<Id>` or the sObject `<sf:Id>`. |
| 86 | |
| 87 | > **Full reference**: [Outbound Messaging](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_om_outboundmessaging.htm) |
| 88 | |
| 89 | ## Streaming Alternative |
| 90 | |
| 91 | For high-volume, real-time, or richer event streams, use **Platform Events** or |
| 92 | **Change Data Capture (CDC)** consumed over the **Pub/Sub API** (or the legacy |
| 93 | CometD Streaming API) rather than Outbound Messages. See |
| 94 | [references/overview.md](references/overview.md). |
| 95 | |
| 96 | ## Environment Variables |
| 97 | |
| 98 | ```bash |
| 99 | SALESFORCE_ORG_ID=00Dxx0000000 |