Developers

Integrate Sellio over HTTP or connect your AI agent

A simple REST API and a native MCP server, with the same key, the same tenant scope and the same rate limit. Discover any object's fields and start reading and writing records in minutes.

Quickstart

From zero to your first call in three steps:

  1. In the CRM, open Settings → API & developers and generate an API key. Store it safely; it is not shown again.
  2. Use the key in the Authorization: Bearer header. It identifies the owning tenant, so everything is already scoped to it.
  3. Make your first call and list your objects.
curl -H "Authorization: Bearer sk_live_..." \
  https://www.selliocrm.com/api/v1/objects

Authentication

Every request carries an API key in the Authorization: Bearer header. That key identifies the owning tenant, so everything is automatically scoped to it, with RLS and RBAC. Never expose the key in the browser; use it server-side only. You generate and revoke keys in Settings → API & developers.

Key scopes

When you generate a key you choose its scope and give each integration only the access it needs. The limits apply the same way in REST and MCP, because both surfaces use the same key and the same control. A key with no scope set (the default) has full read and write access to all objects, as before.

  • Read only: the key reads data but cannot create, edit or delete. It blocks POST, PATCH and DELETE in REST and the create_record, update_record and delete_record tools in MCP, with 403 and { "error": "errors.apiKeyReadOnly" }.
  • Limited to objects: choose the objects the key may access. Any object outside the list returns 403 with { "error": "errors.apiKeyObjectDenied" }. The object list already comes filtered to the scope, and activities count as the activity object.
  • Allow deletion: by default a key NEVER deletes (not even with write enabled). Turn this on when generating the key to enable DELETE /{object}/{id} and the delete_record tool. Without it, deletion returns 403 with { "error": "errors.apiKeyDeleteDenied" }. Deletion is soft: records go to the trash and can be restored.

REST API v1

Records of any object, sending and receiving JSON. {object} is the apiName (for example contact, lead, opportunity) and {id} is the record uuid. Custom objects use the same generic endpoints.

GET/objectsList the tenant objects.
GET/objects/{apiName}Object fields (discovery).
GET/{object}List and search records (rich filters, sorting, pagination).
GET/{object}/{id}One record by id.
POST/{object}Create a record.
POST/{object}/bulkBulk create (up to 500; partial success per item).
PATCH/{object}/{id}Update fields (partial).
PATCH/{object}/bulkBulk update ({ updates: [{ id, ...fields }] }).
DELETE/{object}/{id}Delete (soft → trash). Requires the delete scope on the key.
GET/data/{object}Flattened records as rows (BI/automation).
GET/activitiesList activities (use ?recordId for one record).
POST/activitiesCreate an activity linked to a record.

List parameters (GET /{object}): limit (1 to 500, default 50), offset (pagination offset, default 0; the total is in the total field), search (search by name, accent and case insensitive), order (field with :asc or :desc) and filter. Rich filters: repeat filter=field:operator:value to combine several (e.g. filter=amount:gte:1000&filter=stage:eq:won). Operators: eq, neq, contains, gt, lt, gte, lte, empty, not_empty (empty and not_empty take no value). Filters combine with logical AND (there is no OR) and up to 12 per request. The old form filter=field:value (no operator) still means exact equality.

# List (limit, offset, search, filter, order)
curl -H "Authorization: Bearer sk_live_..." \
  "https://www.selliocrm.com/api/v1/opportunity?limit=20&order=updated_at:desc&filter=stage:won"

# Rich filters: repeat filter=field:operator:value (AND between them)
curl -H "Authorization: Bearer sk_live_..." \
  "https://www.selliocrm.com/api/v1/opportunity?filter=amount:gte:1000&filter=stage:eq:won&filter=name:contains:acme"

# One record
curl -H "Authorization: Bearer sk_live_..." \
  https://www.selliocrm.com/api/v1/contact/8f3c...

# Create
curl -X POST -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Ana Souza","email":"ana@acme.com","source":"site"}' \
  https://www.selliocrm.com/api/v1/contact

