Back to all integrations
Webhook

Outbound Webhooks

Pushing AppBO events to your own endpoint, with payload shape, signature verification, retries and idempotent handling.

On this page
What it connectsHow to connect itWhat data movesWorked exampleLimits and gotchas
Need this wired up?

Bring your stack to a call and we will connect it, test every field in both directions, and hand it over documented.

Book a call
01

What it connects

An outbound webhook is the platform telling your system that something happened. On one side is AppBO, emitting events as contacts are created, opportunities change stage, appointments are booked or canceled, forms are submitted, messages arrive or payments settle. On the other side is an HTTP endpoint you own and operate. A route in your application, a serverless function, a queue-backed receiver, or an automation platform's catch hook standing in for one.

Outbound webhooks exist so that you never have to poll. Polling is expensive, slow to notice change, and burns through the account's API budget for the privilege of being told nothing happened. A webhook pushes the change within seconds of it occurring, which is the difference between a near-real-time mirror and a nightly reconciliation job. For anything user-facing. A dashboard, a notification, an entitlement that must flip the moment a payment lands. That latency difference is the whole feature.

There are two ways to emit them. The account-level webhook configuration sends a defined set of platform events to a URL for everything that happens in that account. A workflow step, by contrast, fires a webhook only for the contacts that reached that point in that workflow, with a payload you assemble yourself. The first is a systematic feed; the second is a precise, business-logic-shaped signal. Most mature integrations use both: the systematic feed to stay in sync, and workflow webhooks for the handful of moments that matter.

02

How to connect it

  1. Build the receiving endpoint first. It must accept POST, parse JSON, and return a success status quickly: within a couple of seconds. Anything slower risks being treated as a failure and retried.
  2. Serve the endpoint over HTTPS with a valid certificate. Self-signed certificates and expired chains are rejected, and the resulting failures are easy to misread as the platform not sending.
  3. Make the handler do almost nothing inline: validate, enqueue, respond. Do the real work (database writes, downstream API calls, email) in a background worker. Processing synchronously is the single most common cause of webhook timeouts.
  4. Decide which events you need and where they come from. Use the account-level webhook configuration for the standard event catalog; use a workflow webhook step for anything conditional or with a custom payload.
  5. Register the endpoint. For account-level events, add the URL in the platform's webhook settings and select the event types. For workflow webhooks, add the webhook action to the workflow and set the method, URL, headers and body.
  6. Establish authentication in both directions. Have the platform send a credential your endpoint can check (a bearer token or shared secret in a custom header on a workflow webhook) and reject any request that lacks it.
  7. Verify the signature where the platform provides one. Platform-emitted events are commonly signed; consult the current webhook documentation for the header name and the verification algorithm, and implement the check exactly as specified rather than inventing a scheme. Where no signature is available, the shared-secret header plus an allowlist is your substitute.
  8. Capture one real event before writing any mapping code. Log the raw body verbatim, in full, and build your parser from that rather than from an assumed shape.
  9. Define your idempotency key. Use the event identifier if the payload carries one; otherwise compose one from the object identifier plus the event type plus the timestamp. Store processed keys and skip repeats.
  10. Handle unknown and new event types by ignoring them gracefully. The platform may add fields and event types; a handler that throws on an unrecognised key will break on an update you did not ask for.
  11. Return the right statuses. 2xx means accepted and stops retries. 4xx means do not bother retrying, which is correct for a payload you will never be able to process. 5xx means try again.
  12. Add a dead-letter path. When a queued event fails processing repeatedly, park it with the full raw body so it can be replayed after a fix, rather than losing it.
  13. Verify the connection is live: trigger the event for real (create a contact, move an opportunity) and confirm the request arrives, the signature or secret validates, and your worker produced the expected result.
  14. Test the failure path too. Return a 500 deliberately and confirm you see the retry behavior you expected, so you know what a real outage will look like.
  15. Monitor in production: alert on a drop in event volume (a silent webhook is worse than a failing one), on signature verification failures, and on dead-letter depth.
03

What data moves

Platform to your endpoint

  • Event envelope: event type, an event or request identifier, a timestamp, and the account or location identifier the event belongs to.
  • The affected object: for contact events, the contact record with its standard and custom fields; for opportunity events, the opportunity with pipeline, stage, value and linked contact; for appointments, the calendar, assigned user, times and status.
  • Form and survey submissions, with the submitted field set.
  • Message and conversation events, with direction, channel and body.
  • For workflow webhook steps, exactly the payload you composed, which can include merge values from the contact, the workflow context and static fields of your own.

Your endpoint back to the platform

  • An HTTP status code, which controls retry behavior. That is effectively the whole channel.
  • Response bodies are not consumed as workflow data by an account-level webhook. Where a workflow step is capable of reading a response, treat that as a separate documented capability and confirm it before designing around it.

