Set up webhooks
Get a signed HTTP callback every time something happens in your org - a donor crosses a threshold, a recurring gift fails, a checkout link is paid. Webhooks let your CRM, analytics, or Slack bot react in real time.
Outbound webhooks deliver a signed HTTP POST to a URL you choose every time a subscribed event fires in your organisation. Use them to push a donation into your reporting warehouse, ping #fundraising when a major gift lands, or trigger a thank-you sequence in your CRM the instant a donor confirms.
Before you start
You need:
- ADMIN role on your org. EDITORs and VIEWERs cannot manage webhooks.
- A Grow or Raise subscription. Outbound webhooks are gated; the Free tier does not include them.
- A receiving endpoint - an HTTPS URL on a server you control that can accept a POST. For experiments, an RequestBin or webhook.site URL works.
- The ability to verify a Standard Webhooks signature on your side. Libraries exist for every common language - see the Developer webhooks reference for snippets.
Steps
1. Open the webhooks page
Go to Settings -> Integrations -> Webhooks. You'll see a list of existing endpoints (empty on first visit) and an Add endpoint button.
If you're on Free, the page shows an upgrade prompt instead of the form. Bump to Grow first.
2. Add an endpoint
Click Add endpoint and fill in:
- URL. The HTTPS address that should receive deliveries. Must be reachable from the public internet. Together blocks private-network addresses (loopback, RFC 1918, link-local) to prevent SSRF.
- Description. Optional internal label so future-you can remember what this endpoint is for ("Mailchimp sync", "Slack #fundraising bot").
- Event types. Tick the events you want delivered. Defaults to all. Be conservative - the fewer events, the less load on your receiver and the lower the cost of a bug.
Click Create endpoint.
3. Save the signing secret
Together generates a fresh signing secret per endpoint and shows it to you once. Copy it into your receiver's environment (e.g. TOGETHER_WEBHOOK_SECRET) before you close the dialog.
You can rotate the secret later (in case it leaks), but the rotation invalidates the previous secret on every subsequent delivery, so you must update your receiver atomically.
4. Verify signatures on your end
Every delivery carries three headers your receiver must check:
webhook-id: msg_2W3kZQ...
webhook-timestamp: 1716480000
webhook-signature: v1,<base64-hmac-sha256>
The Developer reference has copy-paste snippets for Node, Python, Ruby, and Go using the official standardwebhooks library. Do not trust an unsigned payload. A receiver that doesn't verify is vulnerable to anyone with the URL forging events.
5. Send a test event
The simplest sanity check is to trigger an event manually. The cheapest options:
- Create a donor at Donors. The endpoint should receive a
donor.createddelivery within a few seconds. - Edit a donation form at Engage -> Forms and save. You'll get
donation_form.updated.
If your receiver returns 2xx, the delivery is marked Delivered. Anything else (4xx, 5xx, timeout, network error) is marked Failed and retried with exponential backoff: 60s, 5m, 30m, 3h, 24h, then dead-lettered.
Watching deliveries
Click an endpoint's row at Settings -> Integrations -> Webhooks to open its detail page. The Recent deliveries table shows:
| Column | What it means |
|---|---|
| Event | The event type and message id (webhook-id header). |
| Sent at | When Together attempted the delivery. |
| Status | Delivered (2xx response), Pending (in retry queue), Failed (gave up after 6 attempts), or in-flight. |
| HTTP | Your receiver's response code, or the network error (timeout, DNS, TLS). |
| Attempts | How many tries Together has made. |
Click a row to see the full request body and response headers. This is the fastest way to diagnose a receiver that's parsing the payload wrong.
Common events to subscribe to
Pick what your integration actually needs. Subscribing to everything is rarely the right answer.
Fundraising operations
donation.succeeded- funds settled. Use this for "actual money has cleared" - it accounts for BECS / PayTo, which can clear days after the donor submitted.donation.refunded- a refund landed (full or partial). The payload carries the new refunded total.donation.failed- a recurring payment dishonoured, a card declined, or a Stripe API error at submit time. Useful for triggering a recovery email or a phone call.
Donor data
donor.created- a new donor record landed (manual entry, donation form, CRM sync).donor.updated- any field changed. Dispatch on the payload'supdated_atand a content hash; we dedupe concurrent updates.donor.merged- two records were merged.data.survivoris who remains;data.merged_donor_idis the absorbed record.
Compliance signals (premium)
disclosure.created- a donor crossed the AEC threshold. The payload carries the receipt number, trigger amount, and deadline. Good for routing to your compliance lead's inbox.disclosure.updated- status changed (PENDING -> DUE_SOON -> OVERDUE -> ORG_DISCLOSED). Dispatch ondata.status.
The Developer reference has the full event catalogue plus the schemas. The OpenAPI spec at /api/v1/openapi.json is the machine-readable source of truth.
Donor PII (email, name, organisation name) is included in event payloads so receivers can act without a follow-up API call. Treat your endpoints as PII egress: HTTPS only, terminate TLS on infrastructure you control, and don't point at a service you haven't done diligence on.
When to disable or rotate
- Receiver is down for maintenance. Toggle the endpoint Inactive at Settings -> Integrations -> Webhooks. Deliveries pause; we don't backfill on resume, but the next event after re-enable delivers normally.
- Secret leaked. Click Rotate secret on the endpoint. The old secret stops verifying immediately - coordinate with whoever owns the receiver.
- Decommissioning. Click Revoke. The endpoint is permanently disabled and stops appearing in active lists.
What to do next
- Hook up your CRM to act on donor + donation events: most CRMs accept an incoming webhook or have a Zapier-style middleware that does.
- Pick a receiver pattern that matches your scale. For one-off integrations, a serverless function works. For higher volumes, an in-house queue (SQS, RabbitMQ) in front of your handler is safer than synchronous processing in the request thread.
- Read the full catalogue at Developer -> Webhooks - the event reference covers payload schemas, dedupe semantics, and the retry policy in detail.