# Bulk create (up to 500; response with partial success per item)
curl -X POST -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"records":[{"name":"Ana"},{"name":"Bruno"}]}' \
  https://www.selliocrm.com/api/v1/contact/bulk
# → { "results": [ { "index": 0, "ok": true, "id": "..." },
#                  { "index": 1, "ok": false, "error": "..." } ],
#     "created": 1, "failed": 1 }

# Update (partial)
curl -X PATCH -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"source":"referral"}' \
  https://www.selliocrm.com/api/v1/contact/8f3c...

# Delete (soft, goes to trash; requires the delete scope on the key)
curl -X DELETE -H "Authorization: Bearer sk_live_..." \
  https://www.selliocrm.com/api/v1/contact/8f3c...

Object and field discovery

Before creating or updating a record, discover the object fields: what exists, what is required and which values a select accepts. No guessing, and no need to inspect an existing record. GET /objects/{apiName} returns the object and its fields, with required, options (for select), targetObject (for lookup), unique and readOnly (formula or rollup fields, which are not writable).

GET https://www.selliocrm.com/api/v1/objects/contact

# → {
#   "apiName": "contact",
#   "label": "Contact",
#   "fields": [
#     { "apiName": "name",  "label": "Name",  "type": "text",  "required": true },
#     { "apiName": "email", "label": "Email", "type": "email", "required": false },
#     { "apiName": "source", "label": "Source", "type": "select", "required": true,
#       "options": [ { "value": "site", "label": "Website" },
#                    { "value": "referral", "label": "Referral" } ] },
#     { "apiName": "company", "label": "Company", "type": "lookup",
#       "required": false, "targetObject": "company" }
#   ]
# }

In MCP, the describe_object tool returns the same fields to the agent.

Data for BI and automation

GET /data/{object} returns records flattened into stable tabular rows (fixed columns id, created_at, updated_at, owner_id, plus one per field), ordered by updated_at desc. It accepts pagination by page and pageSize (or limit and offset), the incremental filters updated_since and created_since (ISO 8601), and filter=field:value. Built for Power BI, Tableau, Looker and for polling from Zapier, Make and n8n.

curl -H "Authorization: Bearer sk_live_..." \
  "https://www.selliocrm.com/api/v1/data/opportunity?pageSize=100&updated_since=2026-01-01T00:00:00Z"

# → { "object": "opportunity",
#     "columns": [ { "name": "id", "type": "id" }, ... ],
#     "page": 1, "pageSize": 100, "total": 42,
#     "rows": [ { "id": "...", "created_at": "...", "amount": 1200, ... } ] }

MCP server for AI agents

Sellio exposes a native MCP (Model Context Protocol) server so your AI agent can read and write the CRM safely. It is the same API key, the same rate limit, the same validations and the same RBAC. The server URL is https://www.selliocrm.com/api/mcp. Available tools:

  • list_objects: List the tenant objects.
  • describe_object: One object's fields (required, types, options).
  • list_records: List and search records.
  • get_record: One record by id.
  • create_record: Create a record.
  • update_record: Update fields.
  • delete_record: Delete (soft → trash). Requires the delete scope on the key.

Clients that support remote MCP over HTTP use the URL with the Authorization: Bearer header. On clients without native remote HTTP, use the mcp-remote bridge:

{
  "mcpServers": {
    "sellio-crm": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://www.selliocrm.com/api/mcp",
        "--header", "Authorization: Bearer sk_live_..."
      ]
    }
  }
}

OAuth and connected apps

To integrate ON BEHALF OF THE USER (without the customer pasting an admin key), use the OAuth 2.0 Authorization Code flow with PKCE. The user sees a consent screen with the requested scopes, approves, and your app receives an access token that can only do what was consented. Apps are curated: register and publish the app in the marketplace to get a client_id and client_secret. The flow in four steps:

  1. Redirect the user to /api/oauth/authorize with client_id, redirect_uri (exact match against the app allowlist), scope, state and PKCE (code_challenge with code_challenge_method=S256). PKCE is required.
  2. The logged-in user sees the consent screen and approves. The CRM redirects back to your redirect_uri with code and the same state.
  3. On your server, exchange the code at POST /api/oauth/token (with code_verifier, client_id, client_secret and redirect_uri) for an access token (Bearer, about 1 hour) and a refresh token.
  4. Call the REST v1 API and the MCP server with Authorization: Bearer at_..., scoped to the consent. Renew with grant_type=refresh_token; the refresh is rotated (the old one dies on use) and reuse revokes the token family.