What does not move

  • Historical events from before the webhook was registered. There is no replay of the past; backfill through the REST API.
  • Related objects not referenced in the payload. A contact event does not carry that contact's entire conversation history: fetch what you need by identifier.
  • Guaranteed ordering. Events can arrive out of sequence, and a retry of an older event can land after a newer one.
  • Exactly-once delivery. At-least-once is the realistic assumption.
  • Any data from an account the webhook was not configured on.
04

Worked example

Scenario: your billing system must grant access the moment an opportunity reaches the Won stage in AppBO.

  1. You deploy POST /hooks/appbo behind HTTPS. The handler validates, pushes the body onto a queue, and returns 202. Median response time is under fifty milliseconds.
  2. In the platform you build a workflow triggered by opportunity stage change, filtered to the Won stage of the sales pipeline, ending in a webhook action.
  3. You configure the webhook action to POST to your endpoint with a custom header carrying a shared secret, and a flat JSON body: event_type, event_id, occurred_at, contact_id, email, opportunity_id and plan_code taken from a custom field.
  4. You move one test opportunity to Won. Your logs show the raw body. You build the parser from that body, not from your notes.
  5. Going live: a sales rep drags a real opportunity into Won. The workflow fires within seconds.
  6. Your handler compares the shared-secret header to the stored value, enqueues the body, and returns 202.
  7. The worker picks the job up, computes the idempotency key from event_id, and finds no prior record of it.
  8. The worker looks up the customer by email, finds the account, activates the entitlement for plan_code, and writes the event_id into its processed-events table inside the same transaction.
  9. A transient network blip means the platform never saw your 202, so it retries the event. The worker computes the same idempotency key, finds it already processed, and returns success without touching the entitlement again.
  10. The following week a deploy breaks the worker. It returns an error, the job retries, exhausts its attempts and lands in the dead-letter queue with the raw body intact. Your alert on dead-letter depth fires.
  11. You fix the bug, replay the dead-lettered events, and the entitlements grant with no data loss, because the raw body was preserved and processing was idempotent.
05

Limits and gotchas

Rate limiting

The constraint here is usually on your side, not the platform's. A bulk action, importing contacts, mass-updating a pipeline, a workflow that enters thousands of contacts at once: produces a matching burst of webhook deliveries. Size your receiver for the burst, not the average, and put a queue in front of it so a spike becomes backlog rather than dropped events. On the platform side, be aware that a workflow webhook step consumes workflow executions like any other step.

Retries and idempotency

Delivery is at-least-once. Any non-2xx response, and any response slow enough to time out, triggers a retry on a backoff schedule, and because a timeout can follow successful processing, duplicates are normal operation rather than an anomaly. Deduplicate on an event identifier, and make the side effects of processing idempotent in their own right: grant an entitlement rather than increment a counter, upsert rather than insert. Return 4xx for payloads you can never process so the platform stops retrying, and reserve 5xx for genuinely transient failures.

Field mapping traps

Payload shapes differ between account-level events and workflow webhook steps, so code written against one will not parse the other. Custom fields may appear keyed by identifier rather than by label. Empty values may arrive as an empty string, null or an absent key: handle all three. Never assume a field is present because it was present in your sample, and never fail hard on a field you did not expect.

Duplicate creation downstream

The classic failure is a webhook that creates a record in your system on every delivery. Two retries become three customers. Key every write on something stable from the payload, and treat creation as an upsert.

Sandbox versus live

Use separate endpoints per environment, and never register a staging URL on a production account. A staging worker connected to production events can send real email to real customers from a half-finished template. Keep the registration list as a reviewed artefact, because stale endpoints that nobody owns are a quiet security problem.

Auth expiry

Shared secrets rotate, and the rotation has to happen in two places at once. Accept both the old and new secret for an overlap window, then drop the old one. If you verify a platform signature, pin to the documented algorithm and header, and re-read the webhook documentation before assuming it is unchanged after a platform update.

What commonly breaks

  • An expired TLS certificate on the receiving endpoint, which looks exactly like the platform having stopped sending.
  • Synchronous processing causing timeouts, retries, and then duplicates.
  • Silent failure: nobody notices a webhook stopped because there is no alert on absence of traffic.
  • Out-of-order delivery letting a stale status overwrite a current one; compare timestamps before writing.
  • A workflow edited so the webhook step is skipped by a new branch, with no error anywhere.
  • Handlers that throw on new fields or unrecognised event types after a platform update.

Connect it with us.

We wire the integration, map the fields, test both directions and hand it over with the SOPs your team needs to run it.

Book a call