Seedly CRM
Help Center

REST API

Integrate with Seedly CRM using the REST API for chatbot, automation, and third-party system integrations.

Table of Contents

  1. System Overview
  2. Authentication
  3. Quick Start: Chatbot Integration
  4. Conversations API
  5. Contacts API
  6. Calendar & Booking API
  7. Webhooks (Real-Time Events)
  8. Workflow Triggers
  9. Pagination
  10. Error Examples
  11. Rate Limits & Best Practices
  12. Versioning & Stability
  13. Future Roadmap

1. System Overview

Architecture

┌──────────────────────┐
│  Your System         │
│  (Chatbot / CRM /    │
│   Automation Tool)   │
└──────────┬───────────┘

           │  Authorization: Bearer sk_live_...

┌──────────▼───────────┐
│  REST API            │
│  /api/v1/*           │
│                      │
│  Contacts, Messages, │
│  Calendars, Webhooks │
└──────────┬───────────┘

┌──────────▼───────────┐     ┌─────────────────────┐
│  Seedly CRM Backend  │────▶│  Messaging Providers │
│                      │     │                      │
│  Business logic,     │     │  SMS, Email, Gmail,  │
│  automation engine,  │     │  Messenger, Instagram│
│  event dispatch      │     │                      │
└──────────┬───────────┘     └─────────────────────┘

           │  Outbound Webhooks (HTTPS POST)

┌──────────▼───────────┐
│  Your Webhook Server │
│  (receives events)   │
└──────────────────────┘

Key Concepts

  • Sub-Account: Each customer or brand operates within a sub-account. All API calls are scoped to a single sub-account via the API key.
  • Channels: Conversations span multiple channels -- sms, email, messenger, instagram, gmail, live_chat, social_dm.
  • Contacts: The central entity. Every conversation, appointment, and opportunity is linked to a contact.
  • Workflows: Automation sequences triggered by events. External systems can trigger workflows and receive event notifications.

Base URL

https://{YOUR_DEPLOYMENT_URL}

Your deployment URL is found in the Seedly CRM dashboard under Settings.

Response Format

All REST API responses use a consistent JSON envelope:

Success:

{
  "data": { ... },
  "meta": { "total": 100, "hasMore": true, "cursor": "..." }
}

Error:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "firstName and lastName are required"
  }
}

2. Authentication

API Keys

The REST API uses API key authentication. Each key is scoped to a single sub-account and has configurable permissions.

Header format:

Authorization: Bearer sk_live_...

Your customer (the sub-account admin) creates the API key in their Seedly CRM dashboard and shares it with you.

CORS warning: API key endpoints are server-to-server only. Do not embed API keys in client-side JavaScript.

Obtaining an API Key

  1. Navigate to Settings > Integrations in the Seedly CRM dashboard
  2. Click the API Keys card
  3. Click Create Key, enter a name, and select the permission scopes you need
  4. Copy the key immediately -- it is shown once and cannot be retrieved later

Available Scopes

ScopeGrants access to
contacts:readList and get contacts
contacts:writeCreate, update, and delete contacts
conversations:readList conversations and read messages
conversations:writeSend messages, update conversation status
calendars:readList calendars, appointment types, appointments, check availability
calendars:writeBook and cancel appointments
webhooks:manageCreate and manage webhook subscriptions

Recommended scopes for a chatbot integration: contacts:read, contacts:write, conversations:read, conversations:write, calendars:read.

Agency-scoped keys use the same scopes. The key operates on whichever sub-account is specified via the X-Sub-Account-Id header.

Key Lifecycle

  • Active -- Key is valid and accepting requests
  • Expired -- Key has passed its optional expiration date (returns 401 KEY_EXPIRED)
  • Revoked -- Key was manually revoked (returns 401 KEY_REVOKED)

Keys can be revoked at any time from the settings UI. Revocation is immediate.

Agency-Scoped API Keys

Agency-scoped API keys work across all sub-accounts under an agency, rather than being tied to a single sub-account. They use the prefixes sk_agency_live_ (production) or sk_agency_test_ (sandbox).

Because an agency-scoped key can access any sub-account, every request must include the X-Sub-Account-Id header to specify which sub-account the call should operate on:

Authorization: Bearer sk_agency_live_...
X-Sub-Account-Id: sub_abc123

Omitting the header returns a 400 error:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "X-Sub-Account-Id header is required for agency-scoped API keys"
  }
}

Discovering Sub-Account IDs

Use the discovery endpoint to list all sub-accounts available to the agency key:

GET /api/v1/sub-accounts
Authorization: Bearer sk_agency_live_...

Response:

{
  "data": [
    { "id": "sub_abc123", "name": "Acme Corp" },
    { "id": "sub_def456", "name": "Widget Co" }
  ]
}

Note: This endpoint requires an agency-scoped key. Sub-account-scoped keys (sk_live_) cannot call it.

Typical Integration Flow

  1. Agency owner creates an agency-scoped API key in Settings > Integrations > API Keys and selects "Agency-wide" scope.
  2. Discover sub-accounts by calling GET /api/v1/sub-accounts to retrieve the list of sub-account IDs.
  3. Include X-Sub-Account-Id on all subsequent calls to target the desired sub-account.
# Step 2 - Discover sub-accounts
GET /api/v1/sub-accounts
Authorization: Bearer sk_agency_live_...

# Step 3 - List contacts for a specific sub-account
GET /api/v1/contacts
Authorization: Bearer sk_agency_live_...
X-Sub-Account-Id: sub_abc123

Backward compatibility: Sub-account-scoped keys (sk_live_, sk_test_) continue to work exactly as before. They do not require the X-Sub-Account-Id header -- the sub-account is implicit in the key itself.

Public Endpoints (No Key Required)

Some endpoints are public and do not require an API key:

CapabilityAuth
Check calendar availabilityNone
Book appointments (public booking flow)None
Live chat widgetRate-limited only
Form submissionsRate-limited, optional CAPTCHA
Inbound workflow triggers (/webhooks/workflow/{slug})Optional HMAC signature

3. Quick Start: Chatbot Integration

This walkthrough shows how to build a chatbot integration that receives messages, processes them, and sends replies.

Flow

1. Receive webhook  ──▶  2. Look up contact  ──▶  3. Get conversation context
       │                                                      │
       │                                                      ▼
6. Track delivery   ◀──  5. Send reply        ◀──  4. Process with AI/logic

Step-by-Step

Step 1 -- Receive message.received webhook event

Configure a webhook subscription (see Webhooks) to listen for message.received. When a message arrives, your server receives:

{
  "event": "message.received",
  "timestamp": "2026-03-27T14:30:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "contact_abc123",
    "messageId": "msg_abc123",
    "conversationId": "conv_xyz789",
    "channel": "email",
    "bodyText": "Hi, I'd like to book an appointment",
    "fromAddress": "[email protected]",
    "direction": "inbound",
    "tags": ["website-lead"]
  }
}

Step 2 -- Look up the contact

GET /api/v1/[email protected]
Authorization: Bearer sk_live_...

This returns an array with the matching contact (or an empty array if not found).

Step 3 -- Get conversation messages for context

GET /api/v1/conversations/{conversationId}/messages?limit=20
Authorization: Bearer sk_live_...

Retrieve recent messages to provide conversation history to your AI or processing logic.

Step 4 -- Process with your AI/logic

Pass the conversation history and the new message to your chatbot engine, LLM, or business logic layer.

Step 5 -- Send reply

POST /api/v1/conversations/{conversationId}/messages
Authorization: Bearer sk_live_...
Content-Type: application/json

{
  "bodyText": "I'd be happy to help you book an appointment! What day works best?",
  "channel": "email",
  "toAddress": "[email protected]",
  "subject": "Re: Appointment Request"
}

Step 6 -- Track delivery

Listen for message.sent, message.delivered, and message.failed webhook events to confirm your reply was delivered successfully.


4. Conversations API

Data Model

Conversation:

FieldTypeDescription
idstringUnique identifier
contactIdstringLinked contact
channelstringsms, email, messenger, instagram, gmail, live_chat, social_dm
statusstringopen, closed, snoozed
assignedTostring?Assigned team member ID
subjectstring?Email subject line
unreadCountnumberUnread inbound messages
isArchivedbooleanWhether archived
lastMessageAtstring?Most recent message time (ISO 8601)
createdAtstringCreation time (ISO 8601)
contactobjectEmbedded contact summary (name, email, phone)

Message:

FieldTypeDescription
idstringUnique identifier
conversationIdstringParent conversation
channelstringChannel this message was sent/received on
directionstringinbound or outbound
bodyTextstring?Plain text content
bodyHtmlstring?HTML content (email)
statusstringpending, sent, delivered, read, failed, bounced
fromAddressstring?Sender (phone/email)
toAddressstring?Recipient (phone/email)
isInternalbooleanInternal note (not sent to contact)
createdAtstringCreation time (ISO 8601)

Endpoints

Create Conversation

POST /api/v1/conversations

Scope: conversations:write

Request body:

{
  "contactId": "contact_id_here",
  "channel": "email",
  "subject": "Welcome to our service"
}
FieldRequiredDescription
contactIdYesContact to associate the conversation with
channelYessms, email, messenger, instagram, gmail, live_chat, social_dm
subjectNoSubject line (email only)

Response:

  • 201 Created -- New conversation created. Response includes "created": true.
  • 200 OK -- An existing open conversation was found for this contact and channel. Response includes "created": false.
{
  "data": {
    "id": "conv_xyz789",
    "contactId": "contact_id_here",
    "channel": "email",
    "status": "open",
    "created": true
  }
}

List Conversations

GET /api/v1/conversations

Scope: conversations:read

Query parameters:

  • status -- open, closed, snoozed
  • channel -- Filter by channel type
  • contactId -- Filter by contact ID to find all conversations for a specific contact
  • assignedTo -- Filter by team member ID
  • limit -- Max results (default 50, max 200)

Response: Array of conversations with embedded contact summary.

Get Conversation

GET /api/v1/conversations/{id}

Scope: conversations:read

List Messages

GET /api/v1/conversations/{id}/messages

Scope: conversations:read

Query parameters:

  • limit -- Max results (default 50, max 100)
  • cursor -- Pagination cursor from previous response

Response:

{
  "data": [
    /* messages, newest first */
  ],
  "meta": { "hasMore": true, "cursor": "..." }
}

