Loading the reference...
API changelog.
Notable changes to the Seedly CRM REST API.
Updated September 18, 2026Notable changes to the Seedly CRM REST API.
Updated September 18, 2026Loading the reference...
All notable changes to the Seedly CRM REST API.
CRM 5.8.11 and 5.8.13.
POST /api/v1/conversations with channel set to sms returns the contact's conversation for calls and texts. A contact's calls and texts are one conversation, so when the contact already has one, the request returns it with "created": false, reopening it if it was closed, instead of starting a second thread beside it. The response shape is unchanged.POST /webhooks/workflow/{slug} and POST /webhooks/event/{slug} answer 404 and start nothing when two or more active workflows with an Inbound Webhook trigger share that URL. Workflows copied from a snapshot used to share one; the 5.8.13 release notes inside your download carry the command that gives each its own.CRM 5.8.7.
PATCH /api/v1/contacts/{id} - update a contact. Contacts could be listed, fetched, and created over the API, but never changed, so anything an assistant learned in a conversation had nowhere to go. The update accepts standard fields and custom fields both, under the existing contacts:write scope. Include only the fields you want to change.PUT /api/v1/contacts/{id}/bot-update - the scalar-only companion. For chatbot custom-tool builders that can only substitute single values into a body and therefore cannot send nested JSON: one instruction per call (set_field, set_custom_field, add_tag, remove_tag), tags merged server-side so the bot never constructs the list, add_tag idempotent and case-insensitive. source and assignedTo are deliberately not settable by a bot.timezone on the contact. The field existed internally but was never returned by the contacts API, and was silently discarded on create. It is now carried end to end - returned, settable on create and update, and used for scheduling in place of the sub-account default.CRM 5.8.5.
POST /api/v1/conversations/{id}/messages now also accepts a contact id in the path. The message is delivered to that contact's most recently active conversation, preferring one that matches the channel body field when given. This is additive, built for template-driven integrators that only know the contact; existing requests addressed by conversation id behave exactly as before.channel became optional on send. When absent (or an empty, unsubstituted {{...}} template value), the message is delivered on the target conversation's own channel. When toAddress is omitted, it falls back to the contact's phone for SMS or email for email channels.404) rather than barging in, so a teammate who answers in a conversation is not talked over by a bot's late replies.CRM 5.8.3 and 5.8.4.
POST /api/v1/booking/appointments/actionable (5.8.4) - the same verified-customer identification as the full lookup, filtered to the appointments the customer can still cancel or move. Cancelled, past, and recurring occurrences are left out, because those are exactly what the write endpoints refuse: an assistant handed a full history has been observed telling the customer the correct upcoming time and then trying to cancel an old cancelled record. When exactly one appointment is actionable, it is handed back on its own in data.appointment so there is nothing to choose between; when several are, meta.ambiguous is true and the assistant should ask the customer by the human-readable label. Same booking-appointments:read scope as the full lookup, because it returns strictly less. The original lookup is untouched.POST /api/v1/booking/appointments answered "no match" for people whose CRM held more than one contact record sharing a phone number or email - which is normal when a form fill lands beside an imported row. The lookup checked one record per identifier and gave up when they turned out to be different rows; it now finds the record that matches both, and a person with several records gets their appointments returned together. Nothing about identification changes: two matching identifiers are still required and must still both belong to the same record.CRM 5.8.0 through 5.8.2. The scope change is breaking for existing keys and is the reason to read this entry before deploying the update.
calendars:read and calendars:write no longer exist. The old calendars:read granted four things at once: the calendar list, the appointment types, the open time slots, and the booked appointments - and that last one returns each customer's name, email address, and phone number with no date range required. So a key handed to an outside company purely to answer "when can you come out?" could also read the customer list. The scopes are now named after what they reach:
| Old scope | New scope | What it reaches now |
|---|---|---|
calendars:read | appointments:read | Booked appointments, with customer names, emails, and phone numbers |
calendars:write | appointments:write | Booking and cancelling appointments; unchanged in what it grants |
| (new) | availability:read | Open times, appointment types, and the calendar list; no customer details |
No old names are kept alive alongside the new ones: a key still holding an old name is refused by the endpoints it used to reach, and the refusal goes to the integration rather than to you. After deploying, go through every key in Settings > Integrations > API Keys: tick availability:read on anything that checks open times, tick appointments:write on anything that books or cancels, and leave appointments:read off any key that never needed the roster - keeping it off booking keys is the whole point of the split.
GET /api/v1/calendars/availability takes an optional endDate beside the date it already took and returns every open slot across the range, up to 31 days. Each slot now arrives with the calendar's timezone, its local date, a ready-to-read label such as "Wednesday, April 1, 2026 at 9:00 AM EDT", and the exact startTime / endTime pair the booking call wants back. The appointment type can be named with appointmentType (name or slug, any capitalisation) instead of its id; an ambiguous name is refused with the candidates listed rather than one being guessed. A range spends one request's worth of the key's rate-limit allowance per day, and a single date must now be a real calendar date written YYYY-MM-DD and near today - 2026-02-31 used to roll over and quietly return another day's times./api/v1/booking/*. GET /api/v1/booking/service-types (service-types:read) returns the published price list, with hasVariablePricing flagging services whose real price depends on quantity or options. GET /api/v1/booking/estimates (booking-estimates:read) returns the quotes belonging to the one contact a conversation is already with - totals and validity only, no line items and deliberately no title, addressed only by the CRM contact id. POST /api/v1/booking/appointments, /cancel, and /reschedule (booking-appointments:read / booking-appointments:write, 5.8.2) let a booking assistant handle one verified caller's own appointment: the caller proves who they are with the contact id or two matching details (email, E.164 phone, last name), and every wrong combination gets the same 404 on purpose, so the surface cannot be used to confirm a customer's details one request at a time. A recurring booking is refused and the sub-account's admins are notified, so a person decides. See the REST API reference for the full family./api/v1/ext. An installed add-on can publish endpoints of its own under /api/v1/ext/, using the same API keys, rate limits, and response envelope as the rest of the API. GET /api/v1/ext lists exactly which endpoints your install offers, what each does, and which scope it needs - that listing is the documentation for them, since the base product cannot describe an add-on's endpoints in advance. Contributed scopes appear on the create-key form beside the built-in ones, an add-on can never claim a core scope, and a contributed endpoint that fails does so on its own: 502/504 against that one endpoint, crash detail to your Convex logs, and never anything the add-on wrote in the error body beyond a capped message on the 4xx statuses it chose to return. With no add-ons installed, the list is empty.Cancelled task state. PATCH /api/v1/tasks/{id} accepts status: "cancelled". Deleting a contact cancels its open tasks rather than marking them completed, so task.updated webhooks fire with the cancelled status. Task-created and task-completed webhook events keep their existing shapes; there is no separate task.cancelled event.X-Forwarded-Host, so sign-in works on any proxied hostname.campaigns:write is selectable on new API keys and gates the campaign create, send, and schedule endpoints above. There is deliberately no campaigns:read scope - the campaigns REST surface has no GET endpoint, so a grantable scope that authorises nothing would be a promise the API cannot keep.These endpoints shipped earlier and have been live in the product and in the bundled OpenAPI spec, but this reference only described contacts, conversations, and calendars. That was a documentation gap, not a missing feature. The reference now covers them properly.
Opportunities (opportunities:read, opportunities:write)
GET /api/v1/opportunities - list opportunitiesGET /api/v1/opportunities/{id} - get an opportunityPOST /api/v1/opportunities - create an opportunityPATCH /api/v1/opportunities/{id} - update an opportunityPUT /api/v1/opportunities/{id}/stage - move to a different stagePUT /api/v1/opportunities/{id}/status - set statusDELETE /api/v1/opportunities/{id} - soft-delete an opportunityTasks (tasks:read, tasks:write)
GET /api/v1/tasks - list tasks, filter by status, assignedTo, contactId, dealId, dueBefore, dueAfterGET /api/v1/tasks/{id} - get a taskPOST /api/v1/tasks - create a taskPATCH /api/v1/tasks/{id} - update a taskPUT /api/v1/tasks/{id}/complete - mark a task completedDELETE /api/v1/tasks/{id} - delete a taskPipelines (opportunities:read)
GET /api/v1/pipelines - list pipelines and their ordered stagesThe scope table in the REST API reference has been corrected to list these. Two further scopes exist but grant no access to /api/v1/* and are intentionally left out. Record timestamps (createdAt, updatedAt) are ISO 8601 here as everywhere else; the scheduling fields (dueDate, completedAt, expectedCloseDate, closedAt) are epoch milliseconds. The reference now states which is which.
invoice.paid webhook event exists, but there are no invoice REST endpoints.X-Webhook-Signature-V2 on every outbound webhook delivery. It signs <timestamp>.<body> with your existing per-subscription secret, so a receiver can trust X-Webhook-Timestamp and reject a replayed delivery outside a freshness window. This is additive: X-Webhook-Signature is still sent, unchanged, and integrations that verify it keep working. See Webhook Payloads for the verification snippet.meta.total is the size of the full result set, not the number of records in the current page. If you were treating total as a page count, or computing a total by summing it across pages, correct that when you upgrade.The outbound webhook catalog has grown from the original 15 events to 26. The following events are now available to subscribe to, in addition to the launch set:
opportunity.updated, opportunity.deletedappointment.cancelled, appointment.rescheduled, appointment.confirmed, appointment.completed, appointment.no_showtask.created, task.updated, task.completed, task.deletedExisting subscriptions are unaffected. See the REST API and Webhook Payloads pages for the full list and payload shapes.
sk_agency_live_ (production) and sk_agency_test_ (sandbox). Requires X-Sub-Account-Id header on every request.GET /api/v1/sub-accounts - Discovery endpoint to list available sub-accounts. Only accessible with agency-scoped keys.MISSING_SUB_ACCOUNT (400), INVALID_SUB_ACCOUNT (403)Initial release of the public REST API.
Authentication:
sk_live_ (production) and sk_test_ (sandbox) prefixescontacts:read, contacts:write, conversations:read, conversations:write, calendars:read, calendars:write, webhooks:manageContacts:
GET /api/v1/contacts - list/search contacts, exact email lookup via email paramGET /api/v1/contacts/{id} - get contactPOST /api/v1/contacts - create contactPATCH /api/v1/contacts/{id} - update contactDELETE /api/v1/contacts/{id} - soft-delete contactGET /api/v1/contacts/fields - list custom field definitionsConversations:
GET /api/v1/conversations - list conversations, filter by contactIdGET /api/v1/conversations/{id} - get conversationPOST /api/v1/conversations - create or find existing conversationGET /api/v1/conversations/{id}/messages - list messages (cursor-based pagination)POST /api/v1/conversations/{id}/messages - send message (all channels)PATCH /api/v1/conversations/{id} - update status/assignmentCalendars:
GET /api/v1/calendars - list active calendarsGET /api/v1/calendars/types - list appointment typesGET /api/v1/calendars/availability - check available slotsGET /api/v1/calendars/appointments - list appointmentsPOST /api/v1/calendars/appointments - book appointmentDELETE /api/v1/calendars/appointments/{id} - cancel appointmentWebhooks:
GET /api/v1/webhooks - list subscriptionsPOST /api/v1/webhooks - create subscriptionPATCH /api/v1/webhooks/{id} - update subscriptionDELETE /api/v1/webhooks/{id} - delete subscriptionPOST /api/v1/webhooks/{id}/regenerate-secret - regenerate signing secretcontact.created, contact.updated, contact.lifecycle_changed, contact.tag_added, opportunity.created, opportunity.stage_changed, opportunity.won, opportunity.lost, message.received, message.sent, message.delivered, message.failed, appointment.booked, form.submitted, invoice.paidGeneral: