Developer & Partner API

Build on SendLynx

A scoped OAuth 2.0 REST API with signed webhooks. Read and manage a business's scheduling data, only after that business explicitly authorizes your app.

PKCE-required auth REST endpoints Signed webhooks

OAuth 2.0 · REST · Webhooks

Overview

The SendLynx API lets your platform read and manage a business's scheduling data after that business explicitly authorizes you. Access is scoped per business: you only ever touch the data of businesses that connected your app.

Connecting an app is a SendLynx Studio feature. If a connected business later drops below Studio, its tokens stop refreshing and API access lapses within an hour (403 access_denied); it resumes automatically if they upgrade again within 30 days. After that the refresh token itself expires and the business re-authorizes your app once.

  1. Redirect the business owner to SendLynx to log in and approve your app.
  2. SendLynx redirects back with a one-time code.
  3. Your server exchanges the code for an access token + refresh token.
  4. You call /api/v1/* with the access token.
  5. SendLynx sends you signed webhooks as bookings and payments change.

Discovery metadata: https://sendlynx.com/.well-known/oauth-authorization-server

MCP connector

SendLynx runs a remote MCP server at https://mcp.sendlynx.com, so an AI app can work in a business's account without anyone writing code. The owner adds that address to their MCP client (in Claude: Settings, then Connectors, then Add custom connector; in ChatGPT: developer mode under Connectors, then add a connector), signs in to SendLynx and approves. It needs SendLynx Pro or Studio, and only an owner or admin can connect it.

Sign-in

OAuth 2.1 authorization code with PKCE (S256, required). A request without a token gets a 401 whose WWW-Authenticate header points at the protected resource metadata (RFC 9728), which names https://sendlynx.com as the authorization server. Clients register themselves with Dynamic Client Registration (RFC 7591) at https://sendlynx.com/api/oauth/register. Registered clients are public (token_endpoint_auth_method: none), and redirect URIs must be https or a loopback address. Send resource=https://mcp.sendlynx.com (RFC 8707) on authorize and token requests. Access tokens last an hour, refresh tokens rotate, and a replayed refresh token revokes the connection.

https://mcp.sendlynx.com/.well-known/oauth-protected-resource
https://sendlynx.com/.well-known/oauth-authorization-server

Transport

Streamable HTTP, stateless: POST JSON-RPC to https://mcp.sendlynx.com (or /mcp) with Authorization: Bearer <access token>. There are no sessions to keep.

What it can do

A connector holds the AI assistant permissions and nothing more: services:read, staff:read, availability:read, calendar:read, bookings:read, bookings:write, bookings:manage, clients:read, clients:write, payments:read, quotes:read, quotes:write, settings:read. Quotes and invoices are drafts only: the connector can create, change and delete drafts and get their PDF, and the owner sends them from SendLynx. It can't send anything to a customer, take payment, text clients, change settings or manage the team. The owner can disconnect it any time in Settings, under Connected apps.

get_business        search_clients      get_client         create_client
list_services       list_staff          check_availability
list_bookings       get_booking         create_booking
reschedule_booking  cancel_booking
list_quotes         get_quote           create_quote_draft
update_quote_draft  delete_quote_draft  get_quote_pdf_link

Every tool runs the same code as its REST endpoint, with the same tenant, plan and permission checks. Money is integer cents. An AI app that can't do OAuth can use an owner's AI assistant key (Dashboard, Integrations) as the bearer token instead, with the same permissions.

Quickstart

Apps are created in two ways: self-serve from your dashboard (Developers → Create app) or by the SendLynx team for partners. Either way you get a client_id, a client_secret (shown once), your scopes, and an optional webhook_secret. The flow is Authorization Code with PKCE (S256, required); keep your client_secret server-side.

Authentication

Generate a PKCE verifier/challenge and send the owner's browser to:

https://sendlynx.com/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/oauth/callback
  &scope=availability:read bookings:read bookings:write clients:write
  &state=RANDOM_CSRF_STRING
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

The owner approves; SendLynx redirects to your redirect_uri with ?code=...&state=... (the redirect_uri must exactly match a registered one). Exchange the code:

curl -X POST https://sendlynx.com/api/oauth/token \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d grant_type=authorization_code \
  -d code=THE_CODE \
  -d redirect_uri=https://yourapp.com/oauth/callback \
  -d code_verifier=YOUR_PKCE_VERIFIER

# → { "access_token": "...", "token_type": "Bearer",
#     "expires_in": 3600, "refresh_token": "...", "scope": "..." }

Access tokens last 1 hour. Refresh tokens last 30 days and rotate on every use, so store the newest one. Replaying an old refresh token revokes the whole authorization.

curl -X POST https://sendlynx.com/api/oauth/token \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d grant_type=refresh_token \
  -d refresh_token=THE_REFRESH_TOKEN

Scopes

services:readView your services, pricing, variants and add-ons
services:writeCreate, update and remove your services
staff:readView your team members
staff:writeAdd, update and remove your team members
availability:readCheck your open appointment times
calendar:readSee when connected personal calendars are busy (never event titles or details)
bookings:readView your appointments
bookings:writeCreate, reschedule and cancel appointments
clients:readView your customer contacts
clients:writeAdd and update customer contacts
payments:readView deposit/payment status for jobs
quotes:readView your quotes, estimates and invoices
quotes:writeCreate, send, convert and collect on quotes
hooks:manageSubscribe to and manage webhook notifications for your business
bookings:manageConfirm, complete, mark no-show and annotate appointments
payments:writeIssue refunds and resend deposit links
schedule:writeEdit availability, exceptions and time blocks
messaging:sendMessage your clients by email or SMS
team:manageManage team members and invitations
waitlist:manageManage your waitlist
reviews:manageView and publish client reviews
settings:readView your business settings
settings:writeUpdate your business settings
insights:readView dashboard metrics and insights
devices:writeRegister this device for push notifications
account:readView your account security status (two-factor)
account:writeManage your account security (enable/disable two-factor)

API console

Once you're logged in on a Studio (or trial) account, you can run real API calls against your own business data right in the browser: pick an endpoint, edit the params, and hit Send.

Open the API console →

Endpoints

Base URL https://sendlynx.com/api/v1; a machine-readable description of every endpoint lives at https://sendlynx.com/api/v1/openapi.json. Pass the access token as Authorization: Bearer …. List endpoints are cursor-paginated (?limit= max 100, ?cursor=) and return { data, next_cursor, has_more }. Send an Idempotency-Key header on POST /bookings so retries never double-book (other endpoints don't honor the header yet).

Services

GET
/api/v1/services
services:read

List services with their variants and add-ons.

activequeryOnly active services (default true).
limitqueryPage size, max 100.
cursorqueryPagination cursor from a previous response.
GET
/api/v1/services/:id
services:read

Retrieve a single service.

idpath · requiredService id.
POST
/api/v1/services
services:write

Create a service. Money is in integer cents. paymentMode defaults to 'deposit' when a deposit is set, else 'in_person'.

Example body

{
  "name": "Men's Haircut",
  "description": "Classic cut and style.",
  "durationMin": 30,
  "priceCents": 5000,
  "depositCents": 1000,
  "paymentMode": "deposit",
  "bufferMin": 5,
  "active": true,
  "rebookEnabled": true,
  "rebookIntervalDays": 28
}
PATCH
/api/v1/services/:id
services:write

Update a service. Send only the fields you want to change.

idpath · requiredService id.

Example body

{
  "priceCents": 5500,
  "active": true
}
DELETE
/api/v1/services/:id
services:write

Delete a service. Fails with 409 if it has bookings. Deactivate it (PATCH active:false) instead to keep its history.

idpath · requiredService id.

Staff

GET
/api/v1/staff
staff:read

List team members (optionally filtered to a service).

serviceIdqueryOnly staff who offer this service.
activequery
POST
/api/v1/staff
staff:write

Create a provider (owner/admin). Enforces your plan's provider cap and seeds default Mon–Fri 9–5 hours. Pass serviceIds to set which services they offer.

Example body

{
  "name": "Jordan Smith",
  "email": "jordan@example.com",
  "bio": "Senior stylist",
  "active": true,
  "serviceIds": []
}
PATCH
/api/v1/staff/:id
staff:write

Update a provider (owner/admin). Send only the fields you want to change; serviceIds replaces their service assignments.

idpath · requiredStaff id.

Example body

{
  "name": "Jordan A. Smith",
  "active": true
}
DELETE
/api/v1/staff/:id
staff:write

Delete a provider (owner/admin). Fails with 409 if they have bookings. Deactivate them (PATCH active:false) instead to keep history.

idpath · requiredStaff id.

Availability

GET
/api/v1/availability
availability:read

Open appointment slots per day across a date range (max 14 days). For a multi-service session pass additionalServiceIds: slots are sized to the merged duration and only providers who offer EVERY selected service are considered (so the slots are bookable with the same combo via POST /bookings).

serviceIdquery · required
staffIdqueryA staff id, or 'any' (default).
fromquery · requiredYYYY-MM-DD.
toqueryYYYY-MM-DD (defaults to `from`).
durationqueryOverride duration in minutes (applies to the primary service).
additionalServiceIdsqueryComma-separated extra service ids for a multi-service session: their duration is added and the provider pool is narrowed to those offering all of them.
GET
/api/v1/calendar/external
calendar:read

When your team's connected personal calendars (Google) are busy, for a window of up to 45 days, so nothing gets booked over them. Integrations and assistants see each block as "Busy" with its start and end only; event titles are shown only inside the SendLynx apps. Providers whose calendar could not be read are listed in `unreadable`.

fromquery · requiredISO-8601 instant.
toquery · requiredISO-8601 instant, at most 45 days after from.

Bookings

GET
/api/v1/bookings
bookings:read

List bookings, newest first.

statusquerypending | confirmed | requested | completed | cancelled | declined | no_show. `requested` is a booking awaiting the owner's Accept/Decline on a requiresConfirmation service (it holds the slot); `declined` is one the owner turned down.
clientIdquery
fromqueryISO datetime: filters startTime.
toqueryISO datetime: filters startTime.
limitquery
cursorquery
POST
/api/v1/bookings
bookings:write

Create an appointment. customer.email OR customer.phone is required (a phone-only customer gets a synthetic address).

smsConsent is a LEGAL CONSENT ASSERTION, not a message toggle: pass true ONLY when your application has captured the customer's express consent to receive appointment texts (TCPA). It is recorded durably on the client. When true (with a phone), SendLynx also sends an SMS confirmation. Set notify:false to suppress SendLynx's confirmation (when you send your own). The response carries sms:{sent,reason} so you can see whether the confirmation text went out (e.g. reason:"sms_disabled").

If a deposit is required, returns a depositCheckoutUrl. MULTI-SERVICE: pass additionalServiceIds:[…] to book several services in one appointment. Their duration and price merge into the session, the chosen provider must offer EVERY service (with staffId:"any" only providers who do all of them are considered), and the booking response includes a services[] snapshot ({serviceId,name,durationMin,priceCents}, primary first). variantId applies to the primary service; addOnIds may reference an add-on of ANY service in the session (primary or an additionalServiceId) and each add-on's price + time merges into the totals.

CUSTOM PRICE (first-party app and owner keys, token holder an owner or admin): pass priceCents (whole cents, 0 allowed) to book this visit at that price instead of the computed total, with an optional priceReason (200 chars). The booking's priceCents and balance due follow it, and its timeline records "Price set to $X by <name>: <reason>". Other applications get 403; an AI assistant key books at the listed price.

Example body

{
  "serviceId": "REPLACE_WITH_SERVICE_ID",
  "staffId": "any",
  "startUtc": "2026-06-20T15:00:00Z",
  "customer": {
    "name": "Jane Doe",
    "phone": "+15551234567"
  },
  "additionalServiceIds": [],
  "addOnIds": [],
  "smsConsent": true,
  "notify": true,
  "serviceAddress": "12 Main St",
  "notes": "Leaky faucet"
}
GET
/api/v1/bookings/:id
bookings:read

Retrieve a single booking.

idpath · required
PATCH
/api/v1/bookings/:id
bookings:write

Reschedule a booking to a new start time.

idpath · required

Example body

{
  "startUtc": "2026-06-21T16:00:00Z"
}
POST
/api/v1/bookings/:id/cancel
bookings:write

Cancel a booking. Money collected on it is refunded, so owners and admins only when it has a payment.

keepDeposit:true is a late cancellation inside the business's cancelCutoffHours where the business keeps the deposit as the fee its cancellation policy names: everything else paid is refunded, and the customer's cancellation email and text say the deposit was kept and quote the policy. Outside the cutoff, with no cutoff, or with no deposit collected, the flag is ignored, the whole ledger is refunded and the response carries a `warnings` entry saying why. The booking's `depositKeptCents` records what was kept.

idpath · required

Example body

{
  "reason": "Customer requested",
  "keepDeposit": false
}
POST
/api/v1/bookings/:id/cancel-series
bookings:write

Cancel this booking and every upcoming occurrence of its recurring series (each goes through the full cancel path: deposit refund, seat release, calendar + waitlist). Falls back to a single cancel if the booking isn't recurring. Returns { cancelled: <count> }.

idpath · required
POST
/api/v1/bookings/:id/arrive
bookings:manage

Check the client in: sets `arrivedAt` once and adds an "Arrived at 2:05 PM" line to the booking's timeline. The status does not change.

A second call is a no-op (200, same `arrivedAt`). Refused (409 not_modifiable) on a cancelled, completed, no-show or requested booking, and when the start is more than 12 hours away. A team member may check in bookings assigned to their own provider profile only (403 forbidden otherwise).

idpath · required
POST
/api/v1/bookings/:id/collect
payments:write

Collect the balance due on a booking. Owners and admins only, like the dashboard's Collect payment dialog.

method is card | cash | other. cash and other record money taken in person: amountCents goes toward the price, tipCents (0 to 100000) is stored beside it and never counts toward the price or the balance due, and emailReceipt (default true) sends the client a receipt with the service, amount, tip and method; a placeholder address gets none. card returns a payment link: without tipCents it is SendLynx's pay page, where the client adds a tip (none, 15%, 20%, 25% or their own amount) before Stripe Checkout opens; with tipCents the link is the Checkout itself, with the tip as its own line. The webhook marks the booking paid and keeps the tip in payments.tipCents. Deposits take no tip. The response carries url (card), paymentStatus and paymentId (cash/other).

idpath · required

Example body

{
  "method": "cash",
  "amountCents": 6000,
  "tipCents": 1000,
  "emailReceipt": true
}
POST
/api/v1/bookings/:id/charge
payments:write

Email the client a payment link for an extra charge on top of the booked price (the job grew). Owners and admins only. Direct charge on the business's connected account; the booking itself is unchanged. tipCents (optional, 0 to 100000) adds a tip the client already agreed to as its own line; it is stored apart from the charge. Returns the link so the app can also show or share it.

idpath · required

Example body

{
  "amountCents": 2500,
  "reason": "Extra room",
  "tipCents": 0
}

Clients

GET
/api/v1/clients
clients:read

List customer contacts (each includes phone). Look up by email or phone, or search by name with q.

An email ending in .sendlynx.local (walkin-...@manual.sendlynx.local, 5551234567@phone.sendlynx.local) or in .invalid is a placeholder for a client with no email on file: nothing is ever mailed to it. Clients carry no street address.

emailqueryExact-match lookup by email.
phonequeryLookup by phone. It matches on the last 10 digits, so any format works (+1, spaces, dashes, parens).
qquerySearch: case-insensitive partial match on name or email.
limitquery
cursorquery
POST
/api/v1/clients
clients:write

Create or update a customer by email (201 if new, 200 if matched).

Example body

{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "phone": "+15551234567",
  "smsConsent": false
}
GET
/api/v1/clients/:id
clients:read

Retrieve a single customer.

idpath · required
PATCH
/api/v1/clients/:id
clients:write

Update a customer. Email can be changed (e.g. a phone-only client who later gives a real address); it's the dedup key, so a collision with another client returns 409. smsConsent can only be turned on; pass notes to set the operator note (empty string clears it).

idpath · required

Example body

{
  "name": "Jane A. Doe",
  "email": "jane@example.com",
  "phone": "+15559876543",
  "notes": "Prefers morning appointments."
}
POST
/api/v1/clients/:id/erase
clients:write

Erase a client's personal data (owner/admin), for a "delete my data" request. Upcoming bookings are cancelled and refunded through the normal cancel path, then the client row and every booking, quote, text and mail log for them is scrubbed. Bookings and payments stay as history under "Deleted client". Returns { ok, alreadyErased, cancelledBookings }; a second call is a no-op.

idpath · required
POST
/api/v1/leads
clients:write

Add a lead (someone who showed interest but hasn't booked) and let SendLynx handle the first touch: a follow-up text with the booking link between 8 AM and 8 PM in the business's timezone (held until morning otherwise), or an email invite when there is no text consent. Text consent is never inferred from the source: pass smsConsent:true only when the form had a text-consent checkbox the person ticked, with its wording as consentText. A Facebook lead posted without smsConsent:true is still created and never texted: with an email the invite goes by email, with a phone alone followUp is { channel: "none", status: "skipped", reason: "no_consent" }; `warnings` says what to pass either way. The owner gets a push and an email with the form's answers. Re-posting the same person (same email or phone) is safe: nothing is re-sent and a booked client is never demoted.

Example body

{
  "name": "Maria Alvarez",
  "email": "maria@example.com",
  "phone": "+17045550168",
  "source": "facebook",
  "formName": "Free consult",
  "formId": "1234567890",
  "answers": [
    {
      "question": "What brings you in?",
      "answer": "Lower back pain"
    }
  ],
  "smsConsent": true,
  "consentText": "I agree to receive text messages from At Home Chiropractic about my request.",
  "consentAt": "2026-10-20T14:05:00Z"
}

Payments

GET
/api/v1/payments
payments:read

List deposit/payment records (no raw Stripe ids). Each carries amountCents (the price part), tipCents (a tip beside it, never toward the price), method (card | cash | other), refundedAmountCents (of the price part) and refundedTipCents (a tip handed back with a full refund).

bookingIdquery
statusquerypending | paid | refunded | failed | not_required
limitquery
cursorquery

Quotes

GET
/api/v1/quotes
quotes:read

List estimates and invoices, newest first. Each carries its line items, totals in cents, status and amountPaidCents.

statusquerydraft | sent | approved | declined | expired | paid | converted | void
limitquery
cursorquery
POST
/api/v1/quotes
quotes:write

Create an estimate or invoice as a draft. Nothing is sent to the customer. Money is integer cents; totals and tax are computed for you.

kind is "estimate" (the default) or "invoice". Each line item has description, quantity (decimals allowed), unitPriceCents, an optional unit label ("hr", "sq ft", up to 16 characters) and taxable (default true; it only matters when taxRateBps is set). taxRateBps is basis points (725 = 7.25%) and applies to taxable lines only. collectOnApproval is none | deposit | full. depositKind defaults to "amount" (depositCents, at least 50 and below the total), so a percent deposit must send depositKind "percent" with depositBps: 30% down is collectOnApproval "deposit", depositKind "percent", depositBps 3000. expiresAt (YYYY-MM-DD) is the valid-until date on an estimate and the due date on an invoice, at the end of that day in the business's timezone; an invoice without one gets the business's default due date when it is sent. Leave terms out to copy in the business's default terms (null when it has none). serviceId is one top-level field, the service the job is for (used when the quote is later booked); line items do not take a serviceId. The EST-/INV- number (docNumber) is assigned when the quote is first sent. customerEmail is required; the customer is matched to (or added as) a client. Each call creates a new draft, so check GET /api/v1/quotes?status=draft for an open draft for the same customer and title first, and ask the owner before making another.

Example body

{
  "kind": "estimate",
  "customerName": "Dana Whitfield",
  "customerEmail": "dana@example.com",
  "customerPhone": "+15551234567",
  "title": "Back deck restain",
  "message": "Semi-transparent cedar tone, same as last time.",
  "lineItems": [
    {
      "description": "Deck boards, clean and stain",
      "quantity": 320,
      "unit": "sq ft",
      "unitPriceCents": 350,
      "taxable": true
    },
    {
      "description": "Railing, stain",
      "quantity": 48,
      "unit": "linear ft",
      "unitPriceCents": 600,
      "taxable": true
    },
    {
      "description": "Labor, prep and cleanup",
      "quantity": 3.5,
      "unit": "hr",
      "unitPriceCents": 6500,
      "taxable": false
    }
  ],
  "collectOnApproval": "deposit",
  "depositKind": "percent",
  "depositBps": 2500,
  "serviceAddress": "12 Main St"
}
GET
/api/v1/quotes/:id
quotes:read

Retrieve one estimate or invoice, including amountPaidCents (the balance is totalCents minus amountPaidCents).

idpath · requiredQuote id.
PATCH
/api/v1/quotes/:id
quotes:write

Replace a draft or sent quote with the body you send. GET it first and send every field back with your changes.

This is a full replacement: customerName, customerEmail, title and lineItems are required, and taxRateBps, expiresAt, serviceAddress, message and the deposit fields are cleared when left out. photos, poNumber and terms are kept when left out (null clears them). Fails with 422 once the customer has approved or paid. AI assistant keys may edit drafts only.

idpath · requiredQuote id.

Example body

{
  "kind": "estimate",
  "customerName": "Dana Whitfield",
  "customerEmail": "dana@example.com",
  "customerPhone": "+15551234567",
  "title": "Back deck restain",
  "message": "Semi-transparent cedar tone, same as last time.",
  "lineItems": [
    {
      "description": "Deck boards, clean and stain",
      "quantity": 320,
      "unit": "sq ft",
      "unitPriceCents": 350,
      "taxable": true
    },
    {
      "description": "Railing, stain",
      "quantity": 60,
      "unit": "linear ft",
      "unitPriceCents": 600,
      "taxable": true
    },
    {
      "description": "Labor, prep and cleanup",
      "quantity": 3.5,
      "unit": "hr",
      "unitPriceCents": 6500,
      "taxable": false
    }
  ],
  "collectOnApproval": "deposit",
  "depositKind": "percent",
  "depositBps": 2500,
  "serviceAddress": "12 Main St"
}
DELETE
/api/v1/quotes/:id
quotes:write

Delete an unsent draft. A quote that was ever sent, or has a payment, is voided in SendLynx instead (422 here).

idpath · requiredQuote id.
GET
/api/v1/quotes/:id/pdf
quotes:read

The estimate or invoice as a PDF (application/pdf), the same document the customer gets, without their pay link.

idpath · requiredQuote id.
POST
/api/v1/quotes/:id/send
quotes:write

Send (or re-send) the quote to the customer by email and/or text. Assigns the EST-/INV- number on first send. Body flags default to email only. Returns 422 delivery_failed when nothing could be delivered. Not available to AI assistant keys (403 assistant_not_allowed).

idpath · requiredQuote id.

Example body

{
  "email": true,
  "sms": false
}
POST
/api/v1/quotes/:id/collect
quotes:write

Create a Stripe payment link for the remaining balance. Returns { data: { url } }. Not available to AI assistant keys (403 assistant_not_allowed).

idpath · requiredQuote id.
POST
/api/v1/quotes/:id/convert
quotes:write

Schedule an approved quote as a booking with a provider. Returns { data: { bookingId } }. Not available to AI assistant keys (403 assistant_not_allowed).

idpath · requiredQuote id.

Example body

{
  "staffId": "REPLACE_WITH_STAFF_ID",
  "startUtc": "2026-11-10T14:00:00Z",
  "durationMin": 240
}
POST
/api/v1/quotes/:id/photos
quotes:write

Upload a job photo (multipart 'file', image/*) and append it to the quote's photos (max 24). Returns { data: { url } }.

idpath · requiredQuote id.

Webhooks

GET
/api/v1/hooks
hooks:manage

List your webhook subscriptions (signing secrets are never returned).

POST
/api/v1/hooks
hooks:manage

Subscribe an HTTPS URL to events: booking.created, booking.requested, booking.canceled, booking.rescheduled, payment.received, client.created. Returns the HMAC signing `secret` ONCE. Store it to verify the X-SendLynx-Signature header. 10 consecutive delivery failures auto-disable a subscription.

Example body

{
  "url": "https://example.com/webhooks/sendlynx",
  "events": [
    "booking.created",
    "payment.received"
  ]
}
DELETE
/api/v1/hooks/:id
hooks:manage

Unsubscribe a webhook (an integration calls this when it's switched off).

idpath · requiredSubscription id.
GET
/api/v1/hooks/sample/:event
hooks:manage

A realistic static sample delivery for an event type, shaped exactly like a live webhook POST body.

eventpath · requiredOne of the event types listed under hooks.create.

Account

GET
/api/v1/account/2fa
account:read

Two-factor status for your account: { enabled, enabledAt }.

POST
/api/v1/account/2fa/enroll
account:write

Begin two-factor enrollment. Returns an otpauth keyUri, a QR data URL, and the manual secret. Does not enable 2FA until you confirm.

POST
/api/v1/account/2fa/confirm
account:write

Confirm enrollment with a live code; enables 2FA and returns the one-time backup codes.

Example body

{
  "code": "123456"
}
POST
/api/v1/account/2fa/disable
account:write

Disable 2FA after re-proving identity with your password OR a current authenticator code.

Example body

{
  "code": "123456"
}

Settings

GET
/api/v1/settings
settings:read

The business's operational config, including smsEnabled (whether SMS confirmations/reminders are turned on), timezone, deposit/notice/cancel policies, and branding.

PATCH
/api/v1/settings
settings:write

Partial update of operational settings (owner/admin). Set smsEnabled:true to turn on SMS for a business. New businesses start with it OFF. (SMS still also requires an SMS-capable plan and per-client consent before a text actually sends.)

Example body

{
  "smsEnabled": true,
  "notifyOnPayment": true
}
GET
/api/v1/audit
settings:read

The Activity log (owner/admin), newest first: who changed settings, hours, the team, refunds and kept deposits, integrations, keys and the booking link. Each row has kind, summary, actor, createdAt and an optional targetType/targetId. Returns { data, has_more, next_before }.

limitqueryPage size, max 100.
beforequeryPage back: the id of the last row you saw (next_before), or an ISO date.
kindqueryA group (settings, team, money, integrations, clients, account) or one exact kind such as refund.issued.
GET
/api/v1/settings/calendar
settings:read

Google Calendar status: { connected, googleEmail, available, connectUrl }. Open connectUrl in a browser to connect.

DELETE
/api/v1/settings/calendar
settings:write

Disconnect Google Calendar for your provider profile.

POST
/api/v1/settings/logo
settings:write

Upload the business logo (multipart 'file', image/* ≤10MB). Sets logoUrl.

POST
/api/v1/settings/cover
settings:write

Upload the business cover image (multipart 'file', image/* ≤10MB). Sets coverImageUrl.

SMS

GET
/api/v1/sms/usage
settings:read

SMS allowance + usage this month: { monthlyCap, usedThisMonth, allowanceRemaining, purchasedCreditBalance }. Buying credits is web-only.

Webhooks

Webhook subscriptions (REST hooks)

Subscribe any HTTPS endpoint to events with POST /api/v1/hooks (scope hooks:manage): pass { url, events } and store the returned secret (shown once). Each event also requires its data scope on your grant:

  • booking.created, booking.requested, booking.canceled and booking.rescheduled need bookings:read
  • payment.received needs payments:read
  • client.created needs clients:read

Subscriptions are bound to your app: narrowing your grant's scopes via re-consent narrows the feed immediately, and revoking the app stops it. A push never carries data the same grant could not read over /api/v1.

Each delivery is a JSON envelope { id, event, createdAt, data } signed via the X-SendLynx-Signature header (same HMAC scheme as below). Unsubscribe with DELETE /api/v1/hooks/:id; get editor sample data from GET /api/v1/hooks/sample/:event. Ten consecutive delivery failures auto-disable a subscription (re-enable it from Developers → Webhooks). This is the surface our Slack alerts and custom endpoints ride on.

Partner-app webhooks

If your OAuth app has a webhook URL, SendLynx POSTs JSON events: booking.created, booking.rescheduled, booking.cancelled, booking.completed (scope bookings:read), payment.completed (scope payments:read) and client.opted_out / client.opted_in (scope clients:read). You only receive the events your grant's scopes cover. A push never carries data the same grant could not read over /api/v1, and narrowing scopes via re-consent stops the feed immediately. Delivery is at-least-once with retries, so dedupe on the Sendlynx-Event-Id header. Verify the Sendlynx-Signature header (no X- prefix; the REST hooks above use X-SendLynx-Signature) before trusting an event:

const crypto = require("crypto");

function verifySendlynxWebhook(rawBody, signatureHeader, secret) {
  // header: "t=1718900000,v1=<hex>"
  if (typeof signatureHeader !== "string") throw new Error("Invalid signature");
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  // timingSafeEqual throws on a length mismatch, so check it first.
  if (typeof parts.v1 !== "string" || parts.v1.length !== expected.length) {
    throw new Error("Invalid signature");
  }
  const ok = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!ok) throw new Error("Invalid signature");
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) throw new Error("Stale webhook");
  return JSON.parse(rawBody); // { id, type, created, business_id, data }
}

Errors

Errors are JSON: { "error": "code", "error_description": "..." }.

400  invalid_request        Malformed request / validation failed
401  invalid_token          Missing, expired, or revoked access token
403  insufficient_scope     Token lacks the required scope
403  access_denied          Business's plan no longer includes API access (Studio)
403  business_inactive      The business isn't taking bookings right now
404  not_found              Resource missing or not yours
409  slot_unavailable       That time was just taken
409  client_conflict        Customer already has an overlapping booking
422  group_booking_unsupported  Class/group services can't be booked via API yet
429  rate_limited           Slow down

Revocation

A business owner can revoke your app anytime from Settings → Connected apps; you can also revoke programmatically (RFC 7009):

curl -X POST https://sendlynx.com/api/oauth/revoke \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d token=THE_TOKEN

After revocation, all tokens for that business stop working immediately.