Send Message

POST /api/v1/conversations/{id}/messages

Scope: conversations:write

Request body:

{
  "bodyText": "Hello {{firstName}}, your appointment is confirmed.",
  "channel": "sms",
  "toAddress": "+15551234567",
  "subject": "Appointment Confirmation",
  "bodyHtml": "<p>Hello...</p>"
}
FieldRequiredDescription
bodyTextYesMessage content (plain text)
channelYessms, email, gmail, messenger, instagram, social_dm
toAddressFor email/SMSRecipient address (derived from contact if omitted)
subjectFor emailEmail subject line
bodyHtmlNoHTML content (email only)

Channel routing: The system automatically sends through the configured provider for the channel (e.g., SMS goes via the configured SMS provider, email via the configured email provider).

Merge fields: {{firstName}}, {{lastName}}, {{email}}, {{phone}}, {{company}}, {{fullName}}, and {{custom_values.key}} are resolved at send time.

DND enforcement: If the contact has opted out of a channel, the send will be silently skipped.

Update Conversation

PATCH /api/v1/conversations/{id}

Scope: conversations:write

Request body:

{
  "status": "closed",
  "assignedTo": "user_id_here"
}

Both fields are optional. Set assignedTo to null to unassign.

Live Chat (Public, No Key Required)

