Skip to content

API reference

The Webinly REST API lets your own code and the tools you already use talk to your account. Pull registrations into a CRM, register people from your own form, read attendance and analytics, manage bookings and deals, enroll students, and get a signed webhook the moment something happens.

Base URL: https://app.webinly.com/api/v1Version: v1

The API and webhooks are included on the Scale and Agency plans. Create a key in your dashboard to get started.

Overview

Every endpoint lives under https://app.webinly.com/api/v1, speaks JSON, and is scoped to the account that owns the key. A request never sees another account's data. All write bodies are JSON; send Content-Type: application/json.

Authentication

Authenticate every request with a secret key in the Authorization header as a bearer token. Keys start with wbl_.

curl https://app.webinly.com/api/v1/webinars \
  -H "Authorization: Bearer wbl_your_secret_key"
  • Create and revoke keys in Settings › API. The secret is shown once, when you create it. Store it somewhere safe; if you lose it, delete the key and make a new one.
  • Each key carries only the scopes you tick. A call that needs a scope the key does not have returns 403. To change a key's scopes, make a new key.
  • Keys can expire. Pick 30 days, 90 days, 1 year, or never when you create one. An expired key is rejected like an unknown one.
  • Keep keys server-side. A key is a password for your whole account's data in its scopes. Never ship one in a browser, a mobile app, or a public repository.

Scopes

A scope is one permission. Tick exactly the ones a key needs.

ScopeGrants
read:webinarsList the webinars on your account.
read:registrationsRead a webinar's registrations and attendance.
read:analyticsRead a webinar's roll-up analytics.
write:registrationsRegister someone for a webinar from your own code.
read:schedulingRead bookable event types, open slots, and bookings.
write:schedulingCreate and cancel bookings.
read:contactsRead your CRM contacts.
read:dealsRead your CRM deals and pipelines.
read:coursesList the courses on your account.
write:coursesGive a person access to a course.
read:webhooksList your webhook endpoints.
write:webhooksCreate and delete webhook endpoints.

Keys created before scopes existed carry a legacy read:* wildcard. It opens only read:webinars, read:registrations and read:analytics; anything newer needs its own box on a new key.

Rate limits

Requests are limited on two dimensions:

120 / minute / IP
Across every key calling from one address.
1000 / minute / key
Per API key, on top of the per-IP budget.

Over the limit returns 429 with a Retry-After header (seconds until capacity frees up). Back off and retry after that many seconds.

Errors

Errors use standard HTTP status codes and a JSON body with an error string.

200 / 201
Success. 201 when a new resource was created.
400
Bad request: invalid JSON, a missing field, or a malformed value.
401
Missing or invalid key.
403
Valid key, but the plan does not include the API or the key lacks the scope.
404
No such resource on your account.
409
A conflict, such as a booking that is already cancelled.
429
Rate limited. See Retry-After.
{
  "error": "Forbidden: key lacks read:contacts scope"
}

Pagination

List endpoints that can grow large use cursor pagination. Pass limit for the page size and follow the cursor the response returns until it is null. A cursor is opaque: pass back exactly what you got, do not build one yourself.

GET /contacts, /deals
limit (1-200, default 50). Response carries next_cursor; pass it as ?cursor= for the next page.
GET /webinars/{slug}/registrations
limit (1-500, default 100). Response carries nextCursor; pass it as ?cursor=.
GET /scheduling/bookings
limit (1-200, default 50) with a numeric offset.

Webinars

GET/webinarsread:webinars

List every webinar on your account, newest first.

curl https://app.webinly.com/api/v1/webinars \
  -H "Authorization: Bearer wbl_..."

{
  "webinars": [
    {
      "id": "0f3d...",
      "slug": "launch-masterclass",
      "title": "Launch Masterclass",
      "name": "Launch Masterclass",
      "description": "...",
      "status": "published",
      "scheduledAt": "2026-11-02T17:00:00.000Z",
      "timezone": "America/New_York",
      "durationSeconds": 3600,
      "createdAt": "2026-10-01T09:12:00.000Z"
    }
  ]
}
GET/webinars/{slug}/registrationsread:registrations

