Skip to content

Winglark

Developer reference

Winglark API and webhooks

Read events, guests, replies and arrivals; add guests; and be told the moment something happens. A REST API with JSON, scoped keys and signed webhooks.

The API and webhooks are part of the Professional and Agency plans. Keys and webhooks are created by the workspace owner or a manager, under Organisation → Integrations.

Overview

The base address is https://winglark.com/api/v1. Every request and answer is JSON over HTTPS. Dates are ISO-8601 in UTC; ids are UUIDs.

The API is for servers. It sends no CORS headers and ignores cookies: call it from your back end, never from a browser, and keep the key out of client code.

Authentication

Send the key in the Authorization header as a bearer token. A key is shown once, when it is created; Winglark keeps only a keyed hash of it.

A key acts as the member who created it, limited to its scopes. It is refused if it is revoked or expired, if that member has left the workspace or can no longer manage integrations, or if the workspace’s plan no longer includes the API.

curl https://winglark.com/api/v1/events \
  -H "Authorization: Bearer wlk_live_ab12cd34_…"

Scopes

Each route needs one scope. A key with guests:write may also read guests. Contact details (e-mail, phone) are masked unless the key was created with contact details visible, by a member who may see them.

ScopeMeaning
events:readRead events. Titles, dates, places and status.
guests:readRead guests. The guest list of every event.
guests:writeAdd and edit guests. Through the same rules as the panel: the plan limit and the duplicate check apply.
rsvps:readRead replies. Who answered, how many are coming. Never dietary, allergy or accessibility answers.
checkins:readRead arrivals. Who came in, when, and through which door.
webhooks:manageManage webhooks. Lets an automation tool subscribe and unsubscribe on its own.

Errors

Errors use the HTTP status and a JSON body with a stable code and a sentence for people. Every answer carries X-Request-Id; quote it when you write to us.

400
A parameter is not valid.
401
No key, an unknown key, or a revoked or expired one.
403
The key lacks the scope, its creator lost access, or the plan does not include the API.
404
No such record in this workspace.
409
The request conflicts with the record: a duplicate address, the plan’s guest limit, a closed event.
422
Fields are missing or not valid.
429
Too many requests; wait for Retry-After.
500
Our fault. Retry with the same Idempotency-Key.
HTTP/1.1 409 Conflict
X-Request-Id: req_4f1e…

{ "error": { "code": "guest_limit_reached", "message": "This event has reached the guest limit of the workspace plan." } }

Pagination

Lists return the newest first, up to limit items (default 50, at most 100). When has_more is true, pass next_cursor as cursor to get the next page. Guest and reply lists also accept updated_since, an ISO date.

Rate limits

600 requests a minute per key and 1,200 per network. Past the limit the answer is 429 with Retry-After in seconds. Repeated requests with an invalid key are refused for ten minutes.

Idempotency

Send an Idempotency-Key header with a create. For 24 hours, a retry with the same key and the same body returns the first answer (marked Idempotent-Replayed) instead of creating a second guest. The same key with a different body is refused with 422.

curl -X POST https://winglark.com/api/v1/events/{event_id}/guests \
  -H "Authorization: Bearer wlk_live_ab12cd34_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-10442-create" \
  -d '{ "first_name": "Ada", "email": "ada@example.com" }'

Endpoints

Every list route takes limit and cursor.

GET/v1/me

The key itself: its scopes and workspace. Use it to test a key.

Scope: —

{
  "object": "api_key",
  "id": "3b2a1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9",
  "name": "CRM sync",
  "prefix": "wlk_live_ab12cd34",
  "scopes": ["events:read", "guests:read", "rsvps:read"],
  "contacts_visible": false,
  "created_at": "2026-11-04T10:00:00.000Z",
  "expires_at": null,
  "organization": { "id": "9d8c7b6a-5f4e-4d3c-a2b1-0f9e8d7c6b5a", "name": "Northwind Events" }
}

GET/v1/events

The workspace’s events, newest first. Filter with status.

Scope: events:read · ?status=active&limit=20