EndpointMethodDescription
/api/chat/config?subAccountId={id}GETGet widget configuration
/api/chat/sessionPOSTCreate chat session. Body: { subAccountId, visitorName, visitorEmail }
/api/chat/messagePOSTSend message. Body: { sessionId, message }
/api/chat/messages?sessionId={id}GETFetch session messages

5. Contacts API

Data Model

FieldTypeDescription
idstringUnique identifier
firstNamestringFirst name
lastNamestringLast name
emailstring?Primary email
phonestring?Primary phone (E.164 format: +15551234567)
companystring?Company name
titlestring?Job title
sourcestring?Lead source
tagsstring[]Tag labels
lifecycleStagestringlead, qualified, customer, repeat, inactive
engagementScorenumber?Computed score (0-100) -- read-only
assignedTostring?Assigned team member ID
additionalEmailsstring[]Secondary email addresses
additionalPhonesstring[]Secondary phone numbers
addressLine1string?Street address
citystring?City
statestring?State/province
postalCodestring?Postal/ZIP code
countrystring?Country
dateOfBirthstring?Date of birth
customFieldsobject?Key-value custom field data
createdAtstringISO 8601 timestamp -- read-only
updatedAtstringISO 8601 timestamp -- read-only

Endpoints

List Contacts

GET /api/v1/contacts

