Skip to content
On this page and tools

Automation

Webhooks

Webhooks tell your server when something happens in your Bynefit space — a new member joins, a form is submitted, a donation arrives — without you having to poll for it.

The gist

  1. You tell Bynefit what URL to call and which events you care about.
  2. When an event happens, Bynefit sends an HTTP POST to that URL with a JSON payload.
  3. You respond with 200 to say "got it."
  4. If your server has trouble, Bynefit will retry for a while before giving up.

Who can use it: a Space admin sets up endpoints. Your server receives them. It is also how Make, n8n and Zapier catch Bynefit events. See No-code and automation tools.

Subscribe

Space admins can manage webhooks at /dash/integrations/webhooks. Give the endpoint a label (up to 160 characters), paste your delivery URL, optionally list the events you want (leave it empty for all of them), and click Add endpoint. You'll be given a signing secret — it starts with whsec_, and you copy it now because it's only shown once. Delivery URLs must be public web addresses (use https); an address that points at a private network is refused.

Keep your signing secret private. Anyone with it can forge delivery payloads. Put it in your server's environment, never in a web page or an app.

Delivery payload

POSTyour server
POST /your/webhook/url HTTP/1.1
Content-Type: application/json
User-Agent: Bynli-Webhooks/1.0
X-Bynefit-Event: form.submitted
X-Bynefit-Event-Id: evt_9c2e5a1f…
X-Bynefit-Delivery: 7b6c1d…
X-Bynefit-Timestamp: 1776800702
X-Bynefit-Signature: sha256=…

{
  "id": "evt_9c2e5a1f…",
  "type": "form.submitted",
  "team_id": 42,
  "created_at": "2026-04-21T19:45:02+00:00",
  "actor": { "id": null },
  "subject": { "type": "form_submission", "id": "sub_4d1e…" },
  "data": {
    "form_id": "frm_AbC123xYz",
    "form_name": "Contact us",
    "form_type": "standard"
  }
}
FieldMeaning
idUnique, stable id of the event. The same event keeps the same id on every retry.
typeThe event name, from the list below.
team_idThe space the event happened in.
created_atWhen it happened, ISO-8601.
actorWho triggered it: { "id": … }, with id null for system events and for people who are not signed in, such as someone filling in a form on a hosted site.
subjectThe record the event is about: type (for example contact, form_submission, task, membership, payment, call, feed_post) and an opaque id. Either can be null when an event has no single record.
dataEvent-specific details, listed below. May be null.

The payload tells you that something happened and which record it is about. It does not carry personal details such as form answers, email addresses or phone numbers. To get the answers to a form, read the submission from your server with a Space key and the forms permission. See Authentication.

Headers

HeaderValue
X-Bynefit-EventThe event name, same as type.
X-Bynefit-Event-IdThe event's id. Use this to de-duplicate.
X-Bynefit-DeliveryA unique id for this delivery attempt. It changes on every retry.
X-Bynefit-TimestampWhen the request was signed, in Unix seconds.
X-Bynefit-Signaturesha256=<hex>, described below.

X-Bynefit-* is the name to read. Every delivery also carries the same value under the older X-Bynli-* name (for example X-Bynli-Signature), so receivers written against it keep working. The header name is not part of what is signed. If you accept both names and a request carries both with different values, reject it.

Available events

In the event filter you can list exact names or a prefix wildcard such as task.*. Separate several with commas.

EventWhen it firesWhat data holds
contact.createdA contact is added to your space.source (for example manual)
contact.updatedA contact's details change.fields, the names of what changed
task.createdA task is created, including follow-up tasks Bynefit opens for your team when a client submits a request or a form arrives.priority, has_due_date, assigned. Follow-up tasks Bynefit opens also carry source (such as engagement, booking or form_submission) and kind (such as order_submitted, client_replied, client_paid or form_submitted).
task.updatedA task's details change.The new status, or the fields that changed
task.assignedA task is assigned or reassigned.from and to
task.completedA task is marked complete.Empty
task.deletedA task is deleted.Empty
form.submittedA form is submitted, from the dashboard or from a hosted site.form_id, form_name, form_type. From a hosted site also source (team_site), site_slug and site_id.
member.joinedA new person joins your space.via and role
member.removedA person is removed from your space.role
donation.receivedA donation is received.amount, total, currency, tx_ref
call.completedA phone-bank call is completed.direction, call_type, duration_sec, status
feed.postedA post is published to your space's feed.tag and has_media
integration.connected, integration.disconnectedA connected service is linked or unlinked.connector, the service's name

