Download OpenAPI specification:
Secure email subscription API with double opt-in, unsubscribe, and admin operations. This file is the single source of truth for the API surface.
Kick off the double opt-in flow for a given email and listId.
canSubscribe: true.X-API-Key: sk_... or Authorization: Bearer sk_....Origin header must match the API key’s websiteOrigin (unless the key supports server-to-server requests).PENDING state and a confirmation email is sent.CONFIRMED, the call returns 400. If PENDING, the confirmation token is refreshed and resent./api/confirm/{token} to complete activation.400 — validation failed or address already confirmed.403 — API key not authorised for the list or origin mismatch.404 — list ID does not exist.429 — caller exceeded the rate limit.| Origin | string <uri> Must match the API key's |
Email address and list to subscribe. Optional metadata is stored verbatim.
| email required | string <email> |
| listId required | string |
| metadata | any |
{- "email": "user@example.com",
- "listId": "cln123456789",
- "metadata": {
- "source": "footer_form"
}
}{- "success": true,
- "message": "Confirmation email resent"
}Finalises the double opt-in flow. This endpoint renders an HTML confirmation page and does not require authentication.
POST /api/subscribe.200 when the token is valid and not expired.404 if the token is invalid or already used.410 when the token has expired.| token required | string Confirmation token from the subscription email. |
Remove a subscriber using their email address instead of the one-click token. Useful for “manage preferences” pages or admin actions.
canUnsubscribe: true.X-API-Key: sk_... or Authorization: Bearer sk_....Origin must match the API key’s websiteOrigin unless the key is server-to-server.200 and sends a confirmation email when the subscriber transitions to UNSUBSCRIBED.400 if the subscriber is already unsubscribed.404 if the email/list combination does not exist.400 — already unsubscribed or validation failure.403 — API key lacks permission or targets a different list.404 — subscription not found.429 — caller exceeded the rate limit.| Origin | string <uri> Must match the API key's |
Email and list identifier to unsubscribe.
| email required | string <email> |
| listId required | string |
{- "email": "user@example.com",
- "listId": "cln123456789"
}{- "success": true,
- "message": "Successfully unsubscribed"
}Send the one‑click unsubscribe email that contains the permanent token link used by
/api/unsubscribe/{token}. The response is always a generic success so that clients
cannot infer whether the email address is subscribed.
canUnsubscribe: true.X-API-Key: sk_... or Authorization: Bearer sk_....Origin header must match the API key’s websiteOrigin unless the key is created with
allowServerToServer.listId.subscription_events entry is logged.200 response without
sending mail, preventing address enumeration.400 — payload fails validation (email or listId missing/invalid).403 — origin mismatch or API key not authorised for the target list.404 — list does not exist or belongs to another tenant.429 — caller exceeded the rate limit.| Origin | string <uri> Must match the API key's |
Email + list to target.
| email required | string <email> |
| listId required | string |
{- "email": "user@example.com",
- "listId": "cln123456789"
}{- "success": true,
- "message": "If this email is subscribed, an email was sent with an unsubscribe link"
}Handles one-click unsubscribe links embedded in transactional emails. Renders an HTML page that reflects the outcome of the operation.
200 with an HTML confirmation message when the token is valid.404 if the token is invalid, expired, or already used.GET /api/unsubscribe/{token} where {token} comes from the email.| token required | string Permanent unsubscribe token contained in the email. |
Retrieve every mailing list configured for your project. Useful for populating dropdowns or management dashboards.
klemailgw_ADMIN_API_KEY).401 — admin key missing or invalid.403 — admin key disabled.{- "success": true,
- "data": {
- "lists": [
- {
- "id": "cln123456789",
- "name": "Blog Newsletter",
- "description": "Weekly updates",
- "createdAt": "2025-10-10T12:00:00.000Z",
- "_count": {
- "subscribers": 1200
}
}
]
}
}Provision a new mailing list identified by name and websiteOrigin. The origin acts as an
allowlist for API keys.
201 with the newly created list.400 if required fields are missing or invalid.PROD - …, STAGE - …) so UI integrations can display intent.websiteOrigin aligned with the domains you expect API keys to use.Attributes for the list.
| name required | string |
| description | string |
| websiteOrigin required | string <uri> |
{- "name": "string",
- "description": "string",
}{- "success": true,
- "data": {
- "list": {
- "id": "cln123456789",
- "name": "Blog Newsletter",
- "description": "Weekly updates",
- "createdAt": "2025-10-10T12:00:00.000Z"
}
}, - "message": "List created successfully"
}Enumerate existing API keys for auditing or UI management. Secrets are never returned—only metadata and prefixes.
lastUsedAt).401 / 403 — admin key missing, invalid, or disabled.{- "success": true,
- "data": {
- "apiKeys": [
- {
- "id": "clk987654321",
- "name": "Blog Website Key",
- "allowServerToServer": false,
- "permissions": {
- "canSubscribe": true,
- "canUnsubscribe": true
}, - "isActive": true,
- "createdAt": "2025-10-10T12:05:00.000Z",
- "lastUsedAt": "2025-10-10T13:00:00.000Z"
}
]
}
}Generate a new API key and return the raw secret exactly once. Store the secret immediately—after the response it cannot be recovered.
201 with both the hashed record and apiKey secret.400 if required fields are missing or invalid.allowServerToServer: true only when you control the caller and no browser Origin will be present.permissions narrowly (e.g., unsubscribe-only webhooks).Key metadata, including optional permissions.
| name required | string |
| websiteOrigin required | string |
| allowServerToServer | boolean Default: false |
object |
{- "name": "string",
- "websiteOrigin": "string",
- "allowServerToServer": false,
- "permissions": {
- "canSubscribe": true,
- "canUnsubscribe": true
}
}{- "success": true,
- "data": {
- "apiKey": "sk_abc123def456...",
- "record": {
- "id": "clk987654321",
- "name": "Blog Website Key",
- "allowServerToServer": false,
- "permissions": {
- "canSubscribe": true,
- "canUnsubscribe": true
}, - "createdAt": "2025-10-10T12:05:00.000Z"
}
}, - "message": "API key created successfully. Save this key securely - it will not be shown again."
}Fetch subscribers for reporting or CRM synchronisation. Supports pagination and filtering by status and list.
listId — limit results to a specific mailing list.status — filter by PENDING, CONFIRMED, or UNSUBSCRIBED.page / limit — standard pagination controls.401 / 403 — admin token missing or invalid.400 — invalid query parameter values.| listId | string Return only subscribers belonging to this list. |
| status | string Enum: "PENDING" "CONFIRMED" "UNSUBSCRIBED" Filter by subscription status. |
| page | integer >= 1 1-based page index. |
| limit | integer [ 1 .. 100 ] Number of items per page (max 100). |
{- "success": true,
- "data": {
- "subscribers": [
- {
- "id": "cls111222333",
- "email": "user@example.com",
- "status": "CONFIRMED",
- "listId": "cln123456789",
- "metadata": {
- "source": "homepage"
}, - "createdAt": "2025-10-10T12:10:00.000Z",
- "confirmedAt": "2025-10-10T12:12:00.000Z",
- "unsubscribedAt": null,
- "list": {
- "id": "cln123456789",
- "name": "Blog Newsletter"
}
}
], - "pagination": {
- "page": 1,
- "limit": 20,
- "total": 1,
- "totalPages": 1
}
}
}Inspect past and in-flight campaigns. Useful for dashboards or auditing.
listId — return only campaigns for a specific mailing list.status — limit to QUEUED, IN_PROGRESS, COMPLETED, or FAILED.page / limit — standard pagination.401 / 403 — admin token missing or invalid.| listId | string Only return campaigns for this list. |
| status | string Enum: "QUEUED" "IN_PROGRESS" "COMPLETED" "FAILED" Filter campaigns by current status. |
| page | integer >= 1 1-based page index. |
| limit | integer [ 1 .. 100 ] Number of items per page (max 100). |
{- "success": true,
- "data": {
- "campaigns": [
- {
- "id": "cmgxxxx",
- "listId": "cln123456789",
- "subject": "New article: How we ship",
- "status": "IN_PROGRESS",
- "total": 2450,
- "sent": 300,
- "failed": 2,
- "lastOffset": 300,
- "createdAt": "2025-10-10T15:15:00.000Z",
- "startedAt": "2025-10-10T15:16:00.000Z",
- "completedAt": null
}
], - "pagination": {
- "page": 1,
- "limit": 20,
- "total": 1,
- "totalPages": 1
}
}
}Schedule a bulk campaign or retry an idempotent request. The first call typically returns 202 while the send is processed asynchronously.
idempotencyKey to safely retry requests.202 when a campaign is queued/started, 200 when reusing an existing idempotent record./api/admin/campaigns/{id} to monitor progress.400 — missing subject/HTML or invalid payload.403 — admin token lacks permission.404 — target list not found.| listId required | string |
| subject required | string |
| html | string |
| text | string |
| previewOnly | boolean Default: false |
| batchSize | integer [ 1 .. 500 ] Default: 100 |
| idempotencyKey | string <= 100 characters |
{- "listId": "string",
- "subject": "string",
- "html": "string",
- "text": "string",
- "previewOnly": false,
- "batchSize": 100,
- "idempotencyKey": "string"
}{- "success": true,
- "data": {
- "campaign": {
- "id": "cmgxxxx",
- "status": "IN_PROGRESS",
- "total": 2450,
- "sent": 100,
- "failed": 0,
- "processed": 100,
- "remaining": 2350,
- "nextOffset": 100
}
}, - "message": "Campaign in progress"
}Retrieve progress information for a specific campaign, including totals and timestamps.
campaign.sends data for detailed delivery metrics.404 — campaign ID not found.401 / 403 — admin token missing or invalid.| id required | string Campaign identifier returned by the create/send endpoints. |
{- "success": true,
- "data": {
- "id": "cmgxxxx",
- "listId": "cln123456789",
- "subject": "New article: How we ship",
- "status": "IN_PROGRESS",
- "total": 2450,
- "sent": 300,
- "failed": 2,
- "lastOffset": 300,
- "createdAt": "2025-10-10T15:15:00.000Z",
- "startedAt": "2025-10-10T15:16:00.000Z",
- "completedAt": null
}
}Manually advance a campaign by sending the next batch. Use this when you want fine-grained control over pacing or to resume sending after a pause.
batchSize (default 100, max 500).202 while work continues in the background.GET /api/admin/campaigns/{id} to check updated counts.404 — campaign not found.409 — (future) if campaign is not in a resumable state.400 — invalid batchSize.| id required | string Campaign identifier to advance. |
| batchSize | integer [ 1 .. 500 ] Default: 100 |
{- "batchSize": 100
}{- "success": true,
- "data": {
- "campaign": {
- "id": "cmgxxxx",
- "status": "IN_PROGRESS",
- "total": 2450,
- "sent": 400,
- "failed": 2,
- "processed": 100,
- "remaining": 2050,
- "nextOffset": 400
}
}
}Resume processing for a campaign that was paused or reached a terminal state but still has unsent recipients (e.g., after recovering from an error).
200 if the campaign is immediately marked IN_PROGRESS or COMPLETED.202 if the system needs additional time to requeue work.404 — campaign ID not found.409 — (future) campaign cannot be resumed (e.g., already in progress).| id required | string Campaign identifier to resume. |
{- "success": true,
- "data": null,
- "message": "string"
}