Scope: contacts:read

Query parameters:

  • search -- Full-text search (name, email, company)
  • email -- Exact email match. Returns array with single contact or empty array. More efficient than search for programmatic lookups.
  • limit -- Max results (default 50, max 100)
  • cursor -- Pagination cursor
  • lifecycleStage -- Filter by stage
  • source -- Filter by lead source
  • tags -- Comma-separated tag filter (e.g., tags=vip,enterprise)
  • assignedTo -- Filter by team member ID
  • hasPhone -- true to only return contacts with phone numbers

Response:

{
  "data": [
    /* contacts */
  ],
  "meta": { "total": 150, "hasMore": true, "cursor": "..." }
}

Get Contact

GET /api/v1/contacts/{id}

Scope: contacts:read

Get Custom Field Definitions

GET /api/v1/contacts/fields

Scope: contacts:read

Returns the list of custom field definitions configured for the sub-account.

Response:

{
  "data": [
    {
      "name": "preferred_language",
      "label": "Preferred Language",
      "type": "select",
      "options": ["English", "Spanish", "French"],
      "isRequired": false
    },
    {
      "name": "company_size",
      "label": "Company Size",
      "type": "number",
      "options": null,
      "isRequired": true
    }
  ]
}

Each field definition includes:

FieldTypeDescription
namestringField key (used in customFields object on contacts)
labelstringHuman-readable label
typestringtext, number, select, date, boolean, url, email, phone
optionsstring[]?Available options (for select type only)
isRequiredbooleanWhether the field is required when creating/updating contacts

Create Contact

POST /api/v1/contacts

Scope: contacts:write

Request body:

{
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "[email protected]",
  "phone": "+15551234567",
  "company": "Acme Corp",
  "lifecycleStage": "lead",
  "tags": ["website-lead"],
  "source": "api"
}
FieldRequiredDescription
firstNameYesFirst name
lastNameYesLast name
All other fieldsNoSee data model above

Side effects:

  • Fires contact.created webhook event
  • Triggers matching workflow automations
  • Validates custom field schemas if provided

Response: 201 Created with the full contact object.

Update Contact

PATCH /api/v1/contacts/{id}

Scope: contacts:write

Include only the fields you want to change.

{
  "lifecycleStage": "customer",
  "tags": ["vip", "enterprise"]
}

Side effects:

  • Fires contact.updated webhook event
  • If tags added: fires contact.tag_added event per new tag
  • If lifecycleStage changed: fires contact.lifecycle_changed event

Delete Contact

DELETE /api/v1/contacts/{id}

Scope: contacts:write

Soft-deletes the contact. Data is retained before permanent deletion.

Cascading effects:

  • Related conversations are soft-deleted
  • Upcoming appointments are cancelled
  • Running workflow automations for this contact are cancelled

6. Calendar & Booking API

Data Model

Appointment:

FieldTypeDescription
idstringUnique identifier
calendarIdstringCalendar
appointmentTypeIdstring?Type of appointment
contactIdstring?Linked contact
titlestring?Appointment title
startTimestringStart time (ISO 8601)
endTimestringEnd time (ISO 8601)
statusstringscheduled, confirmed, completed, cancelled, no_show
assignedTostring?Assigned team member
notesstring?Appointment notes
createdAtstringCreation time (ISO 8601)
contactobject?Embedded contact summary

Endpoints

List Calendars

GET /api/v1/calendars

Scope: calendars:read

Returns all active calendars for the sub-account.

Response:

{
  "data": [
    {
      "id": "cal_abc123",
      "name": "Main Calendar",
      "type": "personal",
      "timezone": "America/New_York"
    }
  ]
}

List Appointment Types

GET /api/v1/calendars/types

Scope: calendars:read

Query parameters:

  • calendarId -- Optionally filter by a specific calendar

Response:

{
  "data": [
    {
      "id": "apt_type_001",
      "calendarId": "cal_abc123",
      "name": "30-Minute Consultation",
      "duration": 30,
      "slug": "30-min-consultation"
    }
  ]
}

List Appointments

