Webhooks
Connect your membership data to other tools with Join It webhooks. Send real-time notifications for membership, payment, renewal, and check-in activity.
Use Join It webhooks to receive an HTTP notification when membership, payment, check-in, or registration data changes in your organization. Webhooks let your application react to changes without repeatedly polling Join It's API.
For each webhook automation, Join It sends an HTTP POST request to the endpoint URL you configure. The request body is JSON and contains the current resource data together with two routing fields:
webhook_object_type: the kind of object in the payload, such asmembershipsorpayments.webhook_event_type: the event subscription that produced the request, such asmemberships.created.
Webhook deliveries can contain personal and financial data. Use an HTTPS endpoint and handle the payload as sensitive data.
Create a webhook
- Open Automations → Webhooks in Join It.
- If this is your first webhook, choose the option to create it. Otherwise, select Create Automation.
- Choose a trigger event.
- Enter the full, publicly reachable HTTPS URL for your receiver.
- Enter an internal name that will help your team identify the destination.
- Select Create Automation.
One automation subscribes one URL to one trigger. To receive several event types, create an automation for each trigger. The automations can all use the same endpoint URL; route each request using webhook_event_type.
Join It cannot send a webhook to localhost or another private address. During local development, expose your receiver through a secure tunnel or use an HTTPS request-inspection endpoint.
Available triggers
| Trigger in Join It | webhook_event_type | webhook_object_type | Payload | When it is sent |
|---|---|---|---|---|
| Memberships All | memberships.all | memberships | Membership | When a membership is created, updated, made inactive, renewed, or has its email changed. The event type remains memberships.all. |
| Memberships Created | memberships.created | memberships | Membership | After Join It creates a membership. |
| Memberships Inactive | memberships.inactive | memberships | Membership | After a membership enters an inactive lifecycle state. |
| Memberships Updated | memberships.updated | memberships | Membership | After supported membership details change. This can also be sent as part of an email change, renewal, or inactive transition. |
| Memberships Renewed | memberships.renewal | memberships | Membership | After Join It completes a membership renewal. Note that the UI says “Renewed,” while the event value is memberships.renewal. |
| Payments Created | payments.created | payments | Payment | After Join It creates a payment record. |
| Check-ins All | check_ins.all | memberships or check_ins | Membership or Check-in | For supported check-in lifecycle events. The event value remains check_ins.all; see the check-in payload note below. |
| Check-ins Created | check_ins.created | memberships | Membership | After a check-in is created. The payload is the related Membership, not the Check-in record. |
| Check-ins Updated | check_ins.updated | check_ins | Check-in | Intended for changes to an existing Check-in record other than check-out. See the availability note below. |
| Check-ins Checked Out | check_ins.checked_out | check_ins | Check-in | After the checked_out value is added to a Check-in record. |
| Event Registrations Created | registrations.created | registrations | Registration | After Join It creates a registration record. |
Memberships Updated
The Memberships Updated trigger is used when supported membership details change, including:
- membership type;
- email address;
- status;
- external membership ID;
- joined or expiration date;
- profile information, such as address, company, job title, phone, date of birth, or gender; and
- custom-field answers.
Because Join It can emit memberships.updated during other lifecycle operations, a receiver should compare the new object with its stored state rather than assume that only one field changed.
Check-in payload behavior
Check-in events do not all have the same payload shape:
check_ins.createdsends the related Membership object. Its_idis the membership ID, not a check-in ID.check_ins.updatedandcheck_ins.checked_outsend the Check-in record.check_ins.allsends the corresponding shape for the underlying event. It can therefore havewebhook_object_type: "memberships"orwebhook_object_type: "check_ins".
When consuming check_ins.all, inspect webhook_object_type before parsing resource-specific fields. The current check_ins.all payload does not separately identify the underlying created, updated, or checked-out action.
Request format
Join It sends requests in this form:
POST /join-it/webhooks HTTP/1.1
Host: example.com
Content-Type: application/json
{ ...resource fields, "webhook_object_type": "memberships", "webhook_event_type": "memberships.updated" }There is no outer envelope. Resource fields and webhook metadata are all at the top level of the JSON object.
Treat all fields other than the two webhook metadata fields as resource data. Fields can be null, optional fields can be absent, and Join It may add fields over time. A resilient receiver should ignore fields it does not recognize.
Membership example
{
"_id": "mbr_8fK2sL4q",
"organization_id": "org_2dP7xQ9n",
"external_id": "MEM-1042",
"email": "[email protected]",
"display_name": "Alex Morgan",
"membership_type": "type_annual",
"membership_name": "Annual Member",
"status": 100,
"joined": "2026-01-15T10:30:00.000Z",
"expiration_date": "2027-01-15T10:30:00.000Z",
"profile": {
"first_name": "Alex",
"last_name": "Morgan",
"company": null,
"phone": "+44 20 7946 0958"
},
"custom_answers": [],
"created": "2026-01-15T10:30:00.000Z",
"updated": "2026-07-22T09:12:45.000Z",
"webhook_object_type": "memberships",
"webhook_event_type": "memberships.updated"
}See the Memberships data model for field definitions and status values.
Payment example
Payment webhooks contain the current Payment object and an additional member_email field resolved from the associated account.
{
"_id": "pay_7cN3vM8r",
"organization_id": "org_2dP7xQ9n",
"member_id": "acct_3yT9pB1k",
"member_email": "[email protected]",
"membership_ids": ["mbr_8fK2sL4q"],
"channel": "stripe",
"status": 100,
"currency": "GBP",
"subtotal": 5000,
"total": 5000,
"payment_date": "2026-07-22T09:12:45.000Z",
"created": "2026-07-22T09:12:45.000Z",
"updated": "2026-07-22T09:12:45.000Z",
"webhook_object_type": "payments",
"webhook_event_type": "payments.created"
}See the Payments data model for the full object definition.
Check-in example
The following shape is used for check-in update and check-out deliveries. A created delivery uses the Membership shape shown above instead.
{
"_id": "checkin_4hR8wS2j",
"organization_id": "org_2dP7xQ9n",
"membership_id": "mbr_8fK2sL4q",
"channel": "kiosk",
"note": null,
"eventlisting_id": "event_6mJ1zA5c",
"checked_in_by_user_id": "user_9qE2tV7b",
"checked_out_by_user_id": "user_9qE2tV7b",
"created": "2026-07-22T09:00:00.000Z",
"checked_out": "2026-07-22T11:15:00.000Z",
"webhook_object_type": "check_ins",
"webhook_event_type": "check_ins.checked_out"
}Registration example
{
"_id": "reg_5vC1nD7p",
"organization_id": "org_2dP7xQ9n",
"app_name": "joinit",
"channel": "portal",
"membership_id": "mbr_8fK2sL4q",
"members_id": "acct_3yT9pB1k",
"registration_type_id": "regtype_8uF4kL2s",
"event_id": "event_6mJ1zA5c",
"email": "[email protected]",
"order_id": "order_1aX6gH3m",
"payment_id": "pay_7cN3vM8r",
"created": "2026-07-22T09:12:45.000Z",
"updated": "2026-07-22T09:12:45.000Z",
"webhook_object_type": "registrations",
"webhook_event_type": "registrations.created"
}Acknowledge a delivery
Return any HTTP 2xx response to acknowledge successful receipt. A response body is not required. For example:
HTTP/1.1 204 No ContentReturn the success response only after your application has durably recorded the event. For longer-running work, save the event to a queue and return promptly, then process it asynchronously.
Keep the configured endpoint URL current and point it directly at the receiver. Avoid redirects, and do not return a non-2xx response after successfully accepting an event.
Retries and duplicate events
Retry behavior currently varies by trigger:
| Trigger family | Delivery attempts |
|---|---|
| Membership events, Payments Created, and Check-ins Created | One attempt |
| Check-ins All, Check-ins Updated, Check-ins Checked Out, and Registrations Created | Up to three total attempts for a network error or non-2xx response |
The retried events use exponential backoff with jitter, beginning at approximately one second.
Do not rely on a retry to recover an event. Join It does not guarantee exactly-once delivery or event ordering, and a retried request can produce duplicates. Make processing idempotent.
For most resource events, a practical deduplication key can include webhook_event_type, _id, and the resource's updated or created timestamp. Do not use that strategy for check_ins.created: its _id identifies the membership, so multiple check-ins for the same membership are not uniquely identified by the current payload.
Secure your receiver
Join It currently sends Content-Type: application/json but does not add a webhook signature, authorization header, delivery ID, or timestamp header.
Until signed requests are available:
- use HTTPS only;
- use a high-entropy, unguessable path for the receiver and treat the full endpoint URL as a secret;
- do not expose the endpoint URL in client-side code or public logs;
- validate
organization_id,webhook_object_type, andwebhook_event_typeagainst the values your integration expects; - apply request-size limits and rate limiting at your edge;
- process the minimum personal data needed for the integration; and
- protect stored payloads and redact personal data from logs.
Because requests are not cryptographically signed, payload fields alone cannot prove that a request came from Join It. Do not use a webhook as the sole authorization for a high-risk action. When required, retrieve the referenced resource from Join It's authenticated API before taking that action.
Receiver example
This Express example validates the basic routing fields, records a deduplication key, and acknowledges the request before starting longer work. Replace the in-memory processedEvents set with durable storage in production.
import express from 'express';
const app = express();
const processedEvents = new Set();
app.use(express.json({ limit: '1mb' }));
app.post(
'/join-it/webhooks/a-long-random-secret',
function handleJoinItWebhook(req, res) {
const payload = req.body;
const eventType = payload?.webhook_event_type;
const objectType = payload?.webhook_object_type;
if (!eventType || !objectType || !payload?._id) {
return res.status(400).json({ message: 'Invalid webhook payload' });
}
if (payload.organization_id !== process.env.JOIN_IT_ORGANIZATION_ID) {
return res.status(403).json({ message: 'Unexpected organization' });
}
const resourceTimestamp = payload.updated || payload.created || 'unknown';
const deduplicationKey = `${eventType}:${payload._id}:${resourceTimestamp}`;
if (processedEvents.has(deduplicationKey)) {
return res.sendStatus(204);
}
processedEvents.add(deduplicationKey);
res.sendStatus(204);
// Queue resource-specific work here. Route on eventType and objectType.
}
);
app.listen(3000);The sample deduplication key is not sufficient for check_ins.created because that payload does not currently contain a check-in ID.
Test a webhook
- Configure an HTTPS request-inspection endpoint or a receiver in a non-production environment.
- Create a webhook automation that points to it.
- Perform the selected action in a test organization.
- Confirm that your receiver sees an HTTP
POSTwithContent-Type: application/json. - Confirm the exact
webhook_event_type,webhook_object_type, nullable fields, and resource shape. - Return a
2xxresponse and verify that your handler records the event only once. - Where retries apply, return a temporary non-
2xxresponse and confirm that duplicate attempts are safe.
For membership webhooks, a membership record's automation section includes Re-send this Webhook. The current membership resend tool sends the record as memberships.created, regardless of the original automation trigger. Payment records offer a similar resend action for payments.created.
There is not currently an equivalent record-level resend tool for check-in or registration webhooks. Test those events by performing the corresponding action in a test organization.
See Testing Webhooks for the current Join It walkthrough.
Troubleshooting
No request arrives
- Confirm the automation is active and belongs to the organization where you performed the action.
- Confirm that the selected trigger matches the action. A Memberships Created subscription will not receive Payments Created events.
- Confirm that the endpoint is public and uses a valid HTTPS certificate.
- Check firewalls, allowlists, reverse proxies, and request-size limits.
- Remember that Check-ins Updated is not currently emitted by normal check-in updates.
Join It receives a non-2xx response
2xx response- Check your receiver logs for the response status and application error.
- Ensure the route accepts
POSTrequests and JSON bodies. - Return a
2xxonly after the payload has been durably recorded. - Keep error responses concise; the response body is not used as an application-level reply.
The payload does not match the expected model
- Route on both
webhook_event_typeandwebhook_object_type. - For
check_ins.created, expect a Membership object. - For
check_ins.all, accept either a Membership or Check-in object. - Treat optional fields as nullable or absent.
- Ignore new fields so additive schema changes do not break processing.
The same event is processed more than once
Webhook consumers must be idempotent. Store a durable deduplication record before applying side effects. Also check whether several Join It automations point the same trigger at the same endpoint.
Updated 6 days ago