List a webinar's registrations with attendance metrics. Cursor paginated.

limit
1-500, default 100.
cursor
The nextCursor from the previous page.
includeTest
Set to 1 to include host test registrations (excluded by default).
curl "https://app.webinly.com/api/v1/webinars/launch-masterclass/registrations?limit=100" \
  -H "Authorization: Bearer wbl_..."

{
  "registrations": [
    {
      "id": "a1b2...",
      "name": "Sara Lee",
      "email": "sara@example.com",
      "registeredAt": "2026-10-10T14:03:00.000Z",
      "sessionAt": null,
      "utm": { "source": "newsletter" },
      "attended": true,
      "attendedAt": "2026-11-02T17:01:10.000Z",
      "watchPct": 82,
      "completed": false
    }
  ],
  "nextCursor": "eyJyZWdpc3RlcmVkQXQiOiIuLi4ifQ",
  "limit": 100
}
POST/webinars/{slug}/registrationswrite:registrations

Register someone from your own code. The response returns a signed watch link you can email them. The same person posted twice gets the same link back (idempotent per email), with alreadyRegistered: true.

name
Required. 1-200 characters.
email
Required. A valid address.
utm
Optional. { source, medium, campaign, term, content }.
referrerRid
Optional. The registration id that referred them.
curl -X POST https://app.webinly.com/api/v1/webinars/launch-masterclass/registrations \
  -H "Authorization: Bearer wbl_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "Sara Lee", "email": "sara@example.com" }'

{
  "registration": {
    "id": "a1b2...",
    "name": "Sara Lee",
    "email": "sara@example.com",
    "sessionAt": null,
    "registeredAt": "2026-10-10T14:03:00.000Z",
    "utm": null,
    "referrerRegistrationId": null
  },
  "watchUrl": "https://app.webinly.com/w/launch-masterclass/registered?rid=a1b2...&sig=...",
  "signature": "...",
  "alreadyRegistered": false
}
GET/webinars/{slug}/analyticsread:analytics

The same roll-ups your results page shows, in one JSON shape.

curl https://app.webinly.com/api/v1/webinars/launch-masterclass/analytics \
  -H "Authorization: Bearer wbl_..."

{
  "webinar": { "id": "0f3d...", "slug": "launch-masterclass", "title": "Launch Masterclass" },
  "totals": {
    "registrations": 1240,
    "attended": 712,
    "noShow": 528,
    "completed": 401,
    "offerClicks": 96,
    "offerLinkPeople": 71,
    "reportedSales": 18,
    "attendanceRate": 57.4,
    "completionRate": 56.3,
    "avgWatchPct": 64.2
  },
  "utmSources": [ { "source": "newsletter", "n": 410 } ],
  "referralLeaderboard": [ { "referrerId": "a1b2...", "n": 22 } ]
}

Scheduling

Read what is bookable, read open slots, and create or cancel bookings. Every slot the API offers is one the booking page would offer: the same hours, buffers, caps, min-notice and calendar busy rules apply. The API never charges a card; a paid event returns a hosted checkout URL for the attendee to pay.

GET/scheduling/event-typesread:scheduling

List your active, public event types. Secret event types are omitted.

curl https://app.webinly.com/api/v1/scheduling/event-types \
  -H "Authorization: Bearer wbl_..."

{
  "event_types": [
    {
      "id": "e7c0...",
      "slug": "intro-call",
      "name": "Intro call",
      "duration_minutes": 30,
      "timezone": "Europe/London",
      "location_kind": "google_meet",
      "deposit_cents": 0,
      "requires_approval": false
    }
  ]
}
GET/scheduling/event-types/{id}/slotsread:scheduling

Open times for one event type in a date range.

from, to
Required. YYYY-MM-DD.
tz
Optional IANA timezone. Defaults to the event type's.
duration
Optional minutes, for events that offer a choice.
curl "https://app.webinly.com/api/v1/scheduling/event-types/e7c0.../slots?from=2026-11-01&to=2026-11-07&tz=Europe/London" \
  -H "Authorization: Bearer wbl_..."