GET /api/v1/calendars/appointments

Scope: calendars:read

Query parameters:

  • calendarId -- Filter by calendar
  • startDate -- Start of date range (Unix timestamp, milliseconds)
  • endDate -- End of date range (Unix timestamp, milliseconds)
  • status -- scheduled, confirmed, completed, cancelled, no_show
  • limit -- Max results (default 50, max 200)

Check Availability

GET /api/v1/calendars/availability?appointmentTypeId={id}&date=2026-04-01

Scope: calendars:read

Query parameters:

  • appointmentTypeId -- Required
  • date -- Date in YYYY-MM-DD format

Response:

{
  "data": [
    { "start": "2026-04-01T09:00:00-04:00", "end": "2026-04-01T09:30:00-04:00" },
    {
      "start": "2026-04-01T09:30:00-04:00",
      "end": "2026-04-01T10:00:00-04:00",
      "seatsRemaining": 3
    }
  ]
}

Availability accounts for: day-of-week schedules, date overrides (blocked days), existing bookings, buffer times, minimum notice, maximum advance, and seat limits.

Book Appointment

POST /api/v1/calendars/appointments

Scope: calendars:write

Request body:

{
  "appointmentTypeId": "...",
  "startTime": 1743508800000,
  "endTime": 1743510600000,
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "[email protected]",
  "phone": "+15551234567",
  "notes": "First consultation"
}
FieldRequiredDescription
appointmentTypeIdYesType of appointment to book
startTimeYesStart time (Unix timestamp, milliseconds)
endTimeYesEnd time (Unix timestamp, milliseconds)
firstNameYesBooker's first name
lastNameYesBooker's last name
emailYesBooker's email
phoneNoBooker's phone
notesNoAdditional notes

Behavior:

  • Creates or finds a contact by email
  • Validates slot availability and conflicts
  • Assigns based on calendar type (personal, round-robin, collective, class)
  • Fires appointment.booked webhook event
  • Sends confirmation email if configured

Cancel Appointment

DELETE /api/v1/calendars/appointments/{id}

Scope: calendars:write

Query parameters:

  • reason -- Optional cancellation reason (e.g., ?reason=Customer%20requested)

Response: 200 OK

{
  "data": {
    "id": "appt_abc123",
    "cancelled": true
  }
}

Public Booking (No Key Required)

The same availability check and booking flow is also available without an API key via the public booking page widget.


7. Webhooks (Real-Time Events)

Overview

Seedly CRM pushes real-time event notifications to your server via HTTPS POST. Events are cryptographically signed, retried on failure, and logged for debugging.

Managing Subscriptions

Webhook subscriptions can be managed via the REST API or the settings UI.

List Subscriptions

GET /api/v1/webhooks

Scope: webhooks:manage

Create Subscription

POST /api/v1/webhooks

Scope: webhooks:manage

Request body:

{
  "url": "https://your-server.com/webhooks/seedly",
  "events": [
    "contact.created",
    "message.received",
    "message.sent",
    "message.delivered",
    "message.failed",
    "appointment.booked"
  ],
  "description": "Chatbot integration"
}

Response: Returns the subscription with a signing secret. Store this securely -- it is shown once.

Update Subscription

PATCH /api/v1/webhooks/{id}

Scope: webhooks:manage

Delete Subscription

DELETE /api/v1/webhooks/{id}

Scope: webhooks:manage

Regenerate Signing Secret

POST /api/v1/webhooks/{id}/regenerate-secret

Scope: webhooks:manage

Regenerates the signing secret for a webhook subscription. The old secret is immediately invalidated.

Response:

{
  "data": {
    "secret": "whsec_new_plaintext_secret_here"
  }
}

Store the new secret securely -- it is shown once. Update your webhook verification code with the new secret immediately.

Available Events

Event NameFires when...
contact.createdNew contact created
contact.updatedContact record updated
contact.lifecycle_changedLifecycle stage changed
contact.tag_addedTag added to contact
opportunity.createdNew opportunity created
opportunity.updatedOpportunity record updated
opportunity.stage_changedOpportunity pipeline stage changed
opportunity.wonOpportunity marked as won
opportunity.lostOpportunity marked as lost
opportunity.deletedOpportunity deleted
message.receivedInbound message received (any channel)
message.sentOutbound message sent to provider
message.deliveredOutbound message confirmed delivered
message.failedOutbound message delivery failed
appointment.bookedNew appointment booked
appointment.cancelledAppointment cancelled
appointment.rescheduledAppointment rescheduled
appointment.confirmedAppointment confirmed
appointment.completedAppointment marked completed
appointment.no_showAppointment marked no-show
form.submittedPublic form submission received
invoice.paidInvoice payment received
task.createdNew task created
task.updatedTask record updated
task.completedTask marked completed
task.deletedTask deleted