{
  "object": "list",
  "data": [
    {
      "id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
      "object": "event",
      "title": "Spring partner summit",
      "summary": "",
      "status": "active",
      "type": "corporate",
      "starts_at": "2026-12-03T09:00:00.000Z",
      "ends_at": "2026-12-03T17:00:00.000Z",
      "timezone": "Europe/Berlin",
      "locale": "en",
      "location_mode": "in_person",
      "venue_name": "Hall 4",
      "venue_address": "Messedamm 22, Berlin",
      "rsvp_deadline": "2026-11-26T23:00:00.000Z",
      "created_at": "2026-10-20T08:12:44.120Z",
      "updated_at": "2026-11-02T15:03:10.004Z"
    }
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0xMS0wNFQxMDoyMTowMy41NTFafGI4ZTFkM2M0LTlmMjAtNGE3Yi04YzYxLTVkMmUwZjlhM2IxNw"
}

GET/v1/events/{event_id}

One event.

Scope: events:read

{
  "id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
  "object": "event",
  "title": "Spring partner summit",
  "summary": "",
  "status": "active",
  "type": "corporate",
  "starts_at": "2026-12-03T09:00:00.000Z",
  "ends_at": "2026-12-03T17:00:00.000Z",
  "timezone": "Europe/Berlin",
  "locale": "en",
  "location_mode": "in_person",
  "venue_name": "Hall 4",
  "venue_address": "Messedamm 22, Berlin",
  "rsvp_deadline": "2026-11-26T23:00:00.000Z",
  "created_at": "2026-10-20T08:12:44.120Z",
  "updated_at": "2026-11-02T15:03:10.004Z"
}

GET/v1/events/{event_id}/guests

An event’s guests. include_archived=true adds guests taken off the list.

Scope: guests:read · ?updated_since=2026-11-01T00:00:00Z

{
  "object": "list",
  "data": [
    {
      "id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
      "object": "guest",
      "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
      "first_name": "Ada",
      "last_name": "Kaya",
      "display_name": "Ada Kaya",
      "email": "a•••@example.com",
      "phone": "••• ••• 321",
      "company": "Northwind",
      "job_title": "Head of Partnerships",
      "locale": "en",
      "guest_type": "standard",
      "status": "pending",
      "source": "api",
      "tags": ["partner"],
      "external_reference": "crm-10442",
      "expected_party_size": 1,
      "archived_at": null,
      "created_at": "2026-11-04T10:21:03.551Z",
      "updated_at": "2026-11-04T10:21:03.551Z"
    }
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0xMS0wNFQxMDoyMTowMy41NTFafGI4ZTFkM2M0LTlmMjAtNGE3Yi04YzYxLTVkMmUwZjlhM2IxNw"
}

GET/v1/events/{event_id}/guests/{guest_id}

One guest.

Scope: guests:read

{
  "id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
  "object": "guest",
  "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
  "first_name": "Ada",
  "last_name": "Kaya",
  "display_name": "Ada Kaya",
  "email": "a•••@example.com",
  "phone": "••• ••• 321",
  "company": "Northwind",
  "job_title": "Head of Partnerships",
  "locale": "en",
  "guest_type": "standard",
  "status": "pending",
  "source": "api",
  "tags": ["partner"],
  "external_reference": "crm-10442",
  "expected_party_size": 1,
  "archived_at": null,
  "created_at": "2026-11-04T10:21:03.551Z",
  "updated_at": "2026-11-04T10:21:03.551Z"
}

POST/v1/events/{event_id}/guests

Add a guest. Only first_name is required. Plan limits and the duplicate-address rule apply.

Scope: guests:write

{
  "first_name": "Ada",
  "last_name": "Kaya",
  "email": "ada@example.com",
  "company": "Northwind",
  "tags": ["partner"],
  "external_reference": "crm-10442"
}
{
  "id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
  "object": "guest",
  "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
  "first_name": "Ada",
  "last_name": "Kaya",
  "display_name": "Ada Kaya",
  "email": "a•••@example.com",
  "phone": "••• ••• 321",
  "company": "Northwind",
  "job_title": "Head of Partnerships",
  "locale": "en",
  "guest_type": "standard",
  "status": "pending",
  "source": "api",
  "tags": ["partner"],
  "external_reference": "crm-10442",
  "expected_party_size": 1,
  "archived_at": null,
  "created_at": "2026-11-04T10:21:03.551Z",
  "updated_at": "2026-11-04T10:21:03.551Z"
}

PATCH/v1/events/{event_id}/guests/{guest_id}

Change a guest’s name, contact details, company, job title, language, type or tags.

Scope: guests:write

{ "job_title": "Chief Partnerships Officer", "tags": ["partner", "vip-desk"] }
{
  "id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
  "object": "guest",
  "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
  "first_name": "Ada",
  "last_name": "Kaya",
  "display_name": "Ada Kaya",
  "email": "a•••@example.com",
  "phone": "••• ••• 321",
  "company": "Northwind",
  "job_title": "Head of Partnerships",
  "locale": "en",
  "guest_type": "standard",
  "status": "pending",
  "source": "api",
  "tags": ["partner"],
  "external_reference": "crm-10442",
  "expected_party_size": 1,
  "archived_at": null,
  "created_at": "2026-11-04T10:21:03.551Z",
  "updated_at": "2026-11-04T10:21:03.551Z"
}

GET/v1/events/{event_id}/rsvps

An event’s replies, most recently changed first. Filter with response.

Scope: rsvps:read · ?response=yes

{
  "object": "list",
  "data": [
    {
      "id": "0c9b7a5e-3d1f-4e2a-b6c8-9f0e1d2c3b4a",
      "object": "rsvp",
      "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
      "guest_id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
      "response": "yes",
      "party_size": 2,
      "attendance_mode": "in_person",
      "revision": 1,
      "responded_via": "link",
      "responded_at": "2026-11-05T18:40:12.000Z",
      "updated_at": "2026-11-05T18:40:12.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0xMS0wNFQxMDoyMTowMy41NTFafGI4ZTFkM2M0LTlmMjAtNGE3Yi04YzYxLTVkMmUwZjlhM2IxNw"
}

GET/v1/events/{event_id}/checkins

An event’s door records, newest first.

Scope: checkins:read

{
  "object": "list",
  "data": [
    {
      "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
      "object": "checkin",
      "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
      "guest_id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
      "action": "admitted",
      "result": "ok",
      "admitted": 2,
      "occurred_at": "2026-12-03T08:57:31.000Z",
      "recorded_offline": false
    }
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0xMS0wNFQxMDoyMTowMy41NTFafGI4ZTFkM2M0LTlmMjAtNGE3Yi04YzYxLTVkMmUwZjlhM2IxNw"
}

GET/v1/webhooks

The workspace’s webhook endpoints.

Scope: webhooks:manage

{ "object": "list", "data": [{
    "id": "e2d4c6b8-a0f1-4e3d-9c5b-7a8f6e4d2c1b",
    "object": "webhook_endpoint",
    "url": "https://hooks.example.com/winglark",
    "description": "CRM sync",
    "events": ["rsvp.submitted", "checkin.admitted"],
    "event_ids": [],
    "include_contacts": false,
    "active": true,
    "disabled_reason": null,
    "source": "api",
    "created_at": "2026-11-04T10:30:00.000Z"
  }], "has_more": false, "next_cursor": null }

POST/v1/webhooks

Subscribe an address. The answer includes the signing secret, once.

Scope: webhooks:manage

{
  "url": "https://hooks.example.com/winglark",
  "events": ["rsvp.submitted", "checkin.admitted"],
  "event_ids": [],
  "description": "CRM sync"
}
{
  "id": "e2d4c6b8-a0f1-4e3d-9c5b-7a8f6e4d2c1b",
  "object": "webhook_endpoint",
  "url": "https://hooks.example.com/winglark",
  "description": "CRM sync",
  "events": ["rsvp.submitted", "checkin.admitted"],
  "event_ids": [],
  "include_contacts": false,
  "active": true,
  "disabled_reason": null,
  "source": "api",
  "created_at": "2026-11-04T10:30:00.000Z",
  "secret": "whsec_6Jx…shown once…"
}

DELETE/v1/webhooks/{webhook_id}

Unsubscribe.

Scope: webhooks:manage

204 No Content

Webhooks

Add an endpoint in the panel or with POST /v1/webhooks, choose its event types and, if you like, limit it to some events. Winglark then posts one JSON message per happening.

Answer with any 2xx within 10 seconds. Redirects are not followed. The id in the message is the same on every retry and for every endpoint: use it to ignore a message you have already handled.

Event typeWhen
guest.createdA guest was added, by hand, by import, through the API or at the door.
guest.updatedA guest’s details changed.
guest.archivedA guest was taken off the list.
rsvp.submittedA guest replied or changed their reply.
invitation.sentAn invitation e-mail was handed to the mail provider.
invitation.deliveredThe recipient’s mail server accepted the invitation.
invitation.bouncedThe invitation could not be delivered.
checkin.admittedSomebody was let in at the door or at a kiosk.
ticket.issuedTickets were issued for a paid order.
event.publishedAn event went live: guests hold links and the door matters.
event.cancelledAn event was called off.
seatmap.publishedA seating plan was published to guests.
badge.printedName badges were printed.

The message

Every message has the same envelope. data holds the record it is about, in the same shape the API returns. Messages never include invitation links, ticket codes, the guest’s private note, or dietary, allergy and accessibility answers.

Winglark-Signature
t=1762338012,v1=5f2b…c9e1
Winglark-Event
rsvp.submitted
Winglark-Delivery
7c1e9a42-…
User-Agent
Winglark-Webhooks/1
Content-Type
application/json
{
  "id": "evt_3f9a1c7e5b2d4a6c8e0f1a2b",
  "object": "event",
  "type": "rsvp.submitted",
  "api_version": "v1",
  "created_at": "2026-11-05T18:40:12.481Z",
  "organization_id": "9d8c7b6a-5f4e-4d3c-a2b1-0f9e8d7c6b5a",
  "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
  "data": {
    "rsvp": {
      "id": "0c9b7a5e-3d1f-4e2a-b6c8-9f0e1d2c3b4a",
      "object": "rsvp",
      "event_id": "6f1c2a7e-4b1d-4c55-9a3e-2f8d7b0c1e42",
      "guest_id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17",
      "response": "yes",
      "party_size": 2,
      "attendance_mode": "in_person",
      "revision": 1,
      "responded_via": "link",
      "responded_at": "2026-11-05T18:40:12.000Z",
      "updated_at": "2026-11-05T18:40:12.000Z"
    },
    "guest": { "id": "b8e1d3c4-9f20-4a7b-8c61-5d2e0f9a3b17", "first_name": "Ada", "last_name": "Kaya", "email": "a•••@example.com", "…": "…" },
    "response": "yes",
    "party_size": 2,
    "revision": 1,
    "revised": false,
    "via": "link"
  }
}

Verifying the signature

Each message carries a Winglark-Signature header: t is the Unix time it was signed, v1 is the hex HMAC-SHA256 of t, a full stop and the raw body, keyed with your endpoint’s secret.

Compute it over the raw bytes before parsing the JSON, compare in constant time, and refuse a t more than five minutes from your clock. After a rotation there are two v1 values for 24 hours; accept the message if either matches.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const secret = process.env.WINGLARK_WEBHOOK_SECRET; // whsec_…
const app = express();

// The raw body: the signature covers the bytes, not the parsed JSON.
app.post('/winglark', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Winglark-Signature') ?? '';
  const parts = header.split(',').map((part) => part.split('='));
  const t = Number(parts.find(([key]) => key === 't')?.[1]);
  const signatures = parts.filter(([key]) => key === 'v1').map(([, value]) => value);

  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);

  const expected = createHmac('sha256', secret).update(`${t}.`).update(req.body).digest();
  const valid = signatures.some((signature) => {
    const given = Buffer.from(signature, 'hex');
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
  if (!valid) return res.sendStatus(400);

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.id: a message can arrive more than once.
  res.sendStatus(204);
});

app.listen(3000);

Python

import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

SECRET = os.environ["WINGLARK_WEBHOOK_SECRET"].encode()  # whsec_…
app = Flask(__name__)


@app.post("/winglark")
def winglark():
    header = request.headers.get("Winglark-Signature", "")
    parts = [part.split("=", 1) for part in header.split(",") if "=" in part]
    t = next((value for key, value in parts if key == "t"), None)
    signatures = [value for key, value in parts if key == "v1"]

    if not t or not t.isdigit() or abs(time.time() - int(t)) > 300:
        abort(400)

    raw = request.get_data()  # the raw bytes, before any JSON parsing
    expected = hmac.new(SECRET, t.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not any(hmac.compare_digest(expected, signature) for signature in signatures):
        abort(400)

    event = json.loads(raw)
    # Deduplicate on event["id"]: a message can arrive more than once.
    return "", 204

Retries

A delivery that does not get a 2xx is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, then marked failed. Retries are sent at the first opportunity after they fall due, at the latest within the hour.

If every delivery to an endpoint has failed for three days, Winglark stops sending to it and says so in the panel. Resume it once the receiver is fixed; a failed delivery can be sent again from the log.

  1. 1. 1 min
  2. 2. 5 min
  3. 3. 30 min
  4. 4. 2 h
  5. 5. 6 h
  6. 6. 12 h
  7. 7. 24 h

Versioning

This is version 1. Within it we only add: new fields, new event types, new optional parameters. Ignore fields you do not know. Anything that would break an integration goes into a new version, announced in advance.

Automation platforms

Tools that support REST hooks can subscribe with POST /v1/webhooks and unsubscribe with DELETE /v1/webhooks/{id}, using a key with webhooks:manage. GET /v1/me tests a key. The list routes, newest first, double as sample data.