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.
- 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
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-serverTransport
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_linkEvery 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=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 |
| calendar:read | See when connected personal calendars are busy (never event titles or details) |
| 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; 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
/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 them (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. |
/api/v1/calendar/externalWhen 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`.
| from | query · required | ISO-8601 instant. |
| to | query · required | ISO-8601 instant, at most 45 days after from. |
Bookings
/api/v1/bookingsList bookings, newest first.
| status | query | pending | 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. |
| 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 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"
}/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. 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.
| id | path · required |
Example body
{
"reason": "Customer requested",
"keepDeposit": false
}/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 |
/api/v1/bookings/:id/arriveCheck 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).
| id | path · required |
/api/v1/bookings/:id/collectCollect 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).
| id | path · required |
Example body
{
"method": "cash",
"amountCents": 6000,
"tipCents": 1000,
"emailReceipt": true
}/api/v1/bookings/:id/chargeEmail 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.
| id | path · required |
Example body
{
"amountCents": 2500,
"reason": "Extra room",
"tipCents": 0
}Clients
/api/v1/clientsList 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.
| query | Exact-match lookup by email. | |
| phone | query | Lookup by phone. It matches on the last 10 digits, so any format works (+1, spaces, dashes, parens). |
| q | query | Search: case-insensitive partial match on name or email. |
| 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."
}/api/v1/clients/:id/eraseErase 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.
| id | path · required |
/api/v1/leadsAdd 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
/api/v1/paymentsList 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).
| bookingId | query | |
| status | query | pending | paid | refunded | failed | not_required |
| limit | query | |
| cursor | query |
Quotes
/api/v1/quotesList estimates and invoices, newest first. Each carries its line items, totals in cents, status and amountPaidCents.
| status | query | draft | sent | approved | declined | expired | paid | converted | void |
| limit | query | |
| cursor | query |
/api/v1/quotesCreate 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"
}/api/v1/quotes/:idRetrieve one estimate or invoice, including amountPaidCents (the balance is totalCents minus amountPaidCents).
| id | path · required | Quote id. |
/api/v1/quotes/:idReplace 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.
| id | path · required | Quote 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"
}/api/v1/quotes/:idDelete an unsent draft. A quote that was ever sent, or has a payment, is voided in SendLynx instead (422 here).
| id | path · required | Quote id. |
/api/v1/quotes/:id/pdfThe estimate or invoice as a PDF (application/pdf), the same document the customer gets, without their pay link.
| id | path · required | Quote id. |
/api/v1/quotes/:id/sendSend (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).
| id | path · required | Quote id. |
Example body
{
"email": true,
"sms": false
}/api/v1/quotes/:id/collectCreate a Stripe payment link for the remaining balance. Returns { data: { url } }. Not available to AI assistant keys (403 assistant_not_allowed).
| id | path · required | Quote id. |
/api/v1/quotes/:id/convertSchedule an approved quote as a booking with a provider. Returns { data: { bookingId } }. Not available to AI assistant keys (403 assistant_not_allowed).
| id | path · required | Quote id. |
Example body
{
"staffId": "REPLACE_WITH_STAFF_ID",
"startUtc": "2026-11-10T14:00:00Z",
"durationMin": 240
}/api/v1/quotes/:id/photosUpload a job photo (multipart 'file', image/*) and append it to the quote's photos (max 24). Returns { data: { url } }.
| id | path · required | Quote id. |
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 (an integration calls this when it's switched 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/auditThe 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 }.
| limit | query | Page size, max 100. |
| before | query | Page back: the id of the last row you saw (next_before), or an ISO date. |
| kind | query | A group (settings, team, money, integrations, clients, account) or one exact kind such as refund.issued. |
/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). Each event also requires its data scope on your grant:
booking.created,booking.requested,booking.canceledandbooking.rescheduledneedbookings:readpayment.receivedneedspayments:readclient.createdneedsclients: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 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.