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

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
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. 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 (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.

Bookings

GET
/api/v1/bookings
bookings:read

List bookings, newest first.

statusquerypending | confirmed | completed | cancelled | no_show
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 + 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.

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.

idpath · required

Example body

{
  "reason": "Customer requested"
}
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

Clients

GET
/api/v1/clients
clients:read

List customer contacts (each includes phone). Look up by email or phone.

emailqueryExact-match lookup by email.
phonequeryLookup by phone — matches on the last 10 digits, so any format works (+1, spaces, dashes, parens).
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."
}

Payments

GET
/api/v1/payments
payments:read

List deposit/payment records (no raw Stripe ids).

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

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/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 signature before trusting an event:

const crypto = require("crypto");

function verifySendlynxWebhook(rawBody, signatureHeader, secret) {
  // header: "t=1718900000,v1=<hex>"
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  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.