Skip to content

REST API Reference

SeeSee provides a REST API under /api/v1/. All requests and responses use JSON.

FastAPI auto-generates interactive OpenAPI documentation at /docs on your running instance.

Authentication

SeeSee uses two authentication methods:

ContextMethodHeader
Per-app endpoints (logging emails)Bearer tokenAuthorization: Bearer ss_...
Admin endpoints (managing apps, querying emails)HTTP Basic AuthAuthorization: Basic base64(user:pass)

API keys are prefixed with ss_ and are generated when you create an app. They are shown once — store them securely.

Error responses

All errors follow a consistent format:

{
"error": "Short error description",
"detail": "More detailed explanation (optional)"
}

Common HTTP status codes:

CodeMeaning
200Success
201Created
400Bad request (validation error)
401Unauthorized (missing or invalid credentials)
404Not found
422Unprocessable entity (invalid request body)
500Internal server error

Health

GET /api/v1/health

Returns service status and database health. No authentication required.

Terminal window
curl http://localhost:8080/api/v1/health
{
"status": "ok",
"version": "0.18.0",
"database": "ok"
}

Apps

All app endpoints require admin Basic Auth.

Create app

POST /api/v1/apps

Create a new app and receive its API key and SMTP credentials.

Terminal window
curl -X POST http://localhost:8080/api/v1/apps \
-u admin:your-password \
-H "Content-Type: application/json" \
-d '{
"name": "My Website",
"body_storage_mode": "full"
}'

Request body:

FieldTypeRequiredDefaultDescription
namestringyesDisplay name for the app
body_storage_modestringnofullHow to store email bodies: full, text_only, or preview
retention_max_countintnoPer-app email count limit (overrides global)
retention_max_age_daysintnoPer-app max age in days (overrides global)
retention_degrade_to_text_daysintnoStrip HTML after N days, 0 = never (overrides global)
retention_degrade_to_preview_daysintnoStrip text after N days, 0 = never (overrides global)

Response (201):

{
"id": 1,
"name": "My Website",
"slug": "my-website",
"body_storage_mode": "full",
"retention_max_count": null,
"retention_max_age_days": null,
"last_activity_at": null,
"created_at": "2025-01-15T10:30:00Z",
"api_key": "ss_abc123...",
"smtp_username": "my-website",
"smtp_password": "xyz789..."
}

List apps

GET /api/v1/apps

Terminal window
curl http://localhost:8080/api/v1/apps \
-u admin:your-password

Response (200):

[
{
"id": 1,
"name": "My Website",
"slug": "my-website",
"body_storage_mode": "full",
"retention_max_count": null,
"retention_max_age_days": null,
"last_activity_at": "2025-01-15T12:00:00Z",
"created_at": "2025-01-15T10:30:00Z"
}
]

Update app

PATCH /api/v1/apps/{app_id}

Update app settings. All fields are optional.

Terminal window
curl -X PATCH http://localhost:8080/api/v1/apps/1 \
-u admin:your-password \
-H "Content-Type: application/json" \
-d '{
"body_storage_mode": "text_only",
"retention_max_count": 500
}'

Request body:

FieldTypeDescription
namestringUpdate display name
body_storage_modestringfull, text_only, or preview
retention_max_countintPer-app email count limit
retention_max_age_daysintPer-app max age in days
retention_degrade_to_text_daysintStrip HTML after N days (0 = never)
retention_degrade_to_preview_daysintStrip text after N days (0 = never)

Rotate API key

POST /api/v1/apps/{app_id}/rotate-key

Regenerate the app’s API key. The old key stops working immediately.

Terminal window
curl -X POST http://localhost:8080/api/v1/apps/1/rotate-key \
-u admin:your-password

Response (200):

{
"api_key": "ss_new_key_here...",
"message": "API key rotated successfully"
}

Delete app

DELETE /api/v1/apps/{app_id}

Permanently delete an app and all its emails. Requires admin Basic Auth.

Terminal window
curl -X DELETE http://localhost:8080/api/v1/apps/1 \
-u admin:your-password

Response (200):

{
"message": "App deleted"
}

