Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Conduyt Email Templates 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: Email Templates
description: Reusable email/SMS templates
paths:
/emails/templates:
get:
operationId: listEmailTemplates
summary: List email templates
tags:
- Email Templates
parameters:
- name: category
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: channel
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: is_active
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
- name: page
in: query
required: false
description: Page number
schema:
type: integer
default: 1
minimum: 1
x-default-provenance: handler_literal
- name: per_page
in: query
required: false
description: Items per page (max 200)
schema:
type: integer
default: 50
minimum: 1
maximum: 200
x-default-provenance: handler_literal
- name: provider
in: query
required: false
description: Query parameter read by this route.
schema:
type: string
responses:
'200':
description: Paginated templates
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: {}
meta:
type: object
properties:
page:
type: integer
perPage:
type: integer
total:
type: integer
totalPages:
type: integer
required:
- page
- perPage
- total
- totalPages
required:
- data
- meta
post:
operationId: createEmailTemplate
summary: Create an email template
description: 'Email templates built in the editor keep their builder state in hidden markers: CONDUYT_BLOCKS and CONDUYT_META
(subject B, preheader, custom head HTML, footer HTML). Metadata precedence: the CONDUYT_META marker in bodyHtml
wins over one in body; the editor writes it to bodyHtml only, and body''s marker is read only when bodyHtml has
none. A request that would leave DIFFERENT markers in body and bodyHtml is refused with 400
template_meta_conflict ("body and bodyHtml both carry a CONDUYT_META marker and they differ"). Stored rows are
never rewritten.
Pasted HTML (email only): send bodyMode "html" with html (your own designed email, up to 100 KB). The
stored bodyHtml is built from html: the builder''s blocks (from bodyHtml in the request, else the stored row)
are kept, and body defaults to the HTML''s visible text when none is sent. Sends use the pasted HTML as the
body with only what every email gets: merge fields rendered, the preheader / head / footer HTML from the
metadata, open tracking, and an unsubscribe link unless the HTML already carries {{unsubscribe_url}} (or
{{unsubscribeUrl}}). bodyMode "blocks" takes the builder''s bodyHtml; switching back keeps the pasted HTML
(returned as html). Refusals (400, nothing written): html_required (bodyMode html without html, or empty),
html_too_large (over 100 KB: Gmail clips emails over 102 KB), html_script (a anywhere in the HTML), html_without_html_mode (html
without bodyMode html), invalid_body_mode (not "blocks" / "html", or on an SMS template), body_html_required
(bodyMode blocks without bodyHtml), body_mode_conflict (bodyMode blocks with html-mode metadata in bodyHtml),
html_conditional_comment (an Outlook conditional comment that ends before its , is never closed, or
has an unreadable condition), html_office_markup (VML / Office elements outside an Outlook-only block),
html_duplicate_attribute (an element inside an Outlook-only block with the same attribute twice).
A template returned by POST, PATCH or GET /emails/templates/{id} carries bodyMode ("blocks" / "html"; null
for SMS) and html (the pasted HTML in html mode, the kept copy in blocks mode, else null); a LIST row carries
bodyMode only (fetch the template by id for html).'
tags:
- Email Templates
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmailTemplateCreate'
responses:
'201':
description: Template created
content:
application/json:
schema:
$ref: '#/components/schemas/DataEnvelope'
'400':
$ref: '#/components/responses/BadRequest'
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
/emails/templates/{id}:
get:
operationId: getEmailTemplate
summary: Get an email template by ID
tags:
- Email Templates
parameters:
- $ref: '#/components/parameters/IdParam'
responses:
'200':
description: Template details
content:
application/json:
schema:
$ref: '#/components/schemas/DataEnvelope'
'404':
$ref: '#/components/responses/NotFound'
patch:
operationId: updateEmailTemplate
summary: Update an email template
description: 'Email templates built in the editor keep their builder state in hidden markers: CONDUYT_BLOCKS and CONDUYT_META
(subject B, preheader, custom head HTML, footer HTML). Metadata precedence: the CONDUYT_META marker in bodyHtml
wins over one in body; the editor writes it to bodyHtml only, and body''s marker is read only when bodyHtml has
none. A request that would leave DIFFERENT markers in body and bodyHtml is refused with 400
template_meta_conflict ("body and bodyHtml both carry a CONDUYT_META marker and they differ"). Stored rows are
never rewritten.
Pasted HTML (email only): send bodyMode "html" with html (your own designed email, up to 100 KB). The
stored bodyHtml is built from html: the builder''s blocks (from bodyHtml in the request, else the stored row)
are kept, and body defaults to the HTML''s visible text when none is sent. Sends use the pasted HTML as the
body with only what every email gets: merge fields rendered, the preheader / head / footer HTML from the
metadata, open tracking, and an unsubscribe link unless the HTML already carries {{unsubscribe_url}} (or
{{unsubscribeUrl}}). bodyMode "blocks" takes the builder''s bodyHtml; switching back keeps the pasted HTML
(returned as html). Refusals (400, nothing written): html_required (bodyMode html without html, or empty),
html_too_large (over 100 KB: Gmail clips emails over 102 KB), html_script (a anywhere in the HTML), html_without_html_mode (html
without bodyMode html), invalid_body_mode (not "blocks" / "html", or on an SMS template), body_html_required
(bodyMode blocks without bodyHtml), body_mode_conflict (bodyMode blocks with html-mode metadata in bodyHtml),
html_conditional_comment (an Outlook conditional comment that ends before its , is never closed, or
has an unreadable condition), html_office_markup (VML / Office elements outside an Outlook-only block),
html_duplicate_attribute (an element inside an Outlook-only block with the same attribute twice).
A template returned by POST, PATCH or GET /emails/templates/{id} carries bodyMode ("blocks" / "html"; null
for SMS) and html (the pasted HTML in html mode, the kept copy in blocks mode, else null); a LIST row carries
bodyMode only (fetch the template by id for html).'
tags:
- Email Templates
parameters:
- $ref: '#/components/parameters/IdParam'
- 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
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmailTemplateUpdate'
responses:
'200':
description: Template updated
content:
application/json:
schema:
$ref: '#/components/schemas/DataEnvelope'
'400':
$ref: '#/components/responses/BadRequest'
delete:
operationId: deleteEmailTemplate
summary: Delete an email template
tags:
- Email Templates
parameters:
- $ref: '#/components/parameters/IdParam'
- 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
responses:
'200':
description: Template deleted
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id: {}
deleted:
type: boolean
archived:
type: boolean
required:
- id
- deleted
- archived
required:
- data
/emails/templates/test-send:
post:
operationId: testSendEmailTemplate
summary: Send a test email from a template
tags:
- Email Templates
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
templateId:
description: 'A saved email template of this account. When given, the template is what is sent: a subject and html given beside it are not read. Absent, null or empty = no template (subject and html are used).'
anyOf:
- type: string
pattern: ^$|^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
- type: 'null'
subject:
description: With html, the subject of an unsaved template (used when templateId is absent, null or empty).
anyOf:
- type: string
minLength: 1
- type: 'null'
html:
description: With subject, the stored base HTML of an unsaved template (used when templateId is absent, null or empty).
anyOf:
- type: string
minLength: 1
- type: 'null'
toEmail:
anyOf:
- type: string
- type: 'null'
description: 'This endpoint has conditional or cross-field request requirements. Flat requiredFields are intentionally omitted; read endpoint notes/description and validation errors for valid field combinations. Either templateId (a saved template: a subject and html given beside it are not read), or both subject and html (the draft).'
responses:
'200':
description: Test email sent
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
sent:
type: boolean
to: {}
subject: {}
required:
- sent
- to
- subject
required:
- data
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
components:
schemas:
EmailTemplateCreateHtml:
type: object
required:
- name
- channel
- bodyMode
- html
properties:
name:
type: string
channel:
type: string
enum:
- email
subject:
type: string
body:
type: string
description: Optional; defaults to the HTML's visible text.
bodyHtml:
type: string
description: Optional; only its builder markers (blocks, metadata) are read and kept.
bodyMode:
type: string
enum:
- html
html:
type: string
maxLength: 102400
description: 'Your own email HTML. Up to 100 KB (UTF-8 bytes; Gmail clips emails over 102 KB); no <script>. Merge fields like {{contact.firstName}} work inside it. Outlook''s conditional comments (<!--[if mso]> … <![endif]-->, <!--[if !mso]><!--> … <!--<![endif]-->) and the VML / Office elements inside them are kept with their content sanitised (scripts, frames, event handlers and javascript:/data: URLs removed), while a conditional comment that cannot be read safely (html_conditional_comment) or VML outside an Outlook-only block (html_office_markup) is refused with a 400 that names it.'
category:
type: string
isActive:
type: boolean
default: true
EmailTemplateCreateBlocks:
type: object
required:
- name
- channel
- body
properties:
name:
type: string
channel:
type: string
enum:
- sms
- email
subject:
type: string
body:
type: string
bodyHtml:
type: string
bodyMode:
type: string
enum:
- blocks
description: 'Email only. "blocks" = built from the editor blocks: send the builder bodyHtml. Omitted = bodyHtml stored as sent.'
category:
type: string
isActive:
type: boolean
default: true
not:
required:
- html
DataEnvelope:
type: object
properties:
data:
description: Response payload (shape varies by endpoint)
EmailTemplateUpdate:
type: object
description: 'Partial update: send only the fields to change. html requires bodyMode "html"; bodyMode "html" requires html;
bodyMode "blocks" requires bodyHtml. Builder markers the request leaves out are kept from the stored template.'
properties:
name:
type: string
subject:
type: string
body:
type: string
bodyHtml:
type: string
bodyMode:
type: string
enum:
- blocks
- html
html:
type: string
maxLength: 102400
category:
type: string
provider:
type: string
isActive:
type: boolean
dependentRequired:
html:
- bodyMode
allOf:
- if:
properties:
bodyMode:
const: html
required:
- bodyMode
then:
required:
- html
- if:
properties:
bodyMode:
const: blocks
required:
- bodyMode
then:
required:
- bodyHtml
not:
required:
- html
ErrorResponse:
type: object
required:
- error
properties:
error:
type: string
code:
type: string
description: Machine-readable error code (e.g., TRIAL_EXPIRED, PLAN_LIMIT_REACHED)
EmailTemplateCreate:
description: 'Two shapes. Built from the editor''s blocks (or plain): name, channel and body are required, bodyHtml optional,
bodyMode "blocks" or omitted. Pasted HTML (email only): name, channel "email", bodyMode "html" and html are
required; body is optional (it defaults to the HTML''s visible text).'
oneOf:
- $ref: '#/components/schemas/EmailTemplateCreateBlocks'
- $ref: '#/components/schemas/EmailTemplateCreateHtml'
parameters:
IdParam:
name: id
in: path
required: true
schema:
type: string
format: uuid
description: Resource UUID
responses:
BadRequest:
description: Bad request — validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
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