Build a flow

Eventlayer / Build a flow

Sync attendees to your CRM

Sync registrations and attendance to your CRM using signed webhooks and your own integration endpoint.

What you will build

Keep a contact's event history alongside the rest of your customer relationship. Eventlayer sends ticket activity to your backend; your integration translates it into your CRM's contacts and event-participation records. There is no built-in CRM connector to enable.

The flow you will build
  1. 01Eventlayer webhook
  2. 02Verified inbox
  3. 03Current attendee
  4. 04Your CRM

Prerequisites

  • A test event, attendee, and ticket, plus a backend with a public HTTPS endpoint.
  • Your CRM's API credentials and a durable queue or database for webhook jobs.
  • An Eventlayer API key to fetch the current ticket and attendee records.

1. Decide what to store in your CRM

Create an event-participation record linked to a contact, rather than a single global "attended" flag. One contact can attend multiple events.

CRM fieldValue
External participation keyEventlayer ticket ID
Contact linkYour saved contact ID, or a contact matched by verified business rules
Event referenceEvent ID resolved from the ticket type
Registration stateCurrent ticket status
Attendance timeTicket's checked_in_at

Store the Eventlayer attendee ID to CRM contact ID mapping in your database. Attendees belong to an event; don't assume one attendee ID identifies the same person across every event. Email matching is a fallback, not a universal identity rule.

2. Register the webhook endpoint

In Dashboard → Webhooks → New endpoint, enter your backend's HTTPS URL, such as https://your-site.example/api/eventlayer-webhooks. Subscribe to:

  • ticket.created — includes tickets created already assigned.
  • ticket.assigned — covers tickets assigned later.
  • ticket.updated, ticket.checked_in, and ticket.deleted — maintain the record.

Save the signing secret immediately in your backend environment. API keys are for reading business resources, not managing webhook endpoints. For local testing, expose only your test receiver through an HTTPS tunnel you control; Eventlayer cannot deliver to localhost on your computer.

3. Verify before accepting a delivery

Read the raw request bytes before JSON parsing. Here is a Node.js verifier you can use in your receiver. Supply the current secret and, during rotation, any still-valid previous secret from your secure configuration.

crm.ts · server only
import { createHmac, timingSafeEqual } from "node:crypto";

export function validSignature(
  rawBody: Uint8Array,
  headers: Headers,
  secrets: string[],
  nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
  const timestamp = headers.get("Webhook-Timestamp") ?? "";
  if (!/^\d+$/.test(timestamp)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(nowSeconds - seconds) > 300) {
    return false;
  }
  const signatures = (headers.get("Webhook-Signature") ?? "")
    .split(",")
    .map((part) => part.trim())
    .filter((part) => /^v1=[0-9a-f]{64}$/i.test(part))
    .map((part) => Buffer.from(part.slice(3), "hex"));
  return secrets.filter(Boolean).some((secret) => {
    const expected = createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest();
    return signatures.some((signature) => timingSafeEqual(signature, expected));
  });
}

Reject an invalid signature with 401. For valid requests, parse and validate the payload's event type and resource ID, then atomically insert a queued job with a unique Webhook-Delivery-Id. Require that header; the Idempotency-Key header has the same value. Apply a request-size limit at your receiver.

Return 2xx only after durable acceptance, or for an already-stored delivery. Return 5xx if storage is unavailable so Eventlayer can retry. Do not start an untracked background promise and acknowledge before it has been persisted.

4. Resolve current records and update your CRM

In your job worker, use this concrete SDK helper to prepare the CRM fields. The actual CRM API call is provider-specific and is yours to implement:

crm.ts · server only
import { Eventlayer } from "eventlayer";

const eventlayer = new Eventlayer();

export async function crmParticipation(ticketId: string) {
  const ticket = await eventlayer.tickets.get({ ticketId: ticketId });
  if (ticket.error) throw ticket.error;
  if (!ticket.data.attendee_id) return null;

  const attendee = await eventlayer.attendees.get({
    attendeeId: ticket.data.attendee_id,
  });
  if (attendee.error) throw attendee.error;
  const ticketType = await eventlayer.ticketTypes.get({
    ticketTypeId: ticket.data.ticket_type_id,
  });
  if (ticketType.error) throw ticketType.error;

  return {
    externalId: ticket.data.id,
    eventId: ticketType.data.event_id,
    attendeeId: attendee.data.id,
    email: attendee.data.email,
    name: attendee.data.name,
    status: ticket.data.status,
    checkedInAt: ticket.data.checked_in_at,
  };
}

For create/assign/update/check-in jobs, call this helper, then upsert the CRM participation by externalId. Resolve or create the contact using your mapping; handle missing email without inventing an address. On reassignment, relink the participation to the new contact instead of leaving it on the previous contact.

For ticket.deleted, use the saved ticket mapping to mark participation as revoked; do not require a successful GET of a resource that may no longer exist. If an older job now finds a missing ticket, consult the saved deletion state.

Serialize jobs per ticket and fetch current state when processing so delayed registration notifications do not overwrite attendance. Retries and API replays can have different delivery IDs; the stable ticket-based upsert must still be safe. Mark a job complete only after the CRM operation succeeds, and retry CRM failures from your durable queue.

Test and recover

  1. Create a ticket already assigned to a test attendee. Confirm ticket.created produces one CRM participation record.
  2. Check in that ticket and verify its attendance time appears in the CRM.
  3. Submit the same valid delivery twice; only one job should be accepted.
  4. Process a delayed registration job after check-in; attendance must remain intact.
  5. Alter one byte of a signed payload; the receiver must reject it.
  6. Make the CRM unavailable, then restore it. Your queued job should recover without creating a second contact or participation record.

Inspect Dashboard → Webhooks → Delivery log for delivery failures. See the webhook reference for signing, rotation, retries, and API replay.

Next steps

React to ticket changes or return to all guides.