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, with no re-authorization needed.
- Redirect the business owner to SendLynx to log in and approve your app.
- SendLynx redirects back with a one-time
code. - Your server exchanges the code for an access token + refresh token.
- You call
/api/v1/*with the access token. - 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=S256The 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_TOKENScopes
| services:read | View your services, pricing, variants and add-ons |
| services:write | Create, update and remove your services |
| staff:read | View your team members |
| staff:write | Add, update and remove your team members |
| availability:read | Check your open appointment times |
| bookings:read | View your appointments |
| bookings:write | Create, reschedule and cancel appointments |
| clients:read | View your customer contacts |
| clients:write | Add and update customer contacts |
| payments:read | View deposit/payment status for jobs |
| quotes:read | View your quotes, estimates and invoices |
| quotes:write | Create, send, convert and collect on quotes |
| hooks:manage | Subscribe to and manage webhook notifications for your business |
| bookings:manage | Confirm, complete, mark no-show and annotate appointments |
| payments:write | Issue refunds and resend deposit links |
| schedule:write | Edit availability, exceptions and time blocks |
| messaging:send | Message your clients by email or SMS |
| team:manage | Manage team members and invitations |
| waitlist:manage | Manage your waitlist |
| reviews:manage | View and publish client reviews |
| settings:read | View your business settings |
| settings:write | Update your business settings |
| insights:read | View dashboard metrics and insights |
| devices:write | Register this device for push notifications |
| account:read | View your account security status (two-factor) |
| account:write | Manage 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.
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 POSTs so retries never double-book.
Services
/api/v1/servicesList services with their variants and add-ons.
| active | query | Only active services (default true). |
| limit | query | Page size, max 100. |
| cursor | query | Pagination cursor from a previous response. |
/api/v1/services/:idRetrieve a single service.
| id | path · required | Service id. |
/api/v1/servicesCreate 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
}/api/v1/services/:idUpdate a service. Send only the fields you want to change.
| id | path · required | Service id. |
Example body
{
"priceCents": 5500,
"active": true
}/api/v1/services/:idDelete a service. Fails with 409 if it has bookings — deactivate it (PATCH active:false) instead to keep its history.
| id | path · required | Service id. |
Staff
/api/v1/staffList team members (optionally filtered to a service).
| serviceId | query | Only staff who offer this service. |
| active | query |
/api/v1/staffCreate 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": []
}/api/v1/staff/:idUpdate a provider (owner/admin). Send only the fields you want to change; serviceIds replaces their service assignments.
| id | path · required | Staff id. |
Example body
{
"name": "Jordan A. Smith",
"active": true
}/api/v1/staff/:idDelete a provider (owner/admin). Fails with 409 if they have bookings — deactivate (PATCH active:false) instead to keep history.
| id | path · required | Staff id. |
Availability
/api/v1/availabilityOpen 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).
| serviceId | query · required | |
| staffId | query | A staff id, or 'any' (default). |
| from | query · required | YYYY-MM-DD. |
| to | query | YYYY-MM-DD (defaults to `from`). |
| duration | query | Override duration in minutes (applies to the primary service). |
| additionalServiceIds | query | Comma-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
/api/v1/bookingsList bookings, newest first.
| status | query | pending | confirmed | completed | cancelled | no_show |
| clientId | query | |
| from | query | ISO datetime: filters startTime. |
| to | query | ISO datetime: filters startTime. |
| limit | query | |
| cursor | query |
/api/v1/bookingsCreate 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"
}/api/v1/bookings/:idRetrieve a single booking.
| id | path · required |
/api/v1/bookings/:idReschedule a booking to a new start time.
| id | path · required |
Example body
{
"startUtc": "2026-06-21T16:00:00Z"
}/api/v1/bookings/:id/cancelCancel a booking.
| id | path · required |
Example body
{
"reason": "Customer requested"
}/api/v1/bookings/:id/cancel-seriesCancel 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> }.
| id | path · required |
Clients
/api/v1/clientsList customer contacts (each includes phone). Look up by email or phone.
| query | Exact-match lookup by email. | |
| phone | query | Lookup by phone — matches on the last 10 digits, so any format works (+1, spaces, dashes, parens). |
| limit | query | |
| cursor | query |
/api/v1/clientsCreate 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
}/api/v1/clients/:idRetrieve a single customer.
| id | path · required |
/api/v1/clients/:idUpdate 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).
| id | path · required |
Example body
{
"name": "Jane A. Doe",
"email": "jane@example.com",
"phone": "+15559876543",
"notes": "Prefers morning appointments."
}Payments
/api/v1/paymentsList deposit/payment records (no raw Stripe ids).
| bookingId | query | |
| status | query | pending | paid | refunded | failed | not_required |
| limit | query | |
| cursor | query |
Webhooks
/api/v1/hooksList your webhook subscriptions (signing secrets are never returned).
/api/v1/hooksSubscribe 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"
]
}/api/v1/hooks/:idUnsubscribe a webhook (Zapier calls this when a Zap is turned off).
| id | path · required | Subscription id. |
/api/v1/hooks/sample/:eventA realistic static sample delivery for an event type — shaped exactly like a live webhook POST body.
| event | path · required | One of the event types listed under hooks.create. |
Account
/api/v1/account/2faTwo-factor status for your account: { enabled, enabledAt }.
/api/v1/account/2fa/enrollBegin two-factor enrollment. Returns an otpauth keyUri, a QR data URL, and the manual secret. Does not enable 2FA until you confirm.
/api/v1/account/2fa/confirmConfirm enrollment with a live code; enables 2FA and returns the one-time backup codes.
Example body
{
"code": "123456"
}/api/v1/account/2fa/disableDisable 2FA after re-proving identity with your password OR a current authenticator code.
Example body
{
"code": "123456"
}Settings
/api/v1/settingsThe business's operational config — including smsEnabled (whether SMS confirmations/reminders are turned on), timezone, deposit/notice/cancel policies, and branding.
/api/v1/settingsPartial 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
}/api/v1/settings/calendarGoogle Calendar status: { connected, googleEmail, available, connectUrl }. Open connectUrl in a browser to connect.
/api/v1/settings/calendarDisconnect Google Calendar for your provider profile.
/api/v1/settings/logoUpload the business logo (multipart 'file', image/* ≤10MB). Sets logoUrl.
/api/v1/settings/coverUpload the business cover image (multipart 'file', image/* ≤10MB). Sets coverImageUrl.
SMS
/api/v1/sms/usageSMS 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). Events: booking.created, booking.requested, booking.canceled, booking.rescheduled, payment.received, client.created. 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 Zapier app rides on.
Partner-app webhooks
If your OAuth app has a webhook URL, SendLynx POSTs JSON events: booking.created, booking.rescheduled, booking.cancelled, booking.completed, payment.completed. 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 downRevocation
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_TOKENAfter revocation, all tokens for that business stop working immediately.