Gareth Wilson Gareth Wilson

Guide to Salesforce Webhooks: Features and Best Practices

Published


Salesforce is the world's most widely adopted CRM platform, but unlike many modern SaaS tools, it doesn't offer a single, native webhook primitive. Instead, Salesforce provides several event-driven integration mechanisms — Outbound Messages, Platform Events, Change Data Capture, Apex Callouts, and Flow HTTP Callout Actions — that collectively serve webhook-like purposes. Each comes with its own protocol, payload format, and set of trade-offs.

This guide covers how Salesforce's webhook mechanisms work, how to configure them, and where they fall short.

Salesforce Webhook Features

FeatureDetails
Native webhook primitiveNo — multiple mechanisms serve webhook-like purposes
Outbound notificationsOutbound Messages (SOAP/XML), triggered by Workflow Rules or Approval Processes
REST/JSON outbound callsApex HTTP Callouts or Flow HTTP Callout Actions
Event streamingPlatform Events (custom pub/sub) and Change Data Capture (record changes)
Inbound webhooksCustom Apex REST endpoints via @RestResource hosted on Salesforce Sites
Hashing algorithmNo native HMAC on Outbound Messages; custom HMAC available via Apex Callouts; auto-generated HMAC secret on Data Cloud Webhook Targets
Retry mechanismOutbound Messages: exponential backoff for up to 24 hours (max 2-hour interval)
Delivery guaranteeAt-least-once (duplicates possible)
Delivery orderNot guaranteed
Manual retryYes — via Setup → Outbound Messages → Message Delivery Status
Logging / monitoringOutbound Message delivery status in Setup; Platform Event usage in Event Manager
Declarative (no-code) optionFlow HTTP Callout Actions (GA since Winter '24)

Outbound Message Payload Structure

Outbound Messages deliver SOAP/XML payloads. A typical notification looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Body>
    <notifications xmlns="http://soap.sforce.com/2005/09/outbound">
      <OrganizationId>00D000000000001AAA</OrganizationId>
      <ActionId>04k000000000001AAA</ActionId>
      <SessionId xsi:nil="true"/>
      <EnterpriseUrl>https://na1.salesforce.com/services/Soap/c/62.0/00D000000000001</EnterpriseUrl>
      <PartnerUrl>https://na1.salesforce.com/services/Soap/u/62.0/00D000000000001</PartnerUrl>
      <Notification>
        <Id>04l000000000001AAA</Id>
        <sObject xsi:type="sf:Opportunity" xmlns:sf="urn:sobject.enterprise.soap.sforce.com">
          <sf:Id>006000000000001AAA</sf:Id>
          <sf:Name>Acme Corp Deal</sf:Name>
          <sf:StageName>Closed Won</sf:StageName>
          <sf:Amount>75000.0</sf:Amount>
          <sf:CloseDate>2026-02-27</sf:CloseDate>
        </sObject>
      </Notification>
    </notifications>
  </soapenv:Body>
</soapenv:Envelope>
FieldDescription
OrganizationIdThe Salesforce Org ID — used to verify message origin
ActionIdThe ID of the Outbound Message action that triggered the notification
SessionIdOptional session ID for making callbacks to Salesforce (can be nil)
EnterpriseUrl / PartnerUrlSOAP API endpoints for callbacks to the sending org
Notification > IdUnique notification ID — use for idempotency checks
sObjectThe record data, including only the fields selected in the Outbound Message configuration

Platform Event Payload Structure

Platform Events use JSON and are consumed via the Pub/Sub API or CometD:

{
  "schema": "dPhGFly5L4vuzmw38W3vMg",
  "payload": {
    "CreatedDate": "2026-02-27T14:30:00.000Z",
    "CreatedById": "005000000000001AAA",
    "Order_Status__c": "Shipped",
    "Order_Number__c": "ORD-2026-00542",
    "Tracking_URL__c": "https://tracking.example.com/abc123"
  },
  "event": {
    "replayId": 684215,
    "EventUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
FieldDescription
schemaSchema fingerprint for the Platform Event definition
payloadCustom fields defined on the Platform Event object
event.replayIdSequential ID for replaying missed events within the retention window
event.EventUuidGlobally unique event identifier

Authentication Methods

MethodMechanismApplicable To
Org ID verificationOrganizationId included in payloadOutbound Messages
Session ID callbackOptional SessionId for authenticating callbacksOutbound Messages
Mutual TLS (two-way SSL)Client certificate exchangeOutbound Messages, Apex Callouts
Named CredentialsCentralized credential management with OAuth, Basic Auth, JWT, or custom headersApex Callouts, Flow HTTP Callouts
Custom HMAC signingProgrammatic HMAC header generation in ApexApex Callouts
Auto-generated HMAC secretPlatform-managed secret keyData Cloud Webhook Targets
IP whitelistingRestrict inbound requests by source IPInbound Apex REST endpoints

Setting Up Outbound Webhooks

Flow HTTP Callout Actions are the simplest way to send REST/JSON webhook-style notifications from Salesforce without writing code.

  1. Create a Named Credential: Navigate to Setup → Named Credentials. Configure the endpoint URL and authentication method for your webhook receiver.
  2. Register an External Service: Go to Setup → External Services. Create a new service from an API specification (OpenAPI/Swagger) or let Salesforce auto-generate one from the HTTP Callout configuration.
  3. Build a Record-Triggered Flow: In Flow Builder, create a Record-Triggered Flow on the object you want to monitor (e.g., Opportunity). Set the trigger conditions (e.g., Stage equals "Closed Won").
  4. Add an HTTP Callout Action: In the flow, add an Action element → select your External Service callout. Map record fields to the request body. Select the HTTP method (POST, PUT, PATCH).
  5. Activate the Flow: Save and activate. Salesforce will now send an HTTP request to your endpoint whenever the trigger conditions are met.

Option 2: Apex HTTP Callout (Programmatic)

For full control over payload structure, headers, and authentication, use Apex HTTP Callouts:

public class WebhookService {

    @future(callout=true)
    public static void sendWebhook(String recordId) {
        Opportunity opp = [
            SELECT Id, Name, StageName, Amount, CloseDate,
                   Account.Name
            FROM Opportunity
            WHERE Id = :recordId
        ];

        Map<String, Object> payload = new Map<String, Object>{
            'event' => 'opportunity.closed_won',
            'timestamp' => Datetime.now().formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''),
            'data' => new Map<String, Object>{
                'id' => opp.Id,
                'name' => opp.Name,
                'stage' => opp.StageName,
                'amount' => opp.Amount,
                'closeDate' => String.valueOf(opp.CloseDate),
                'accountName' => opp.Account.Name
            }
        };

        String body = JSON.serialize(payload);
        String secret = 'your_webhook_secret';
        Blob hmac = Crypto.generateMac(
            'HmacSHA256',
            Blob.valueOf(body),
            Blob.valueOf(secret)
        );
        String signature = EncodingUtil.base64Encode(hmac);

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Webhook_Endpoint/events');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('X-Webhook-Signature', signature);
        req.setBody(body);
        req.setTimeout(30000);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() < 200 || res.getStatusCode() >= 300) {
            System.debug(LoggingLevel.ERROR,
                'Webhook failed: ' + res.getStatusCode() + ' ' + res.getBody());
        }
    }
}

Call this from an Apex Trigger:

trigger OpportunityWebhook on Opportunity (after update) {
    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
        if (opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won') {
            WebhookService.sendWebhook(opp.Id);
        }
    }
}