GET/api/oauth/authorizeUser consent (requires a session). Issues the code.
POST/api/oauth/tokenExchange code for tokens; and renew (refresh_token).
POST/api/oauth/revokeRevoke an access or refresh token.
# 1) Send the user to consent (PKCE S256 + state)
GET https://www.selliocrm.com/api/oauth/authorize
  ?response_type=code
  &client_id=app_...
  &redirect_uri=https://your-app.example.com/callback   # EXACT match from the allowlist
  &scope=records:read%20records:write:opportunity%20activities:read
  &state=<random>
  &code_challenge=<base64url(sha256(code_verifier))>
  &code_challenge_method=S256

# → the user approves → 302 redirect_uri?code=<authcode>&state=<...>

# 2) Exchange the code for tokens (server-side; send the client_secret)
curl -X POST https://www.selliocrm.com/api/oauth/token \
  -d grant_type=authorization_code \
  -d code=<authcode> \
  -d code_verifier=<code_verifier> \
  -d client_id=app_... \
  -d client_secret=secret_... \
  -d redirect_uri=https://your-app.example.com/callback

# → { "access_token": "at_...", "token_type": "Bearer",
#     "expires_in": 3600, "refresh_token": "rt_...",
#     "scope": "records:read records:write:opportunity activities:read" }

# 3) Call the v1 / MCP API with the access token (scoped to the consent)
curl -H "Authorization: Bearer at_..." https://www.selliocrm.com/api/v1/contact

# 4) Renew with the refresh token (rotated: the old rt_ dies on use)
curl -X POST https://www.selliocrm.com/api/oauth/token \
  -d grant_type=refresh_token -d refresh_token=rt_... \
  -d client_id=app_... -d client_secret=secret_...

OAuth scopes map to the same enforcement as keys (read, write and per object). Request the minimum needed; the user grants a subset on install and the token only operates within it.

  • records:read and records:write: read and write records of all objects (coarse).
  • records:read:contact and records:write:opportunity: per-object refinement (apiName), when the app needs only some objects.
  • activities:read and activities:write: read and write activities.
  • objects:read: discover the structure of objects and fields.

Embedded widgets

Your app can embed its own screen (a widget) inside the CRM: on a record detail or on the dashboard. The widget runs in a sandboxed iframe, served from your own origin (on the app allowlist). The host sends the widget only UI context over postMessage: recordId, objectApiName, locale and theme. No business data, token or secret travels in that message. To read or write data, your widget uses the OAuth API with its own token (the scopes the tenant granted at install).

  1. Declare the widget in your app (key, title, location record or dashboard and the embedUrl) and list the embedUrl origin on the app origin allowlist.
  2. In your widget, listen for host messages and accept only those from the host origin (event.origin). The host sends sellio:widget:context (version 1) with { recordId, objectApiName, locale, theme, installId }.
  3. Your widget may only send back sellio:widget:ready (to receive the context again) and sellio:widget:resize with { height } (to fit its height, capped). Any other message is ignored.
  4. For data, call the OAuth API (Bearer) with the app token. The host never hands a token or data over postMessage.
// Inside YOUR widget (the page at https://widgets.yourapp.com running in the iframe).
// The host sends ONLY UI context. For DATA, use the OAuth API with YOUR token.
const HOST = 'https://www.selliocrm.com'; // ALWAYS validate the host origin