{
  "timezone": "Europe/London",
  "slots": [
    {
      "start_utc": "2026-11-03T09:00:00.000Z",
      "end_utc": "2026-11-03T09:30:00.000Z",
      "host_account_id": "h1...",
      "seats_remaining": 1,
      "seat_capacity": 1
    }
  ],
  "days_with_slots": ["2026-11-03", "2026-11-04"]
}
GET/scheduling/bookingsread:scheduling

List bookings on your account, newest first.

status
Filter by lifecycle state, e.g. cancelled.
from, to
Filter by slot time (ISO).
limit, offset
1-200, default 50, with a numeric offset.
POST/scheduling/bookingswrite:scheduling

Create a booking on the attendee's behalf. Use a start_utc from the slots endpoint. A paid event responds payment_required with a checkout URL; an approval event responds pending_approval.

event_type_id
Required. UUID.
slot_start_utc
Required. The slot's start_utc.
attendee_name, attendee_email
Required.
attendee_timezone
Optional IANA timezone.
duration_minutes
Optional, for variable-length events.
host_account_id
Optional. Pin a round-robin host (from the slot).
answers
Optional map of question answers.
repeat_weeks
Optional 1-12 to book a weekly series.
curl -X POST https://app.webinly.com/api/v1/scheduling/bookings \
  -H "Authorization: Bearer wbl_..." \
  -H "Content-Type: application/json" \
  -d '{
    "event_type_id": "e7c0...",
    "slot_start_utc": "2026-11-03T09:00:00.000Z",
    "attendee_name": "Sara Lee",
    "attendee_email": "sara@example.com"
  }'

{ "status": "booked", "booking_id": "bk_...", "recurrence": null }
POST/scheduling/bookings/{id}/cancelwrite:scheduling

Cancel a booking. This runs the same refund, calendar cleanup and cancellation email the dashboard does. Set scope: "following" to also cancel later weeks of a series.

curl -X POST https://app.webinly.com/api/v1/scheduling/bookings/bk_.../cancel \
  -H "Authorization: Bearer wbl_..." \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer requested" }'

{ "status": "cancelled", "cancelled_count": 1, "deposit_refund": null }

Contacts and deals

GET/contactsread:contacts

Read your CRM contacts. Cursor paginated.

q
Search name or email.
stage
Filter by lifecycle stage.
tag
Filter by a tag.
limit
1-200, default 50.
cursor
The next_cursor from the previous page.
curl "https://app.webinly.com/api/v1/contacts?limit=50" \
  -H "Authorization: Bearer wbl_..."

{
  "contacts": [
    {
      "id": "c1...",
      "email": "sara@example.com",
      "name": "Sara Lee",
      "first_name": "Sara",
      "last_name": "Lee",
      "phone": "+15551234567",
      "lifecycle_stage": "lead",
      "tags": ["webinar"],
      "created_at": "2026-10-10T14:03:00.000Z"
    }
  ],
  "next_cursor": "2026-10-10T14:03:00.000Z|c1..."
}
GET/dealsread:deals

Read your pipeline. Cancelled deals are excluded by default. Cursor paginated.

status
open, won, or lost.
pipeline_id
Filter to one pipeline (UUID).
limit
1-200, default 50.
cursor
The next_cursor from the previous page.
curl "https://app.webinly.com/api/v1/deals?status=open&limit=50" \
  -H "Authorization: Bearer wbl_..."

{
  "deals": [
    {
      "id": "d1...",
      "title": "Acme - annual",
      "contact_name": "Sara Lee",
      "contact_email": "sara@example.com",
      "value_cents": 120000,
      "currency": "usd",
      "stage_name": "Proposal",
      "pipeline_name": "Sales",
      "status": "open",
      "created_at": "2026-10-10T14:03:00.000Z"
    }
  ],
  "next_cursor": "2026-10-10T14:03:00.000Z|d1..."
}

Courses

GET/coursesread:courses

List your courses with how many students can open each one. Archived courses are omitted.

