Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Conduyt Bulk Operations API
version: 1.0.0
description: 'The Conduyt CRM API provides programmatic access to your CRM data including
contacts, companies, deals, pipelines, tasks, notes, automations, invoices,
email sequences, and more.
## Authentication
All authenticated endpoints require a Bearer token (API key) in the
`Authorization` header:
```
Authorization: Bearer cdy_
```
API keys are generated in **Settings > API Keys** within the Conduyt
dashboard. Keys use the `cdy_` prefix and are hashed with bcrypt on
creation — the full key is shown exactly once at creation time.
Session cookie authentication (`conduyt_session`) is also supported for
browser-based clients but is not recommended for integrations.
## Rate Limiting
API keys enforce a per-key rate limit. New keys default to 100 requests per
minute and can be configured at creation time or with PATCH `/api-keys/{id}`.
Some public and provider-facing endpoints also enforce stricter per-IP or
per-account limits. When exceeded, the API returns `429 Too Many Requests`
with a `Retry-After` header indicating seconds until the next allowed
request.
| Endpoint Group | Limit |
|---------------------|---------------------|
| API keys | Default 100 / min per key |
| Login/reset | 5 / min per IP |
| Registration | 3 / hour per IP |
| Contact/Deal create | 30 / min per IP |
| Inbound messages | 120 / min per account/IP |
## Pagination
List endpoints support cursor-based pagination via query parameters:
- `page` — Page number (default: 1)
- `per_page` — Items per page (default: 50, max: 200)
- `limit` — Accepted as an alias of `per_page` on `GET /contacts`; if both are
sent they must carry the same value.
A page size above the maximum, a non-integer, a value below 1, or a
pagination key repeated with different values is rejected with `400`
(the message names the parameter and the rule); nothing is silently
clamped or ignored.
Responses include a `meta` object: `{ page, per_page, total }`.
An operation that documents its own pagination rules follows those instead.
`GET /sms-providers/options` pages with an opaque `cursor` and a `limit`
that is capped at 50 rather than rejected; its responses carry
`meta: { nextCursor, truncated }`.
## Response Format
All successful responses wrap data in a `data` envelope:
```json
{ "data": { ... } }
```
Error responses use:
```json
{ "error": "Human-readable error message" }
```
## Multi-Tenancy
Conduyt is multi-tenant. All data is scoped to the authenticated user''s
current account. API keys are bound to a specific account at creation time.
## Webhook Events
Outbound webhooks fire on events including:
`contact.created`, `contact.updated`, `contact.deleted`,
`deal.created`, `deal.updated`, `deal.won`, `deal.lost`,
`task.created`, `task.completed`, `note.created`,
`appointment.created`, `appointment.updated`,
`form.submitted`, `invoice.paid`
Payloads are signed with HMAC-SHA256 using the webhook''s secret. Retry
schedule: 1m, 5m, 30m, 2h, 12h, 72h (7 total attempts including the initial delivery).'
contact:
name: Conduyt Support
url: https://conduyt.app
email: support@conduyt.app
license:
name: Proprietary
url: https://conduyt.app/terms
servers:
- url: https://conduyt.app/api/v1
description: Production
security:
- BearerAuth: []
tags:
- name: Bulk Operations
description: Bulk update, delete, and tag contacts and deals
paths:
/bulk:
get:
operationId: getBulkStatus
summary: Get bulk operation status
tags:
- Bulk Operations
responses:
'200':
description: Status of running bulk operations
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
operations: {}
required:
- operations
required:
- data
parameters: []
/bulk/contacts/delete:
post:
operationId: bulkDeleteContacts
summary: Bulk delete contacts
tags:
- Bulk Operations
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- ids
properties:
ids:
type: array
items:
type: string
format: uuid
responses:
'200':
description: Contacts deleted
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
deleted:
type: number
requested: {}
followupsQueued: {}
required:
- deleted
- requested
required:
- data
parameters: []
/bulk/contacts/edit:
post:
operationId: bulkEditContacts
summary: Bulk edit contact fields
tags:
- Bulk Operations
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- ids
- updates
properties:
ids:
type: array
items:
type: string
format: uuid
updates:
type: object
description: The fields to set on every listed contact. A non-empty `country` may be changed; it cannot be cleared (null, empty and whitespace-only values are refused).
properties:
country:
type: string
minLength: 1
pattern: \S
description: A non-empty country. Clearing it is refused with `country_invalid`.
addressLine1:
description: Read by the handler from the JSON body (the route declares no schema for its type).
addressLine2:
description: Read by the handler from the JSON body (the route declares no schema for its type).
assignedTo:
description: Read by the handler from the JSON body (the route declares no schema for its type).
city:
description: Read by the handler from the JSON body (the route declares no schema for its type).
company:
description: Read by the handler from the JSON body (the route declares no schema for its type).
customFields:
description: Read by the handler from the JSON body (the route declares no schema for its type).
doNotContact:
description: Read by the handler from the JSON body (the route declares no schema for its type).
doNotContactReason:
description: Read by the handler from the JSON body (the route declares no schema for its type).
email:
description: Read by the handler from the JSON body (the route declares no schema for its type).
firstName:
description: Read by the handler from the JSON body (the route declares no schema for its type).
jobTitle:
description: Read by the handler from the JSON body (the route declares no schema for its type).
lastName:
description: Read by the handler from the JSON body (the route declares no schema for its type).
optedOutEmail:
description: Read by the handler from the JSON body (the route declares no schema for its type).
optedOutSms:
description: Read by the handler from the JSON body (the route declares no schema for its type).
phone:
description: Read by the handler from the JSON body (the route declares no schema for its type).
source:
description: Read by the handler from the JSON body (the route declares no schema for its type).
state:
description: Read by the handler from the JSON body (the route declares no schema for its type).
zip:
description: Read by the handler from the JSON body (the route declares no schema for its type).
responses:
'200':
description: Contacts updated
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
updated:
type: number
requested:
type: number
required:
- data
'400':
description: Nothing was written. `code` is `country_invalid` ("country cannot be cleared") when `updates.country` is null, empty or whitespace-only; `phone_invalid` when a phone or phone-type custom field is not a valid number for a row's country; other 400s describe the invalid request in `error`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
parameters:
- name: dry_run
in: query
required: false
description: Validate the mutation without persisting changes.
schema:
type: boolean
- name: sandbox
in: query
required: false
description: Run the mutation in sandbox mode.
schema:
type: boolean
/bulk/contacts/tag:
post:
operationId: bulkTagContacts
summary: Bulk add/remove tags on contacts
tags:
- Bulk Operations
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
contactIds:
description: Read by the handler from the JSON body (the route declares no schema for its type).
tagId:
description: Read by the handler from the JSON body (the route declares no schema for its type).
responses:
'200':
description: Tags updated
content:
application/json:
schema:
$ref: '#/components/schemas/DataEnvelope'
parameters:
- name: Idempotency-Key
in: header
required: false
description: 'Optional: one key per bulk tag operation (1-255 printable ASCII characters, not blank), reused only to retry the SAME request. Same key + same request body replays the original response (Idempotent-Replay: true); same key + a different body is refused with 422 code idempotency_request_conflict (the key is bound to the first request for good: verify it, then use a NEW key). Two DISTINCT 409s under this key, never interchangeable: (1) the ORIGINAL attempt is still being processed (code idempotency_in_progress) — wait for the Retry-After seconds given, then retry with the SAME key; (2) the key was already SPENT by a prior attempt that failed or has an unknown outcome (code idempotency_key_spent) — verify whether that attempt took effect BEFORE sending a brand-new key; resending the SAME key here only replays this 409 again, it never re-runs the request. A third, rare 409 (code idempotency_conflict_retry) means a concurrent claim of this key could not be resolved and nothing was run or stored: retry with the SAME key. If the wrapper''s OWN write to RELEASE a failed attempt''s claim cannot be confirmed, the SAME key instead answers 503 idempotency_claim_held until the held claim expires — retry with a NEW key, never the same one. If the wrapper''s OWN write to confirm a SUCCESSFUL completion cannot be confirmed, the mutation may have gone through anyway: the SAME key instead answers 503 idempotency_outcome_unknown — verify the outcome before any new-key retry, exactly as case (2) above. A blank or malformed key is refused with 400 idempotency_key_invalid, on dry_run requests too (a dry run never records the key).'
schema:
type: string
pattern: ^[!-~](?:[ -~]{0,253}[!-~])?$
/bulk/contacts/update:
post:
operationId: bulkUpdateContacts
summary: Bulk update contacts with field values
tags:
- Bulk Operations
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
updates:
type: array
items:
type: object
properties:
id:
description: Read by the handler from the JSON body (the route declares no schema for its type).
description: Read by the handler from the JSON body (the route declares no schema for its type).
description: Read by the handler from the JSON body (the route declares no schema for its type).
responses:
'200':
description: Contacts updated
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
summary: {}
results: {}
required:
- summary
- results
required:
- data
parameters: []
/bulk/deals/edit:
post:
operationId: bulkEditDeals
summary: Bulk edit deal fields
tags:
- Bulk Operations
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- ids
- updates
properties:
ids:
type: array
items:
type: string
format: uuid
updates:
type: object
properties:
assignedTo:
description: Read by the handler from the JSON body (the route declares no schema for its type).
currency:
description: Read by the handler from the JSON body (the route declares no schema for its type).
expectedCloseDate:
description: Read by the handler from the JSON body (the route declares no schema for its type).
lostReason:
description: Read by the handler from the JSON body (the route declares no schema for its type).
priority:
description: Read by the handler from the JSON body (the route declares no schema for its type).
stageId:
description: Read by the handler from the JSON body (the route declares no schema for its type).
status:
description: Read by the handler from the JSON body (the route declares no schema for its type).
title:
description: Read by the handler from the JSON body (the route declares no schema for its type).
value:
description: Read by the handler from the JSON body (the route declares no schema for its type).
responses:
'200':
description: Deals updated
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
updated: {}
skippedNoOp: {}
requested: {}
errors: {}
required:
- updated
- skippedNoOp
- requested
- errors
required:
- data
parameters: []
/bulk/deals/update:
post:
operationId: bulkUpdateDeals
summary: Bulk update deals
description: 'Updates up to 100 deals in one request. Each item in `updates` carries the deal `id` plus the fields to change for THAT deal, and each is applied on its own: one item failing never blocks the others. A valid batch of 1 to 100 items answers 200 with a per-item outcome for every item; authentication, billing, request-shape, rate-limit and server failures use their documented top-level statuses instead. Rate limited to 5 requests per minute per account and IP. Length limits count UTF-16 code units as JavaScript does, so a character outside the Basic Multilingual Plane such as an emoji counts 2; JSON Schema counts it 1, so a string near the limit can pass this schema and still be refused per item.'
tags:
- Bulk Operations
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- updates
properties:
updates:
type: array
minItems: 1
maxItems: 100
description: One entry per deal. A body without a non-empty `updates` array answers 400; more than 100 entries answers 422.
items:
type: object
required:
- id
description: The deal `id` plus any of the editable fields below. Any other key is ignored, not applied. An item with no editable field fails with "No valid fields to update".
properties:
id:
type: string
format: uuid
description: The deal to update. A deal that does not exist, or that the caller cannot edit, fails that item with \"Deal not found\".
title:
type: string
minLength: 1
maxLength: 200
description: Max 200 UTF-16 code units (an emoji counts 2).
stageId:
type: string
format: uuid
pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
description: Must be a stage of the deal's pipeline; stage-entry rules (required fields, lost reason) apply per item.
value:
description: A number or a plain decimal string (digits with an optional fraction, no exponent); null clears it. After rounding half away from zero to 2 decimal places the value must lie between -9,999,999,999.99 and 9,999,999,999.99, so 9999999999.994 is accepted and 9999999999.995 is not.
oneOf:
- type: number
exclusiveMinimum: -9999999999.995
exclusiveMaximum: 9999999999.995
- type: string
pattern: ^-?0*(?!9999999999\.99[5-9])\d{1,10}(\.\d+)?$
- type: 'null'
currency:
type: string
maxLength: 10
description: Max 10 UTF-16 code units (an emoji counts 2).
status:
type: string
enum:
- open
- won
- lost
priority:
type: string
maxLength: 30
description: Max 30 UTF-16 code units (an emoji counts 2).
assignedTo:
type:
- string
- 'null'
format: uuid
pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
expectedCloseDate:
type:
- string
- 'null'
maxLength: 50
pattern: ^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$
description: ISO 8601 date (YYYY-MM-DD) or date-time (YYYY-MM-DDTHH:MM[:SS[.fff]][Z|+HH:MM], a space may replace the T). The pattern checks the shape only; the route also rejects impossible calendar or clock values (2026-02-31, month 13, hour 24) with a per-item error, which a JSON schema cannot express. null clears it.
lostReason:
type:
- string
- 'null'
maxLength: 500
description: Max 500 UTF-16 code units (an emoji counts 2); null clears it.
customFields:
type: object
additionalProperties: true
description: Merged into the deal's existing custom fields (keys you send replace those keys; others stay). The merged map, as stored, may not exceed 65,536 bytes.
example:
updates:
- id: 5f0c2b1e-8a47-4c1d-9e3a-2b7d6c4a1f90
stageId: 7d3e9a52-1c64-4b8f-a0d2-3e5f8b6c9a11
priority: high
- id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
value: 12500
expectedCloseDate: 2026-11-30
responses:
'200':
description: 'Per-item results: each update succeeds or fails on its own (success false with an error). An item whose merged customFields map, as stored (PostgreSQL''s jsonb form, where numbers like 1e300 expand to their full digits), would exceed 65,536 bytes fails with the error "customFields exceeds maximum size of 65536 bytes" and is not written.'
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
type: object
required:
- summary
- results
properties:
summary:
type: object
required:
- total
- succeeded
- failed
- skippedNoOp
- written
properties:
total:
type: integer
description: Items in the request.
succeeded:
type: integer
description: Items whose desired state holds, including skippedNoOp items.
failed:
type: integer
skippedNoOp:
type: integer
description: Succeeded items that changed nothing because the deal already held the requested state.
written:
type: integer
description: Items that actually changed a deal (succeeded minus skippedNoOp).
results:
type: array
description: One entry per request item, in request order.
items:
type: object
required:
- id
- success
properties:
id:
type: string
description: The item's id, or \"unknown\" when the item had none.
success:
type: boolean
skippedNoOp:
type: boolean
description: Present and true on a success that changed nothing.
error:
type: string
description: Present when success is false, for example \"Deal not found\", \"Missing id\", \"No valid fields to update\", a validation message, or \"Deal changed concurrently — refresh and retry\". There is no per-item error code.
example:
data:
summary:
total: 3
succeeded: 2
failed: 1
skippedNoOp: 0
written: 2
results:
- id: 5f0c2b1e-8a47-4c1d-9e3a-2b7d6c4a1f90
success: true
- id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
success: true
- id: 0d9c8b7a-6f5e-4d3c-8b2a-1f0e9d8c7b6a
success: false
error: Deal not found
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
description: The account's trial or subscription is not active.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
$ref: '#/components/responses/Forbidden'
'422':
description: More than 100 entries in `updates` (\"Maximum 100 updates per request\"). Nothing was written.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
$ref: '#/components/responses/RateLimited'
'500':
description: Unexpected failure outside the per-item loop, for example a request body that is not valid JSON. Nothing is reported per item.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
parameters: []
/bulk-operations/unified:
get:
x-generated: true
operationId: getBulkOperationsUnified
summary: List bulk-operations unified
tags:
- Bulk Operations
parameters:
- name: dateFrom
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: dateTo
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: objectType
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: page
in: query
required: false
description: Query parameter read by this route.
schema:
type: integer
default: 1
minimum: 1
x-default-provenance: handler_literal
- name: per_page
in: query
required: false
description: Query parameter read by this route.
schema:
type: integer
default: 20
minimum: 1
maximum: 100
x-default-provenance: handler_literal
- name: search
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: status
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: type
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: userId
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
data: {}
meta: {}
required:
- data
- meta
required:
- data
components:
responses:
Forbidden:
description: Insufficient permissions
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
RateLimited:
description: Too many requests
headers:
Retry-After:
schema:
type: integer
description: Seconds until next allowed request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BadRequest:
description: Bad request — validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
DataEnvelope:
type: object
properties:
data:
description: Response payload (shape varies by endpoint)
ErrorResponse:
type: object
required:
- error
properties:
error:
type: string
code:
type: string
description: Machine-readable error code (e.g., TRIAL_EXPIRED, PLAN_LIMIT_REACHED)
securitySchemes:
BearerAuth:
type: http
scheme: bearer
description: 'API key authentication. Generate keys in Settings > API Keys.
Keys use the `cdy_` prefix.
Example: `Authorization: Bearer cdy_a1b2c3d4e5f6...`
'
x-mcp:
transport:
- stdio
- http
orchestration: code
endpoint_tools: hidden