Setting Up Inbound Webhooks

To receive webhooks from external services, expose a custom REST endpoint using @RestResource:

@RestResource(urlMapping='/webhook/incoming/*')
global class InboundWebhookHandler {

    @HttpPost
    global static void handlePost() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        // Verify authentication header
        String authToken = req.headers.get('X-Webhook-Token');
        if (authToken != 'expected_token_value') {
            res.statusCode = 401;
            res.responseBody = Blob.valueOf('{"error":"Unauthorized"}');
            return;
        }

        String body = req.requestBody.toString();
        Map<String, Object> payload =
            (Map<String, Object>) JSON.deserializeUntyped(body);

        // Process the incoming event
        String eventType = (String) payload.get('event');
        Map<String, Object> data =
            (Map<String, Object>) payload.get('data');

        // Handle accordingly
        if (eventType == 'payment.completed') {
            processPayment(data);
        }

        res.statusCode = 200;
        res.responseBody = Blob.valueOf('{"status":"received"}');
    }

    private static void processPayment(Map<String, Object> data) {
        // Insert or update records based on incoming data
    }
}

Host this endpoint on a Salesforce Site and grant the guest user profile access only to the specific Apex class. The resulting endpoint URL follows the pattern: https://<site-domain>/services/apexrest/webhook/incoming.

Best Practices

Verify Webhook Signatures

When receiving webhooks on an external endpoint from Salesforce Apex Callouts that include HMAC signatures, always verify them:

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express middleware
app.post('/salesforce/webhook', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const rawBody = req.rawBody; // ensure raw body is preserved

  if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process the event
  res.status(200).json({ status: 'received' });
});

Implement Idempotency

