diff --git a/docs/API.md b/docs/API.md index 96df7d9..6b39756 100644 --- a/docs/API.md +++ b/docs/API.md @@ -64,18 +64,18 @@ Remove a subscriber using their per-subscription unsubscribe token (included in # Management API -Grants full access to sending mails and managing lists. A cli client is provided. Third-party frontends should be reasonably easy to set up by using the following documentation. +Grants full access to sending mails and managing lists. A CLI client is provided. Third-party frontends should be reasonably easy to set up by using the following documentation. All management endpoints require Ed25519 request signing when a public key is configured on the server. -This API should not be exposed publicly, even if it is authenticated. Prefer some kind of private tunneling/VPN, or using it right on the machine the server runs on via ssh. +This API should not be exposed publicly, even if it is authenticated. Prefer some kind of private tunneling/VPN, or using it right on the machine the server runs on via SSH. The request signing is intended to be additional hardening, not the main security measure. **Required headers** -| Header | Value | -|---------------|----------------------------------------------------------------------------| -| `X-Timestamp` | Unix timestamp (seconds) of the request | -| `X-Signature` | Hex-encoded Ed25519 signature over `timestamp\nMETHOD\npath\nbodyHash` | +| Header | Value | +|---------------|------------------------------------------------------------------------| +| `X-Timestamp` | Unix timestamp (seconds) of the request | +| `X-Signature` | Hex-encoded Ed25519 signature over `timestamp\nMETHOD\npath\nbodyHash` | The signed message is: `\n\n\n` @@ -83,6 +83,24 @@ Requests whose timestamp differs from the server's clock by more than 5 minutes --- +## GET `/lists` + +Return all mailing lists. + +**Responses** + +| Status | Meaning | +|--------|----------------| +| `200` | List of lists | +| `500` | Internal error | + +**Response body** +```json +[{ "name": "newsletter" }, { "name": "announcements" }] +``` + +--- + ## POST `/lists` Create a new mailing list. @@ -103,90 +121,92 @@ Create a new mailing list. **Response body** ```json -{ "id": 1, "name": "my-list" } +{ "name": "newsletter" } ``` --- -## GET `/lists/{id}` +## GET `/lists/{name}` Get list details including subscriber counts. **Path params** -- `id` - numeric list ID +- `name` - list name **Responses** | Status | Meaning | |--------|----------------| | `200` | List details | -| `400` | Invalid ID | | `404` | List not found | +| `500` | Internal error | **Response body** ```json { - "id": 1, - "name": "my-list", + "name": "newsletter", "subscribers": { "total": 42, "confirmed": 38 } } ``` --- -## PUT `/lists/{id}` +## PUT `/lists/{name}` Rename a mailing list. **Path params** -- `id` - numeric list ID +- `name` - current list name **Body** - `application/json` | Field | Type | Required | |--------|--------|----------| -| `name` | string | yes | +| `name` | string | yes | new name | **Responses** | Status | Meaning | |--------|----------------------| -| `200` | Renamed list | +| `200` | Renamed | | `400` | Missing/invalid name | | `500` | Internal error | +**Response body** +```json +{ "name": "new-name" } +``` + --- -## DELETE `/lists/{id}` +## DELETE `/lists/{name}` Delete a mailing list and all its subscribers. **Path params** -- `id` - numeric list ID +- `name` - list name **Responses** | Status | Meaning | |--------|----------------| | `204` | Deleted | -| `400` | Invalid ID | | `500` | Internal error | --- -## GET `/lists/{id}/users` +## GET `/lists/{name}/users` -List all subscribers of a mailing list. +List all subscribers of a mailing list (confirmed and unconfirmed). **Path params** -- `id` - numeric list ID +- `name` - list name **Responses** | Status | Meaning | |--------|-----------------| | `200` | Subscriber list | -| `400` | Invalid ID | | `500` | Internal error | **Response body** @@ -201,7 +221,7 @@ List all subscribers of a mailing list. ## POST `/lists/{name}/send` -Render a markdown newsletter and send it to all confirmed subscribers of the named list. +Render a markdown newsletter and send it immediately to all confirmed subscribers of the named list. The sent mail is archived and retrievable via the newsletters endpoints. **Path params** - `name` - list name @@ -223,6 +243,40 @@ Render a markdown newsletter and send it to all confirmed subscribers of the nam --- +## POST `/lists/{name}/schedule` + +Schedule a markdown newsletter for future delivery to all confirmed subscribers of the named list. + +**Path params** +- `name` - list name + +**Body** - `application/json` + +| Field | Type | Required | Description | +|---------------|---------|----------|------------------------------------------| +| `raw` | string | yes | Raw markdown content of the mail | +| `scheduledAt` | integer | yes | Delivery time as a unix timestamp (UTC) | + +**Responses** + +| Status | Meaning | +|--------|----------------------------------| +| `201` | Scheduled mail created | +| `400` | Missing `raw` or `scheduledAt` | +| `500` | Internal error | + +**Response body** +```json +{ + "id": 3, + "mailingListName": "newsletter", + "scheduledAt": 1776042000, + "sentAt": null +} +``` + +--- + ## POST `/mail/test` Send a rendered test mail to a single recipient without touching any list. @@ -243,3 +297,221 @@ Send a rendered test mail to a single recipient without touching any list. | `200` | Test mail sent | | `400` | Missing `recipient.email` or `raw` | | `500` | Internal error | + +--- + +## GET `/newsletters` + +List all archived sent newsletters (summary, no recipient list). + +**Responses** + +| Status | Meaning | +|--------|----------------| +| `200` | Newsletter list| +| `500` | Internal error | + +**Response body** +```json +[ + { + "id": 1, + "subject": "Issue #12", + "senderName": "The Team", + "sentAt": "2026-04-14T10:00:00Z", + "mailingLists": ["newsletter"] + } +] +``` + +--- + +## GET `/newsletters/{id}` + +Get a single archived newsletter including full recipient list and raw markdown. + +**Path params** +- `id` - numeric newsletter ID + +**Responses** + +| Status | Meaning | +|--------|---------------------| +| `200` | Newsletter detail | +| `400` | Invalid ID | +| `404` | Newsletter not found| + +**Response body** +```json +{ + "id": 1, + "subject": "Issue #12", + "senderName": "The Team", + "rawMarkdown": "# Issue #12\nHello...", + "sentAt": "2026-04-14T10:00:00Z", + "recipients": [ + { "id": 1, "name": "Alice", "email": "alice@example.com", "confirmed": true } + ], + "mailingLists": ["newsletter"] +} +``` + +--- + +## DELETE `/newsletters/{id}` + +Delete an archived newsletter record. Does not affect any subscribers or scheduled mails. + +**Path params** +- `id` - numeric newsletter ID + +**Responses** + +| Status | Meaning | +|--------|----------------| +| `204` | Deleted | +| `400` | Invalid ID | +| `500` | Internal error | + +--- + +## GET `/scheduled` + +List all scheduled mails (including already-sent ones). + +**Responses** + +| Status | Meaning | +|--------|-----------------------| +| `200` | Scheduled mail list | +| `500` | Internal error | + +**Response body** +```json +[ + { + "id": 3, + "mailingListName": "newsletter", + "scheduledAt": 1776042000, + "sentAt": null + }, + { + "id": 1, + "mailingListName": "announcements", + "scheduledAt": 1775000000, + "sentAt": 1775000060 + } +] +``` + +`sentAt` is `null` for pending mails and a unix timestamp once delivered. + +--- + +## GET `/scheduled/{id}` + +Get a single scheduled mail. + +**Path params** +- `id` - numeric scheduled mail ID + +**Responses** + +| Status | Meaning | +|--------|----------------------------| +| `200` | Scheduled mail detail | +| `400` | Invalid ID | +| `404` | Scheduled mail not found | + +**Response body** +```json +{ + "id": 3, + "mailingListName": "newsletter", + "scheduledAt": 1776042000, + "sentAt": null +} +``` + +--- + +## DELETE `/scheduled/{id}` + +Delete a scheduled mail. Has no effect if the mail has already been sent. + +**Path params** +- `id` - numeric scheduled mail ID + +**Responses** + +| Status | Meaning | +|--------|----------------| +| `204` | Deleted | +| `400` | Invalid ID | +| `500` | Internal error | + +--- + +## PUT `/scheduled/{id}/schedule` + +Change the delivery time of a scheduled mail. + +**Path params** +- `id` - numeric scheduled mail ID + +**Body** - `application/json` + +| Field | Type | Required | Description | +|---------------|---------|----------|-----------------------------------------| +| `scheduledAt` | integer | yes | New delivery time as a unix timestamp (UTC) | + +**Responses** + +| Status | Meaning | +|--------|-----------------------------| +| `200` | Updated scheduled mail | +| `400` | Missing/invalid `scheduledAt` | +| `500` | Internal error | + +**Response body** +```json +{ + "id": 3, + "mailingListName": "newsletter", + "scheduledAt": 1776128400, + "sentAt": null +} +``` + +--- + +## PUT `/scheduled/{id}/content` + +Replace the markdown content of a scheduled mail. + +**Path params** +- `id` - numeric scheduled mail ID + +**Body** - `application/json` + +| Field | Type | Required | Description | +|-------|--------|----------|--------------------------| +| `raw` | string | yes | New raw markdown content | + +**Responses** + +| Status | Meaning | +|--------|------------------------| +| `200` | Updated scheduled mail | +| `400` | Missing `raw` | +| `500` | Internal error | + +**Response body** +```json +{ + "id": 3, + "mailingListName": "newsletter", + "scheduledAt": 1776042000, + "sentAt": null +} +```