There is no event for engagement or Business Setup changes, for partner events, or for site visits. A new engagement request or a client reply shows up as a task.created for your team, without the engagement in it.

Verifying a delivery

Every delivery is signed with your signing secret so you can confirm it came from Bynefit and wasn't modified in transit. The signature is in the X-Bynefit-Signature header as sha256=<hex>, where <hex> is the HMAC-SHA256, keyed with your signing secret, of the timestamp, a literal dot, and the raw request body — {timestamp}.{body}, using the value of X-Bynefit-Timestamp. Compute it over the exact bytes you received, before any JSON parsing.

const crypto = require("crypto");

function verify(rawBody, headers, secret) {
  const pick = (name) => {
    const a = headers["x-bynefit-" + name];
    const b = headers["x-bynli-" + name];
    if (a !== undefined && b !== undefined && a !== b) return "";
    return (a !== undefined ? a : b) || "";
  };
  const ts = pick("timestamp");
  if (!/^\d+$/.test(ts || "") || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  const a = Buffer.from(pick("signature"));
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

module.exports = { verify };

The samples above also reject any delivery whose X-Bynefit-Timestamp is more than five minutes away from your server's clock, which stops an attacker replaying a captured payload later. Keep your server clock synced. Compare signatures in constant time, as the samples do.

A whole receiver

Read the body as raw bytes before anything parses it, check the signature, skip an event you have already handled, and answer quickly. The Node and Python versions use the verifier above, saved next to them as verify.js and verify.py. The in-memory seen set is for the example; use a database or cache that survives a restart.

const express = require("express");
const { verify } = require("./verify");

const app = express();
const seen = new Set();

app.post("/bynefit", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.headers, process.env.BYNEFIT_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(raw);
  if (seen.has(event.id)) return res.sendStatus(200);
  seen.add(event.id);
  console.log(event.type, event.subject);
  res.sendStatus(200);
});

app.listen(3000);

Try your receiver before a real event

Until test events exist (see below), send a delivery to yourself. This signs a made-up form.submitted exactly the way Bynefit does. Run it against a copy of your receiver, and use a secret you made up, not the real one.

cURLbash
URL="https://your-server.example.com/bynefit"
SECRET="whsec_your_signing_secret"

TS=$(date +%s)
BODY='{"id":"evt_test0001","type":"form.submitted","team_id":42,"created_at":"2026-10-10T14:03:22+00:00","actor":{"id":null},"subject":{"type":"form_submission","id":"sub_test0001"},"data":{"form_id":"frm_test0001","form_name":"Contact us","form_type":"standard"}}'
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -i -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-Bynefit-Event: form.submitted" \
  -H "X-Bynefit-Event-Id: evt_test0001" \
  -H "X-Bynefit-Delivery: local-test-1" \
  -H "X-Bynefit-Timestamp: $TS" \
  -H "X-Bynefit-Signature: sha256=$SIG" \
  -d "$BODY"

Responding

Retries and back-off

Deliveries go out about a minute after the event happens. When your server returns a non-2xx or times out, Bynefit tries again after about 1 minute, then 5 minutes, then 30 minutes, then 2 hours. That is five attempts in all, over roughly two and a half hours, and then Bynefit gives up on that event for that endpoint.

Separately, an endpoint is switched off after 20 failed deliveries in a row, and the Space's admins are emailed. A successful delivery resets the count. Turn the endpoint back on from /dash/integrations/webhooks once your server is healthy.

Idempotency. Because retries exist, the same event can arrive more than once. Use X-Bynefit-Event-Id (the same value as the payload's id, constant across retries) to de-duplicate on your side. X-Bynefit-Delivery changes on every attempt, so it is for logging, not de-duplication.

Send a test event

Available On /dash/integrations/webhooks, choose Send test event on an endpoint. Bynefit posts a sample event to that address straight away and shows you the status code your server answered and how long it took.

Replay a past delivery

Available On /dash/integrations/webhooks, open Recent deliveries for an endpoint and choose Replay on a row. Bynefit sends that delivery's stored event to the endpoint's address again and shows the status your server answered.

Managing endpoints

You can pause, resume, rotate the signing secret, and delete endpoints from /dash/integrations/webhooks. Rotating the signing secret invalidates the previous one immediately, so update your server first or accept a short gap. The page also lists the recent deliveries for each endpoint with the status your server returned.