Using the API
All Conduit features are accessible through a versioned JSON REST API at
/api/v1. This reference covers authentication, common conventions, and a
summary of every available endpoint. For features that are also available in
the web UI, the relevant UI path is noted alongside the API endpoint.
Base URL
https://conduit.email/api/v1
Authentication
Conduit supports two distinct authentication mechanisms for API access. Both
are presented identically in requests — include a token in the Authorization
header:
Authorization: Bearer <token>
The server tries JWT validation first, then falls back to API token validation, so the calling code never needs to know which type of token it holds.
Method 1 — Session tokens (JWT)
Session tokens are short-lived JWTs issued when you sign in with your email and password (or via OAuth). They consist of two parts:
- Access token — valid for 15 minutes. Include this in every API request.
- Refresh token — long-lived. Exchange it for a new access token when the current one expires, without re-entering credentials.
Obtaining tokens
POST /api/v1/sessions
Content-Type: application/json
{
"email": "you@example.com",
"password": "correct-horse-battery-staple"
}
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"expires_in": 900
}
Refreshing an access token
POST /api/v1/sessions/refresh
Content-Type: application/json
{
"refresh_token": "eyJ..."
}
Signing out
Revoking the refresh token immediately invalidates the session:
DELETE /api/v1/sessions
Authorization: Bearer <access_token>
Content-Type: application/json
{
"refresh_token": "eyJ..."
}
The web UI equivalent is the Sign out action available from the navigation.
When to use session tokens
Session tokens are best suited for interactive, user-facing applications:
- Web and mobile apps where a human types their credentials at login time.
- Short-lived scripts or one-off API calls where you can sign in at the start and discard the tokens when done.
- Any context where you want the authentication to be tied to the account owner's active session so that signing out everywhere immediately revokes access.
Pros and cons
| ✅ Short-lived — a leaked access token expires quickly (15 minutes) | |
| ✅ Revocable — signing out invalidates the refresh token immediately | |
| ✅ Compatible with 2FA — the sign-in flow enforces TOTP if enabled | |
| ❌ Requires refresh logic — callers must implement token rotation | |
| ❌ Awkward for automation — storing a password or refresh token in a CI secret is nearly equivalent to storing an API token |
Method 2 — Long-lived API tokens
API tokens are opaque, long-lived credentials you create in the API or web UI. Unlike session tokens they do not expire by default and do not need to be refreshed.
See the API Tokens section below for the full CRUD reference.
When to use API tokens
API tokens are best suited for automated, non-interactive clients:
- CI/CD pipelines that need to create or update webhooks as part of a deployment.
- Server-side scripts or cron jobs that run unattended.
- Third-party integrations where you want to issue a dedicated credential per integration that can be revoked independently.
- Any context where implementing a token-refresh loop is impractical.
Scoping and restricting tokens
API tokens support two optional restrictions that reduce blast radius if a token is leaked:
expires_in— set a finite lifetime in seconds. Useful when you only need access for a bounded period (e.g. a one-time migration).allowed_ips— restrict which client IPs or CIDR ranges may use the token. Useful when your automation runs from a known IP range (e.g. a GitHub Actions runner pool or a fixed office NAT).
Pros and cons
| ✅ No refresh loop — tokens work until they expire or are revoked | |
| ✅ Per-integration revocation — each token is independent; revoking one does not affect others | |
| ✅ IP restrictions — optionally restrict which IPs may use a token | |
| ✅ Optional expiry — can be time-bounded for temporary access | |
| ❌ Long-lived by default — a leaked token remains valid until manually revoked | |
| ❌ Not 2FA-protected — token creation requires an active session, but using a token bypasses the TOTP challenge | |
| ❌ No automatic rotation — you must explicitly revoke and replace tokens you want to cycle |
Choosing the right method
| Situation | Recommended method |
|---|---|
| Interactive web / mobile app | Session token (JWT) |
| User-initiated CLI tool | Session token (JWT) |
| CI/CD pipeline or cron job | API token |
| Server-to-server integration | API token |
| Short one-off automation | Either — API token is simpler; session token avoids storing a long-lived secret |
| Temporary / bounded access | API token with expires_in |
| Access from a known IP range | API token with allowed_ips |
Two-factor authentication (TOTP)
If 2FA is enabled on your account, the POST /api/v1/sessions response
returns a TOTP challenge instead of tokens:
{
"totp_required": true,
"totp_token": "..."
}
Complete the challenge with a code from your authenticator app:
POST /api/v1/sessions/2fa
Content-Type: application/json
{
"totp_token": "...",
"code": "123456"
}
Errors
All errors follow the same shape:
{
"error": "Human-readable message",
"code": "machine_readable_code"
}
Common HTTP status codes:
| Status | Meaning |
|---|---|
400 |
Bad request — malformed JSON or missing required fields |
401 |
Unauthorized — missing or invalid access token |
404 |
Not found |
409 |
Conflict — e.g. email address already registered |
422 |
Validation error — request was understood but values are invalid |
Authentication-specific error codes:
| Code | Meaning |
|---|---|
email_not_confirmed |
The account exists, but the signup confirmation link has not been opened yet |
token_expired |
The password-reset or confirmation token has expired |
token_invalid |
The password-reset or confirmation token is malformed or no longer valid |
Accounts
| Operation | API | Web UI |
|---|---|---|
| Create account | POST /api/v1/accounts |
/app/signup |
| Get your account | GET /api/v1/accounts/me |
— |
| Change password | PUT /api/v1/accounts/me/password |
/app/settings/account |
| Update timezone | PUT /api/v1/accounts/me/timezone |
/app/settings/account |
| Request password reset | POST /api/v1/accounts/me/password-reset |
/app/reset-password |
| Confirm password reset | PUT /api/v1/accounts/me/password-reset |
/app/reset-password/confirm |
| Delete account | DELETE /api/v1/accounts/me |
— |
Create an account
POST /api/v1/accounts
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Account email address |
password |
string | Yes | Minimum 12 characters |
Account creation sends a signup confirmation email. The account cannot create a session until that email has been confirmed.
Change your password
PUT /api/v1/accounts/me/password
| Field | Type | Required | Description |
|---|---|---|---|
current_password |
string | Yes | Your current password |
new_password |
string | Yes | Minimum 12 characters |
Update your timezone
PUT /api/v1/accounts/me/timezone
| Field | Type | Required | Description |
|---|---|---|---|
timezone |
string | Yes | IANA timezone name (e.g. America/New_York, Europe/Berlin, UTC) |
The timezone setting controls how timestamps are displayed in the web UI. All timestamps in API responses remain in UTC regardless of this setting.
Request a password reset
POST /api/v1/accounts/me/password-reset
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Account email address |
This endpoint always returns success for both known and unknown addresses. When the account exists, Conduit sends a password-reset email containing the token used by the confirmation endpoint below.
Confirm a password reset
PUT /api/v1/accounts/me/password-reset
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Reset token (from the reset email) |
new_password |
string | Yes | Minimum 12 characters |
Delete your account
DELETE /api/v1/accounts/me
| Field | Type | Required | Description |
|---|---|---|---|
password |
string | Yes | Current password to confirm deletion |
confirm |
boolean | Yes | Must be true to acknowledge that deletion is irreversible |
This action is irreversible. All webhooks, delivery logs, security policies, and custom domains are permanently removed.
Two-factor authentication
| Operation | API | Web UI |
|---|---|---|
| Set up TOTP | POST /api/v1/accounts/me/2fa/setup |
/app/settings/2fa/setup |
| Enable TOTP | POST /api/v1/accounts/me/2fa/enable |
/app/settings/2fa/setup (same form) |
| Disable TOTP | DELETE /api/v1/accounts/me/2fa |
/app/settings/account |
| Regenerate backup codes | POST /api/v1/accounts/me/2fa/backup-codes |
/app/settings/account |
Set up TOTP
POST /api/v1/accounts/me/2fa/setup
Returns a secret, an otpauth_url, and a qr_code_png data URI. Scan the
QR code with your authenticator app.
Enable TOTP
POST /api/v1/accounts/me/2fa/enable
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | 6-digit code from your authenticator app |
Disable TOTP
DELETE /api/v1/accounts/me/2fa
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | 6-digit TOTP code to confirm |
Backup codes
When you enable TOTP (POST /api/v1/accounts/me/2fa/enable), the response
includes 8 single-use backup codes:
{
"backup_codes": [
"A1B2C-D3E4F",
"G5H6I-J7K8L",
...
]
}
Save these somewhere safe. Each code can be used once in place of a TOTP code
when completing the sign-in challenge (POST /api/v1/sessions/2fa). After a
backup code is used, it is consumed and cannot be reused.
Regenerate backup codes
POST /api/v1/accounts/me/2fa/backup-codes
Returns a new set of 8 single-use backup codes and invalidates all previous backup codes. Requires an active 2FA session (you must already be signed in).
Webhooks
See Webhook Payload Reference for the JSON structure delivered to your target URL, template variables, and custom headers.
| Operation | API | Web UI |
|---|---|---|
| List webhooks | GET /api/v1/webhooks |
/app/webhooks |
| Create a webhook | POST /api/v1/webhooks |
/app/webhooks/new |
| Get a webhook | GET /api/v1/webhooks/{id} |
/app/webhooks/{id} |
| Update a webhook | PUT /api/v1/webhooks/{id} |
/app/webhooks/{id}/edit |
| Delete a webhook | DELETE /api/v1/webhooks/{id} |
/app/webhooks/{id} (Delete button) |
| Toggle active state | PUT /api/v1/webhooks/{id} (set active) |
/app/webhooks/{id} (Activate/Deactivate button) |
| Rotate secret | PUT /api/v1/webhooks/{id} (set secret) |
/app/webhooks/{id} (Rotate secret button) |
| Simulate email delivery | POST /api/v1/webhooks/{id}/simulate |
/app/webhooks/{id} (Simulate button) |
| View delivery logs | GET /api/v1/webhooks/{id}/logs |
/app/webhooks/{id}/logs |
Create a webhook
POST /api/v1/webhooks
| Field | Type | Required | Description |
|---|---|---|---|
address |
string | No | Full email address to receive mail (e.g. alerts@mail.example.com). Omit to use the public domain; the local part is derived automatically from the webhook ID and cannot be customised. Providing an address on the public domain is rejected (address_lhs_not_allowed). |
target_url |
string | Yes | HTTPS URL to deliver the webhook payload to |
secret |
string | No | Custom HMAC secret; auto-generated if omitted |
active |
boolean | No | Defaults to true |
custom_headers |
object | No | Key/value headers to include in every delivery |
payload_template |
string | No | Go text/template for the JSON payload |
rate_limit |
integer | No | Max emails per minute (0 = disabled) |
smtp_security_policy_id |
string | No | ID of an SMTP security policy to attach |
Note:
custom_headers,payload_template, andrate_limitare currently only configurable through the API.smtp_security_policy_idcan also be set via the webhook edit form in the web UI.
The secret field is only returned at creation time.
Update a webhook
PUT /api/v1/webhooks/{id}
Accepts the same fields as create. To detach the current security policy, set
clear_security_policy: true.
Simulate an email delivery
Trigger a synthetic test delivery to verify that a webhook target is reachable and behaving correctly.
POST /api/v1/webhooks/{id}/simulate
The request body is optional. When provided, these fields customise the simulated email:
| Field | Type | Default | Description |
|---|---|---|---|
from |
string | simulate@conduit.example |
Sender address |
subject |
string | Test email from Conduit |
Email subject line |
text |
string | This is a simulated test email sent from Conduit. |
Plain-text body |
The response contains the delivery outcome:
| Field | Description |
|---|---|
http_status |
HTTP status code returned by the webhook target, if reached |
duration_ms |
Time taken for the delivery attempt in milliseconds |
error |
Error message if delivery was unsuccessful |
simulated |
Always true |
A log entry with simulated: true is written to the delivery log regardless of outcome.
Delivery logs
| Operation | API | Web UI |
|---|---|---|
| List logs for a webhook | GET /api/v1/webhooks/{id}/logs |
/app/webhooks/{id}/logs |
| Get a specific log entry | GET /api/v1/webhooks/{id}/logs/{logId} |
— |
Pagination
The list endpoint accepts two optional query parameters:
| Parameter | Default | Maximum | Description |
|---|---|---|---|
page |
1 |
— | Page number (1-based) |
page_size |
50 |
200 |
Number of results per page |
Example — fetch the second page of 100 results:
GET /api/v1/webhooks/wh_01HX.../logs?page=2&page_size=100
Authorization: Bearer <access_token>
Log entry fields
Each log entry includes:
| Field | Description |
|---|---|
id |
Log entry ID |
webhook_id |
Webhook ID |
smtp_message_id |
SMTP Message-ID header value |
sender |
Envelope sender address |
http_status |
HTTP status code returned by the target (if reached) |
error |
Error detail, if delivery failed |
duration_ms |
Delivery round-trip time in milliseconds |
simulated |
true when the entry was created by a simulation, not a real inbound email |
attempted_at |
Timestamp of the delivery attempt |
Simulated log entries are labelled SIM in the web UI delivery log view.
SMTP security policies
See Configuring an SMTP Security Policy for a full guide.
| Operation | API | Web UI |
|---|---|---|
| List policies | GET /api/v1/smtp-policies |
/app/smtp-policies |
| Create a policy | POST /api/v1/smtp-policies |
/app/smtp-policies/new |
| Get a policy | GET /api/v1/smtp-policies/{id} |
/app/smtp-policies/{id} |
| Update a policy | PUT /api/v1/smtp-policies/{id} |
/app/smtp-policies/{id}/edit |
| Delete a policy | DELETE /api/v1/smtp-policies/{id} |
/app/smtp-policies/{id} (Delete button) |
Domains
Domain management is currently only available through the API. See Using a Custom Domain for a full guide.
| Operation | API |
|---|---|
| List domains | GET /api/v1/domains |
| Register a domain | POST /api/v1/domains |
| Get a domain | GET /api/v1/domains/{id} |
| Verify a domain | POST /api/v1/domains/{id}/verify |
| Delete a domain | DELETE /api/v1/domains/{id} |
Register a domain
POST /api/v1/domains
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Domain name to claim (e.g. mail.example.com) |
Verify a domain
POST /api/v1/domains/{id}/verify
Performs a DNS TXT lookup to confirm the verification token is published. See Using a Custom Domain for the full verification workflow.
API Tokens
Long-lived API tokens let you authenticate without short-lived JWTs — useful for CI/CD pipelines, scripts, and integrations where refreshing tokens is impractical.
Tokens are presented exactly like JWTs: Authorization: Bearer <token>. The authentication middleware tries JWT validation first, then falls back to API token validation.
Important: The raw token value is returned once at creation time. Store it securely — it cannot be retrieved again.
List tokens
GET /api/v1/accounts/me/api-tokens
Returns an array of tokens (without the raw token value).
Create a token
POST /api/v1/accounts/me/api-tokens
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable label (e.g. CI/CD Pipeline) |
expires_in |
integer | No | Token lifetime in seconds. Omit or 0 for no expiry. |
allowed_ips |
string[] | No | Allowed client IPs or CIDR ranges. Omit to allow any IP. |
Response (201):
{
"id": "tok_01HX...",
"name": "CI/CD Pipeline",
"token": "aB3c...raw-token-shown-once...",
"expires_at": "2026-01-01T00:00:00Z",
"allowed_ips": ["192.168.1.0/24"],
"last_used_at": null,
"created_at": "2025-01-01T12:00:00Z"
}
Revoke a token
DELETE /api/v1/accounts/me/api-tokens/{id}
Returns 204 No Content on success.