Payload Format

{
  "event": "contact.created",
  "timestamp": "2026-03-27T14:30:00.000Z",
  "data": {
    "subAccountId": "sa_001",
    "contactId": "abc123",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "[email protected]",
    "tags": ["website-lead"]
  }
}

Common fields in data:

  • subAccountId -- Always present. Identifies which sub-account the event belongs to.
  • contactId -- Present when the event is associated with a specific contact.
  • tags -- Present when the associated contact has tags.

See Webhook Payloads for complete payload examples for all 26 events.

Request Headers

HeaderValue
Content-Typeapplication/json
X-Webhook-Signaturesha256={hex} -- HMAC-SHA256 of the raw body
X-Webhook-EventEvent name
X-Webhook-DeliveryUnique delivery ID (use as idempotency key)
User-AgentSeedly-CRM-Webhook/1.0

Verifying Signatures

Always verify the X-Webhook-Signature header using timing-safe comparison.

Node.js example:

const crypto = require('crypto');

function verifySignature(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}

Retry Policy

If your endpoint does not return a success response, the delivery is retried automatically several times with an increasing back-off between attempts. After the retries are exhausted, the delivery is marked as failed. Delivery logs are retained and can be reviewed in the dashboard.

Your server must respond with a 2xx status within a reasonable time window to be considered successful.

Security Notes

  • Webhook URLs pointing to private or reserved IP ranges are blocked (SSRF protection)
  • Always verify signatures before processing events
  • Use the X-Webhook-Delivery header for idempotency -- your server may receive duplicate deliveries

8. Workflow Triggers

Overview

Workflows are automation sequences that execute when events occur. External systems can trigger workflows via HTTP and inject custom events into running executions.

Workflow triggers use the /webhooks/workflow/{slug} endpoint with optional HMAC signature, not API key authentication.

Trigger a Workflow

Each workflow with an "Inbound Webhook" trigger type has a unique URL:

POST https://{YOUR_DEPLOYMENT_URL}/webhooks/workflow/{slug}

Request body:

{
  "contactId": "optional_contact_id",
  "email": "[email protected]",
  "customData": "any data your workflow needs"
}

Optional header: X-Webhook-Signature: sha256={hex} -- HMAC-SHA256 of the body using the workflow's signing secret. If the workflow has a secret configured, the signature is required.

Response:

{ "ok": true, "triggered": 1 }

Inject Events into Running Workflows

Running workflows can wait for custom events at "goal" nodes. Inject events with:

POST https://{YOUR_DEPLOYMENT_URL}/webhooks/event/{slug}

Request body:

{
  "eventType": "payment_completed",
  "contactId": "optional_contact_id",
  "data": { "amount": 99.99 }
}

Rate Limits

Both workflow endpoints are rate-limited on a per-endpoint basis.


9. Pagination

The API uses cursor-based pagination. Cursors are opaque strings -- do not parse or construct them.

Walkthrough

First request -- no cursor:

GET /api/v1/contacts?limit=50
Authorization: Bearer sk_live_...

Response:

{
  "data": [
    /* first 50 contacts */
  ],
  "meta": {
    "total": 150,
    "hasMore": true,
    "cursor": "eyJpZCI6ImNvbnRhY3RfMDUwIn0"
  }
}

Read the cursor from meta.cursor and pass it in the next request:

GET /api/v1/contacts?limit=50&cursor=eyJpZCI6ImNvbnRhY3RfMDUwIn0
Authorization: Bearer sk_live_...

Response:

{
  "data": [
    /* next 50 contacts */
  ],
  "meta": {
    "total": 150,
    "hasMore": true,
    "cursor": "eyJpZCI6ImNvbnRhY3RfMTAwIn0"
  }
}

Continue until hasMore is false:

{
  "data": [
    /* final 50 contacts */
  ],
  "meta": {
    "total": 150,
    "hasMore": false,
    "cursor": null
  }
}

Tips

  • Cursors are stable even when data changes between requests (unlike offset-based pagination).
  • Do not store cursors long-term -- they may expire. Use them immediately for sequential page fetching.
  • The total field reflects the total count at the time of the query and may change between pages.

10. Error Examples

401 Unauthorized -- Missing or invalid API key

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid API key. Provide a valid key in the Authorization header: Bearer sk_live_..."
  }
}

400 Validation Error -- Invalid request body

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "firstName and lastName are required"
  }
}

403 Forbidden -- Missing required scope

{
  "error": {
    "code": "FORBIDDEN",
    "message": "API key missing required scope: contacts:write. Update key permissions in Settings > Integrations > API Keys."
  }
}

429 Rate Limited -- Too many requests

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry after the period indicated in the Retry-After header."
  }
}

Response headers:

Retry-After: 12
X-RateLimit-Limit: ...
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1743091200

11. Rate Limits & Best Practices

Rate Limits

Rate limits are applied per API key. Read operations have higher limits than write operations. Rate limit headers are included in every response so your integration can monitor usage and avoid exceeding limits:

X-RateLimit-Limit: ...
X-RateLimit-Remaining: ...
X-RateLimit-Reset: ...

When a limit is exceeded, the API returns HTTP 429 with a Retry-After header indicating how many seconds to wait before retrying.

Workflow trigger and event injection endpoints are rate-limited on a per-endpoint basis. Public endpoints such as live chat and form submissions are rate-limited by IP address.

For high-volume integrations requiring higher rate limits, contact the Seedly CRM team.

Server-side only: All API key endpoints are designed for server-to-server use. Do not call the REST API from client-side JavaScript -- API keys would be exposed.

Best Practices

  1. Verify webhook signatures -- Always use timing-safe comparison. Never skip verification in production.

  2. Use idempotency keys -- Use X-Webhook-Delivery to deduplicate webhook events. Track message IDs to prevent duplicate sends.

  3. Handle retries gracefully -- Respond to webhooks promptly. Return 200 even for events you don't process. Do heavy work asynchronously.

  4. Use cursor-based pagination -- Use cursor from the meta object, not offset-based page numbers. Cursors are stable when data changes between requests.

  5. Respect DND -- The system enforces Do-Not-Disturb server-side, but checking DND status before sending prevents silent failures.

  6. Use merge fields -- Use {{firstName}}, {{email}}, etc. in message bodies. The system resolves them at send time using current contact data.

  7. Channel constraints:

    • SMS: Phone numbers must be E.164 format (+1XXXXXXXXXX). Messages over 160 characters are segmented.
    • Email: Provide both bodyText and bodyHtml for maximum compatibility.
    • Messenger/Instagram: Subject to Meta's 24-hour messaging window policy.

Error Codes

StatusCodeMeaning
200--Success
201--Created
400VALIDATION_ERRORInvalid request parameters
401UNAUTHORIZEDMissing or invalid API key
401KEY_EXPIREDAPI key has expired
401KEY_REVOKEDAPI key was revoked
403FORBIDDENAPI key missing required scope
404NOT_FOUNDResource not found
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORServer error -- retry with backoff

12. Versioning & Stability

Current Version

All endpoints are served under /api/v1/. The X-API-Version: v1 header is included in every response.

Backwards Compatibility

We will not make breaking changes to v1 without a deprecation period:

  • Not breaking (may happen at any time): New fields in responses, new optional query parameters, new endpoints, new webhook events, new error codes
  • Breaking (requires deprecation): Removing response fields, changing field types, removing endpoints, changing endpoint behavior, renaming fields

Deprecation Process

  1. A Deprecation: true response header is added to affected endpoints (minimum 90 days before removal)
  2. A changelog entry is published in the API Changelog
  3. When v2 is introduced, v1 will remain supported for at least 12 months

Recommendations

  • Parse responses tolerantly -- ignore unknown fields
  • Do not depend on field ordering in JSON responses
  • Check the API Changelog periodically for updates

13. Future Roadmap

FeatureStatusDescription
Webhook event filteringPlannedFilter subscriptions by criteria (e.g., only message.received on SMS channel)
Batch APIPlannedCreate/update multiple records in a single call
OAuth 2.0 providerFutureAuthorization code flow for marketplace app integrations