window.addEventListener('message', (event) => {
  if (event.origin !== HOST) return;            // accept only the host origin
  const msg = event.data;
  if (!msg || msg.version !== 1) return;
  if (msg.type === 'sellio:widget:context') {
    const { recordId, objectApiName, locale, theme, installId } = msg.payload;
    render(recordId, objectApiName, locale, theme);
    // Fetch data with YOUR OAuth token (never comes via postMessage):
    // fetch(HOST + '/api/v1/' + objectApiName + '/' + recordId,
    //       { headers: { Authorization: 'Bearer ' + accessToken } })
  }
});

// Request the context on load and adjust the height to your content:
parent.postMessage({ type: 'sellio:widget:ready', version: 1 }, HOST);
parent.postMessage({ type: 'sellio:widget:resize', version: 1,
  payload: { height: document.body.scrollHeight } }, HOST);

Security: the iframe is sandboxed and cross-origin, so the widget cannot reach the host DOM or cookies. The host validates the exact origin on every postMessage (in and out) and the embedUrl is always checked against the app allowlist. Request the widgets:embed scope to place widgets.

Outbound webhooks

Receive CRM events in real time. Register a destination URL in Settings and pick the events you subscribe to. Each delivery is a POST with a JSON body, the x-sellio-event header with the event name, and the x-sellio-signature header with the HMAC-SHA256 signature of the body. The secret (whsec_...) is shown when you create the webhook.

Event catalog

record.createdrecord.updatedflow.enrolledflow.message_sentflow.repliedflow.step_completedflow.completed

Example payload

POST https://your-server.example.com/webhook
x-sellio-event: record.created
x-sellio-signature: sha256=<hmac-hex>
Content-Type: application/json

{
  "event": "record.created",
  "tenantId": "…",
  "objectApiName": "lead",
  "recordId": "8f3c…",
  "data": { "name": "Ana Souza", "email": "ana@acme.com" },
  "timestamp": "2026-07-23T12:00:00.000Z"
}

Campaign and flow events (flow.*) carry extra enrollment fields:

{
  "event": "flow.replied",
  "tenantId": "…",
  "flowId": "…",
  "flowName": "Outbound Q3",
  "recordId": "8f3c…",
  "enrollmentId": "…",
  "objectApiName": "lead",
  "data": { "name": "Ana Souza", "email": "ana@acme.com" },
  "meta": {},
  "timestamp": "2026-07-23T12:00:00.000Z"
}

How to verify the signature

Compute the HMAC-SHA256 of the RAW body (exactly as received, without reserializing) using your whsec_... secret, in the form sha256=<hex>, and compare it against the x-sellio-signature header in constant time. Reject when it does not match.

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody = the EXACT received body (string), without re-serializing.
function isValid(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signatureHeader || '');
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express: use express.raw({ type: 'application/json' }) to get the raw body.
app.post('/webhook', (req, res) => {
  const ok = isValid(req.body.toString('utf8'), req.header('x-sellio-signature'), process.env.SELLIO_WHSEC);
  if (!ok) return res.status(401).end();
  const event = req.header('x-sellio-event');
  // ... process the event
  res.status(200).end();
});

Rate limit

Each key has a per-minute limit (default 120, configurable). Responses include the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. On excess, the API returns 429 with the Retry-After header.

Error format

Errors return JSON in the form { "error": "message" } with the matching HTTP status:

{ "error": "required field: source" }   # HTTP 400
  • 200 and 201: success (201 on create).
  • 400: validation or business rule.
  • 401: missing or invalid key.
  • 403: no permission (RBAC) for the object, or blocked by the key scope (read only, object outside the scope, or deletion not enabled).
  • 404: object or record not found (including DELETE/PATCH of a nonexistent id).
  • 429: rate limit exceeded (see the Retry-After header).

Known limitations

  • Filters combine with logical AND (all true at once); there is no OR between filters.
  • Updates are partial (PATCH): you send only the fields to change. There is no PUT for full record replacement.

Specification and collection

Import the OpenAPI 3.1 specification into Swagger, Insomnia or Postman, or download the ready Postman collection with every endpoint and the apiKey and baseUrl variables.

Create your account and generate the first key

Create free account
Developers · Sellio