Salesforce's at-least-once delivery guarantee means duplicate messages are expected. Always deduplicate:

import hashlib
import redis

r = redis.Redis()

def handle_webhook(event):
    # Use notification ID or event UUID as idempotency key
    event_id = event.get('event', {}).get('EventUuid') or event.get('notificationId')

    if not event_id:
        event_id = hashlib.sha256(
            json.dumps(event, sort_keys=True).encode()
        ).hexdigest()

    lock_key = f"sf_webhook:{event_id}"

    # Check if already processed (TTL of 72 hours to match Platform Event retention)
    if r.exists(lock_key):
        return {"status": "duplicate", "id": event_id}

    # Mark as processing
    r.setex(lock_key, 259200, "processing")  # 72-hour TTL

    # Process the event
    process_event(event)

    r.setex(lock_key, 259200, "completed")
    return {"status": "processed", "id": event_id}

Use Hub-and-Spoke for Event Distribution

Platform Event deliveries are counted per subscriber — one event delivered to five subscribers counts as five deliveries against your daily allocation. Implement a single listener that distributes events downstream:

Salesforce → [Single Subscriber] → Fan-out to N consumers

This approach keeps your delivery count at 1× per event rather than N×, preserving your daily allocation of 50,000 base deliveries.

Limitations and Pain Points

No Native Webhook Primitive

The Problem: Salesforce lacks a unified webhook system. Developers must evaluate and choose between Outbound Messages, Platform Events, CDC, Apex Callouts, or Flow HTTP Callouts — each with different formats, protocols, and constraints.

Why It Happens: Salesforce evolved as an enterprise CRM with SOAP-era integrations. Its event-driven architecture was added incrementally across multiple products rather than designed as a cohesive webhook system.

Workarounds:

  • Evaluate each mechanism against your specific use case.
  • Use Outbound Messages for simple record-change SOAP notifications, Platform Events for custom pub/sub, CDC for comprehensive change tracking including deletes, and Apex or Flow Callouts for REST/JSON webhook pushes.

How Hookdeck Can Help: Hookdeck provides a unified webhook infrastructure layer that normalizes events from any of Salesforce's mechanisms into a single, consistent pipeline with routing, transformation, and delivery management — eliminating the need to manage multiple integration patterns.

SOAP-Only Outbound Messages

The Problem: Outbound Messages use SOAP/XML exclusively. Most modern webhook consumers expect JSON over REST.

Why It Happens: Outbound Messages were designed during the SOAP era of Salesforce's architecture and have never been modernized to support REST/JSON.

Workarounds:

How Hookdeck Can Help: Hookdeck can receive SOAP/XML Outbound Messages and transform them into JSON before delivering to your downstream services, bridging the protocol gap without requiring custom middleware.

Strict Governor Limits and API Quotas

The Problem: Apex callouts are capped at 100 per transaction with a 120-second max timeout. Platform Event deliveries are capped at 50,000/day at base allocation. High-volume integrations can easily exhaust these quotas.

Why It Happens: Salesforce's multi-tenant architecture enforces strict governor limits to prevent any single org from monopolizing shared infrastructure resources.

Workarounds:

  • Implement hub-and-spoke patterns where one subscriber distributes events downstream.
  • Batch operations where possible.
  • Use asynchronous Apex (Queueable, Batch) to spread callouts across transactions.
  • Purchase add-on licenses for higher allocations (up to 150,000 deliveries/day).

How Hookdeck Can Help: By acting as the single subscriber endpoint, Hookdeck minimizes delivery counts on the Salesforce side while handling fan-out, queuing, and rate-limited delivery to multiple downstream consumers externally.

At-Least-Once Delivery with Possible Duplicates

The Problem: Outbound Messages may be delivered multiple times, even after acknowledgment. Platform Events can also result in duplicate deliveries during retries.

Why It Happens: Salesforce prioritizes delivery reliability over exactly-once semantics. Network issues, internal retries, and queuing mechanics can cause duplicate sends.

Workarounds:

  • Implement idempotency on the receiving endpoint using notification IDs or event ReplayIds.
  • Maintain a deduplication store that tracks processed message identifiers.

How Hookdeck Can Help: Hookdeck provides built-in deduplication that automatically detects and filters duplicate events before they reach your downstream services, eliminating the need to build custom idempotency logic.

No Delivery Order Guarantee

The Problem: Outbound Messages may arrive out of sequence. Platform Events may also be delivered out of order under high load.

Why It Happens: Messages are queued and sent by background processes that don't enforce ordering. Exponential backoff retries further shuffle delivery timing.