Full Endpoint Reference

REST API (Requires API Key)

MethodPathScopeDescription
GET/api/v1/contactscontacts:readList/search contacts (supports email query param for exact match)
GET/api/v1/contacts/{id}contacts:readGet contact
GET/api/v1/contacts/fieldscontacts:readGet custom field definitions
POST/api/v1/contactscontacts:writeCreate contact
PATCH/api/v1/contacts/{id}contacts:writeUpdate contact
DELETE/api/v1/contacts/{id}contacts:writeDelete contact
GET/api/v1/conversationsconversations:readList conversations (supports contactId query param)
GET/api/v1/conversations/{id}conversations:readGet conversation
POST/api/v1/conversationsconversations:writeCreate conversation
GET/api/v1/conversations/{id}/messagesconversations:readList messages
POST/api/v1/conversations/{id}/messagesconversations:writeSend message
PATCH/api/v1/conversations/{id}conversations:writeUpdate status/assignment
GET/api/v1/calendarscalendars:readList active calendars
GET/api/v1/calendars/typescalendars:readList appointment types (optional calendarId filter)
GET/api/v1/calendars/availabilitycalendars:readCheck available slots
GET/api/v1/calendars/appointmentscalendars:readList appointments
POST/api/v1/calendars/appointmentscalendars:writeBook appointment
DELETE/api/v1/calendars/appointments/{id}calendars:writeCancel appointment
GET/api/v1/webhookswebhooks:manageList webhook subscriptions
POST/api/v1/webhookswebhooks:manageCreate subscription
PATCH/api/v1/webhooks/{id}webhooks:manageUpdate subscription
DELETE/api/v1/webhooks/{id}webhooks:manageDelete subscription
POST/api/v1/webhooks/{id}/regenerate-secretwebhooks:manageRegenerate signing secret
GET/api/v1/sub-accountsAgency-scoped key requiredList all sub-accounts under the agency

Public Endpoints (No Key Required)

MethodPathDescription
GET/api/chat/configLive chat widget config
POST/api/chat/sessionCreate chat session
POST/api/chat/messageSend chat message
GET/api/chat/messagesGet chat messages
POST/api/forms/submitSubmit public form
POST/webhooks/workflow/{slug}Trigger workflow
POST/webhooks/event/{slug}Inject workflow event

This document is intended for engineering teams building integrations with Seedly CRM. For questions, contact the Seedly CRM team.

On this page

Table of Contents1. System OverviewArchitectureKey ConceptsBase URLResponse Format2. AuthenticationAPI KeysObtaining an API KeyAvailable ScopesKey LifecycleAgency-Scoped API KeysDiscovering Sub-Account IDsTypical Integration FlowPublic Endpoints (No Key Required)3. Quick Start: Chatbot IntegrationFlowStep-by-Step4. Conversations APIData ModelEndpointsCreate ConversationList ConversationsGet ConversationList MessagesSend MessageUpdate ConversationLive Chat (Public, No Key Required)5. Contacts APIData ModelEndpointsList ContactsGet ContactGet Custom Field DefinitionsCreate ContactUpdate ContactDelete Contact6. Calendar & Booking APIData ModelEndpointsList CalendarsList Appointment TypesList AppointmentsCheck AvailabilityBook AppointmentCancel AppointmentPublic Booking (No Key Required)7. Webhooks (Real-Time Events)OverviewManaging SubscriptionsList SubscriptionsCreate SubscriptionUpdate SubscriptionDelete SubscriptionRegenerate Signing SecretAvailable EventsPayload FormatRequest HeadersVerifying SignaturesRetry PolicySecurity Notes8. Workflow TriggersOverviewTrigger a WorkflowInject Events into Running WorkflowsRate Limits9. PaginationWalkthroughTips10. Error Examples401 Unauthorized -- Missing or invalid API key400 Validation Error -- Invalid request body403 Forbidden -- Missing required scope429 Rate Limited -- Too many requests11. Rate Limits & Best PracticesRate LimitsBest PracticesError Codes12. Versioning & StabilityCurrent VersionBackwards CompatibilityDeprecation ProcessRecommendations13. Future RoadmapFull Endpoint ReferenceREST API (Requires API Key)Public Endpoints (No Key Required)