curl https://app.webinly.com/api/v1/courses \
  -H "Authorization: Bearer wbl_..."

{
  "courses": [
    {
      "id": "co_...",
      "slug": "launch-course",
      "title": "Launch Course",
      "status": "published",
      "billingType": "one_time",
      "priceCents": 29900,
      "currency": "usd",
      "students": 142,
      "createdAt": "2026-09-01T00:00:00.000Z"
    }
  ]
}
POST/courses/{id}/enrollwrite:courses

Give one person access to a course from another system. Free for the student. Provide an email or a contactId. Someone the host removed is not put back (409).

email
The student's email (or use contactId).
contactId
An existing contact's UUID.
name
Optional display name.
sendEmail
Set false to skip the sign-in email.
curl -X POST https://app.webinly.com/api/v1/courses/co_.../enroll \
  -H "Authorization: Bearer wbl_..." \
  -H "Content-Type: application/json" \
  -d '{ "email": "sara@example.com", "name": "Sara Lee" }'

{
  "enrolled": true,
  "alreadyHad": false,
  "reactivated": false,
  "emailed": true,
  "enrollmentId": "en_..."
}

Webhook endpoints

Manage the URLs Webinly posts events to. You can also add these on the Integrations page; the API is for tools that subscribe on their own, such as a Zapier or n8n trigger.

GET/hooksread:webhooks

List your webhook endpoints. The secret is never returned here.

POST/hookswrite:webhooks

Create an endpoint. The signing secret is returned once, on create. The URL must be https and a public host. An empty events list means every event.

url
Required. An https URL, max 500 characters.
events
Optional array of event ids, max 10. Empty = all.
label
Optional name, max 80 characters.
curl -X POST https://app.webinly.com/api/v1/hooks \
  -H "Authorization: Bearer wbl_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/webinly", "events": ["registration.created"] }'

{
  "hook": {
    "id": "wh_...",
    "url": "https://example.com/webinly",
    "label": null,
    "events": ["registration.created"],
    "active": true,
    "createdAt": "2026-10-15T00:00:00.000Z",
    "secret": "whsec_..."
  }
}
DELETE/hooks/{id}write:webhooks

Remove an endpoint so Webinly stops posting to it.

curl -X DELETE https://app.webinly.com/api/v1/hooks/wh_... \
  -H "Authorization: Bearer wbl_..."

{ "ok": true, "id": "wh_..." }

Receiving webhooks

When an event fires, Webinly sends a POST to each matching endpoint with this JSON body:

{
  "event": "registration.created",
  "created_at": 1760000000,
  "data": {
    "registration_id": "a1b2...",
    "email": "sara@example.com",
    "name": "Sara Lee",
    "webinar_id": "0f3d...",
    "webinar_slug": "launch-masterclass",
    "webinar_title": "Launch Masterclass",
    "registered_at": "2026-10-10T14:03:00.000Z",
    "source": "api"
  }
}

Events

EventFires when
registration.createdSomeone registered for a webinar.
payment.completedA purchase or payment completed (a course sale too).
payment.refundedA refund was issued.
booking.createdA new booking was made.
booking.cancelledA booking was cancelled.
booking.rescheduledA booking moved to a new time.
booking.completedA booking was marked completed.
deal.wonA deal was marked won.
deal.lostA deal was marked lost.
deal.stage_changedA deal moved to a new stage.
course.enrolledA student joined a course.
course.completedA student finished a course.
course.access_removedA student lost access to a course.

Verifying the signature

Every delivery carries two headers: webinly-event (the event id) and webinly-signature. The signature is Stripe-style: t=<unix>,v1=<hmac>, where the HMAC is HMAC-SHA256(secret, t + "." + rawBody) as lowercase hex. The timestamp is folded into the signed bytes so a captured payload cannot be replayed under a new timestamp. Verify against the raw request body, before any JSON parse.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const expected = createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Reject anything that does not verify, and optionally reject a t more than a few minutes old. Respond 2xx quickly; do the real work after you answer.

Need a hand?

Questions about the API, a scope you need, or an event that is not listed yet? Email contact@webinly.com or head back to the docs home.