Workarounds:

  • Include timestamps in payloads and implement ordering logic on the consumer side.
  • Use sequence numbers or version fields to detect and handle out-of-order messages.

How Hookdeck Can Help: Hookdeck supports ordered delivery by sequencing events based on configurable keys, ensuring your consumers receive events in the correct order even when Salesforce delivers them out of sequence.

No Native HMAC Signature Verification

The Problem: Outbound Messages don't include HMAC signatures for webhook authentication. This makes it harder to verify message authenticity at the receiver.

Why It Happens: Outbound Messages rely on Org ID and optional Session ID for identity verification rather than modern cryptographic signing mechanisms.

Workarounds:

  • Use Apex HTTP Callouts with custom HMAC header signing.
  • Enable mutual TLS (two-way SSL) for certificate-based verification.
  • Implement IP whitelisting on the receiver.

How Hookdeck Can Help: Hookdeck adds HMAC signature verification to incoming events and signs outbound deliveries, providing the cryptographic authentication layer that Salesforce Outbound Messages lack.

Short Retry Windows and No Dead Letter Queue

The Problem: Outbound Messages retry for only 24 hours before being permanently dropped. Platform Events are retained for 72 hours maximum. There is no built-in dead letter queue for failed deliveries.

Why It Happens: Salesforce's queue management is designed for transient failures, not extended outages. Storage constraints in multi-tenant architecture limit retention periods.

Workarounds:

  • Monitor delivery status proactively via Setup → Outbound Messages.
  • Set up alerts for failed messages.
  • Implement external retry and replay logic.
  • Store events in an external system for long-term retention.

How Hookdeck Can Help: Hookdeck provides configurable retry policies with much longer retention windows, automatic dead letter queuing for persistently failed deliveries, and manual replay capabilities — ensuring no events are lost even during extended downstream outages.

Limited Payload Customization (Outbound Messages)

The Problem: Outbound Messages send only field values from the triggering record. You cannot include related records, computed values, or custom payload structures.

Why It Happens: Outbound Messages were designed as a simple notification mechanism, not a flexible data export tool.

Workarounds:

  • Use Apex HTTP Callouts for full payload control — query related data, compute values, and structure the payload exactly as needed.
  • Alternatively, use Platform Events with custom fields for structured payloads.

How Hookdeck Can Help: Hookdeck's transformation capabilities can enrich and reshape payloads after they leave Salesforce, adding computed fields or restructuring data before delivery to your consumers.

Testing Salesforce Webhooks

Testing webhook integrations from Salesforce requires triggering the actual events:

  1. Outbound Messages: Create or update a record that matches the Workflow Rule criteria. Monitor delivery status at Setup → Outbound Messages → click the message name → "View Message Delivery Status."
  2. Apex Callouts: Use Salesforce's Developer Console or Anonymous Apex to invoke your callout method directly. For unit testing, Salesforce requires HttpCalloutMock implementations:
@isTest
private class WebhookServiceTest {

    private class MockHttpResponse implements HttpCalloutMock {
        public HTTPResponse respond(HTTPRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(200);
            res.setBody('{"status":"received"}');
            return res;
        }
    }

    @isTest
    static void testSendWebhook() {
        Test.setMock(HttpCalloutMock.class, new MockHttpResponse());

        Opportunity opp = new Opportunity(
            Name = 'Test Deal',
            StageName = 'Closed Won',
            CloseDate = Date.today(),
            Amount = 50000
        );
        insert opp;

        Test.startTest();
        WebhookService.sendWebhook(opp.Id);
        Test.stopTest();
    }
}
  1. Platform Events: Publish test events via Anonymous Apex with EventBus.publish() and subscribe through the Pub/Sub API or CometD to verify receipt.
  2. Flow HTTP Callouts: Use Flow Builder's Debug mode to step through the flow and inspect the HTTP request and response.

Conclusion

Salesforce's webhook capabilities are powerful but fragmented. Rather than a single webhook feature, developers must navigate Outbound Messages, Platform Events, Change Data Capture, Apex Callouts, and Flow HTTP Callout Actions — each suited to different integration scenarios. Flow HTTP Callout Actions represent Salesforce's most accessible option for teams that want REST/JSON outbound calls without writing code, while Apex Callouts remain the most flexible choice for teams that need full control over payloads, headers, and authentication.

The key challenges are inherent to Salesforce's multi-tenant architecture and legacy design decisions. For simple, low-volume integrations, Salesforce's native tools are often sufficient. For production workloads that demand reliable delivery, signature verification, payload transformation, and robust retry logic, an external webhook infrastructure layer like Hookdeck can bridge the gaps between what Salesforce provides and what modern event-driven architectures require.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.