Email Logging

Log a single email

POST /api/v1/log

Requires app Bearer token authentication.

Terminal window
curl -X POST http://localhost:8080/api/v1/log \
-H "Authorization: Bearer ss_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": ["user@example.com"],
"from": "app@example.com",
"subject": "Welcome!",
"body_html": "<h1>Welcome</h1><p>Thanks for signing up.</p>",
"body_text": "Welcome! Thanks for signing up.",
"status": "sent",
"provider": "resend"
}'

Request body:

FieldTypeRequiredDescription
tostring[]yesRecipient email addresses
fromstringyesSender email address
subjectstringyesEmail subject line
body_htmlstringnoHTML body content
body_textstringnoPlain text body content
statusstringnoEmail status (e.g., sent, failed, queued, bounced)
providerstringnoSending provider (e.g., resend, sendgrid, ses, smtp)
provider_message_idstringnoProvider’s message ID for tracking
error_messagestringnoError details if delivery failed
metadataobjectnoArbitrary key-value metadata
ccstring[]noCC recipients
bccstring[]noBCC recipients
reply_tostringnoReply-to address
tagsstring[]noTags for categorization
logged_atstringnoISO 8601 timestamp (defaults to now)

Response (201):

{
"id": 42,
"status": "sent",
"created_at": "2025-01-15T12:00:00Z"
}

Log a batch of emails

POST /api/v1/log/batch

Log up to 100 emails in a single request. Same auth and fields as the single endpoint.

Terminal window
curl -X POST http://localhost:8080/api/v1/log/batch \
-H "Authorization: Bearer ss_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{
"to": ["user1@example.com"],
"from": "app@example.com",
"subject": "Welcome user 1",
"body_text": "Hello user 1",
"status": "sent"
},
{
"to": ["user2@example.com"],
"from": "app@example.com",
"subject": "Welcome user 2",
"body_text": "Hello user 2",
"status": "sent"
}
]
}'

Response (201):

{
"logged": 2,
"errors": []
}

Email Management

Update email status

PATCH /api/v1/emails/{email_id}/status

Update an email’s status after initial logging. Requires admin Basic Auth.

Use case: provider webhooks update delivery status (e.g., sentdelivered or bounced).

Terminal window
curl -X PATCH http://localhost:8080/api/v1/emails/42/status \
-u admin:your-password \
-H "Content-Type: application/json" \
-d '{"status": "delivered"}'

Request body:

FieldTypeRequiredDescription
statusstringyesNew status value

Response (200): Full email detail object with updated status.

Delete a single email

DELETE /api/v1/emails/{email_id}

Permanently delete a single email. Requires admin Basic Auth.

Terminal window
curl -X DELETE http://localhost:8080/api/v1/emails/42 \
-u admin:your-password

Response (200):

{
"message": "Email deleted"
}

Bulk delete emails

DELETE /api/v1/emails

Delete all emails matching search criteria. At least one filter is required to prevent accidental deletion of all data. Requires admin Basic Auth.

Terminal window
curl -X DELETE "http://localhost:8080/api/v1/emails?app_id=1&status=bounced" \
-u admin:your-password

Query parameters: Same filters as GET /api/v1/emailsq, app_id, status, provider, date_from, date_to.

Response (200):

{
"deleted": 15,
"message": "Deleted 15 emails"
}

Purge all emails for an app

DELETE /api/v1/apps/{app_id}/emails

Delete all emails for a specific app. Requires admin Basic Auth.

Terminal window
curl -X DELETE http://localhost:8080/api/v1/apps/1/emails \
-u admin:your-password

Response (200):

{
"message": "Deleted 42 emails"
}

Email Queries

All query endpoints require admin Basic Auth.

List and search emails

GET /api/v1/emails

Terminal window
curl "http://localhost:8080/api/v1/emails?q=welcome&status=sent&per_page=10" \
-u admin:your-password

Query parameters:

ParameterTypeDefaultDescription
qstringFull-text search query (searches subject, body, addresses, errors)
app_idintFilter by app ID
statusstringFilter by status
providerstringFilter by provider
date_fromstringFilter emails logged after this ISO date
date_tostringFilter emails logged before this ISO date
sortstringlogged_atSort field: logged_at, created_at, subject
orderstringdescSort order: asc or desc
pageint1Page number
per_pageint20Results per page (1–100)

Response (200):

{
"emails": [
{
"id": 42,
"app_id": 1,
"to_addresses": "[\"user@example.com\"]",
"from_address": "app@example.com",
"subject": "Welcome!",
"body_preview": "Thanks for signing up...",
"status": "sent",
"provider": "resend",
"ingest_method": "api",
"logged_at": "2025-01-15T12:00:00Z",
"created_at": "2025-01-15T12:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 20,
"pages": 1
}

Get email detail

GET /api/v1/emails/{email_id}

Terminal window
curl http://localhost:8080/api/v1/emails/42 \
-u admin:your-password

Response (200):

Returns the full email detail including body_html, body_text, body_size_bytes, metadata, cc_addresses, bcc_addresses, reply_to, tags, provider_message_id, and error_message.

Preview email HTML

GET /api/v1/emails/{email_id}/preview

Returns rendered HTML content with security headers (CSP) suitable for iframe embedding. For plain-text emails, the text is wrapped in <pre> tags with HTML escaping.

Terminal window
curl http://localhost:8080/api/v1/emails/42/preview \
-u admin:your-password

Stats

Dashboard statistics

GET /api/v1/stats

Requires admin Basic Auth.

Terminal window
curl http://localhost:8080/api/v1/stats \
-u admin:your-password

Response (200):

{
"total_emails": 1234,
"emails_24h": 45,
"emails_7d": 312,
"emails_30d": 890,
"total_apps": 5,
"by_status": {
"sent": 1100,
"failed": 50,
"queued": 84
},
"by_app": [
{ "id": 1, "name": "My Website", "count": 500 },
{ "id": 2, "name": "Newsletter", "count": 734 }
]
}

Webhooks

Webhook endpoints receive delivery status callbacks from email providers and automatically update the matching email’s status in SeeSee. No admin auth is required — these endpoints authenticate via provider-specific signature/token verification.

Receive webhook

POST /api/v1/webhooks/{provider}

Supported providers: resend, sendgrid. Unknown providers return 404.

Events are matched to stored emails using the provider and provider_message_id fields that were set when the email was originally logged.

Resend setup

  1. In your Resend dashboard, go to Webhooks and add a new endpoint:
    • URL: https://your-seesee-domain/api/v1/webhooks/resend
    • Events: Select the events you want (e.g., email.delivered, email.bounced, email.complained)
  2. Copy the Signing secret (starts with whsec_) and set it as SEESEE_WEBHOOK_SECRET_RESEND

Resend uses Svix HMAC-SHA256 signatures via svix-id, svix-timestamp, and svix-signature headers.

Event mapping:

Resend EventSeeSee Status
email.sentsent
email.delivereddelivered
email.delivery_delayeddelayed
email.bouncedbounced
email.complainedcomplained

Example Resend payload:

{
"type": "email.delivered",
"created_at": "2026-01-01T00:00:00.000Z",
"data": {
"email_id": "d1234567-abcd-1234-efgh-123456789012",
"from": "sender@example.com",
"to": ["user@example.com"],
"subject": "Welcome"
}
}

SendGrid setup

  1. In your SendGrid dashboard, go to Settings > Mail Settings > Event Webhook
  2. Set the HTTP Post URL to: https://your-seesee-domain/api/v1/webhooks/sendgrid?verification_token=YOUR_SECRET
  3. Set the same secret value as SEESEE_WEBHOOK_SECRET_SENDGRID in your SeeSee environment
  4. Select the events you want (e.g., delivered, bounce, spamreport)

Event mapping:

SendGrid EventSeeSee Status
processedsent
delivereddelivered
bouncebounced
droppeddropped
deferreddeferred
spamreportcomplained

Example SendGrid payload:

[
{
"email": "user@example.com",
"timestamp": 1234567890,
"event": "delivered",
"sg_message_id": "abc123.filter0001.16648.5515E0B88.0"
}
]

Webhook response

All successfully parsed webhooks return 200:

{
"processed": 1,
"matched": 1,
"events": [
{
"provider_message_id": "d1234567-abcd-1234-efgh-123456789012",
"event_type": "email.delivered",
"new_status": "delivered",
"email_id": "uuid-of-matched-email",
"matched": true
}
]
}
FieldDescription
processedNumber of events parsed from the payload
matchedNumber of events that matched a stored email
events[].matchedWhether this specific event matched a stored email
events[].email_idThe SeeSee email ID (null if not matched)

Data Export (GDPR)

Export recipient data

GET /api/v1/export

Export all emails associated with a specific recipient email address. Supports GDPR Article 15 (right of access) — when a user requests their data, the admin can export everything SeeSee has logged for that address.

Searches across to_addresses, cc_addresses, and bcc_addresses fields (case-insensitive). Requires admin Basic Auth.

Terminal window
curl "http://localhost:8080/api/v1/export?recipient=user@example.com" \
-u admin:your-password

Query parameters:

ParameterTypeRequiredDescription
recipientstringyesEmail address to export data for
formatstringnoResponse format: json (default) or csv

You can also request CSV format via the Accept: text/csv header instead of the format parameter.

JSON response (200):

{
"recipient": "user@example.com",
"total": 2,
"exported_at": "2026-02-20T12:00:00Z",
"emails": [
{
"id": "abc-123",
"app_id": "def-456",
"to_addresses": ["user@example.com"],
"from_address": "app@example.com",
"subject": "Welcome!",
"body_preview": "Thanks for signing up...",
"body_html": "<h1>Welcome</h1><p>Thanks for signing up.</p>",
"body_text": "Welcome! Thanks for signing up.",
"status": "sent",
"provider": "resend",
"ingest_method": "api",
"logged_at": "2026-02-20T10:00:00",
"cc_addresses": null,
"bcc_addresses": null
}
]
}
FieldDescription
recipientThe email address that was searched
totalNumber of matching emails found
exported_atTimestamp when the export was generated
emailsArray of email records with metadata and body content

CSV response:

Terminal window
curl "http://localhost:8080/api/v1/export?recipient=user@example.com&format=csv" \
-u admin:your-password \
-o export.csv

Returns a CSV file with columns: id, app_id, to_addresses, from_address, subject, body_preview, body_html, body_text, status, provider, ingest_method, logged_at, cc_addresses, bcc_addresses.


Admin

Trigger retention cleanup

POST /api/v1/admin/cleanup

Trigger an immediate retention cleanup cycle. This runs the same cleanup that the background scheduler runs on the configured interval. Requires admin Basic Auth.

Terminal window
curl -X POST http://localhost:8080/api/v1/admin/cleanup \
-u admin:your-password

Response (200):

{
"message": "Cleanup completed"
}

Persistence diagnostics

GET /api/v1/admin/debug/persistence

Return diagnostic information about database persistence and volume mounting. Useful for debugging data loss after container redeploys, especially on platforms like Coolify. Requires admin Basic Auth.

Terminal window
curl http://localhost:8080/api/v1/admin/debug/persistence \
-u admin:your-password

Response (200):

{
"db_path": "/data/seesee.db",
"db_size_bytes": 245760,
"db_modified_at": "2026-02-20T10:30:00+00:00",
"app_count": 3,
"email_count": 1234,
"schema_version": "1",
"oldest_app_created_at": "2026-01-15T10:30:00",
"uptime_seconds": 3661.42,
"hostname": "abc123def456",
"volume_mounted": true,
"mount_info": "device=overlay mount=/data fstype=overlay"
}
FieldDescription
db_pathResolved path to the SQLite database file
db_size_bytesDatabase file size in bytes
db_modified_atLast modification time (ISO 8601)
app_countNumber of registered apps
email_countTotal number of stored emails
schema_versionDatabase schema version
oldest_app_created_atCreation time of the oldest app
uptime_secondsSeconds since application startup
hostnameContainer hostname (container ID in Docker)
volume_mountedWhether /data is a separate mount point
mount_infoFilesystem device and type for the data directory