# Internal Endpoints
These endpoints power the RelayBook app and are authenticated with **JWT tokens** (not API keys). They provide full campaign management, email settings, image uploads, and other app-specific functionality.
> [!quote] The Full Feature Set
> While the [[Introduction|public API]] covers core CRUD operations, the internal API provides ==the complete campaign workflow== - stats, step management, test emails, and more.
## Authentication
Internal endpoints require a JWT token obtained by logging in through the app:
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```
Most endpoints also require the `X-Book-ID` header, which is automatically set by the app based on the currently selected book.
## Role Requirements
| Role | Access |
|------|--------|
| **Viewer** | Read-only endpoints (list, get, stats) |
| **Member** | Full read/write access |
| **Owner** | Email settings write access |
Campaign endpoints additionally require the **Campaigns add-on** to be enabled for the team.
---
## Campaign Step Management
These endpoints let you update, delete, and reorder steps within a campaign. For creating steps, see the [[Campaigns#Create Step|public API]].
### Update Step
Update one or more fields on a campaign step. Only include the fields you want to change.
```
PATCH /api/campaigns/{id}/steps/{sid}
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
| `sid` | UUID | Step ID |
#### Request Body
All fields are optional. Only provided fields are updated.
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Step name |
| `active` | boolean | Enable or disable the step |
| `schedule_type` | string | `immediate`, `delay`, or `fixed_date` |
| `delay_days` | integer | Days to wait |
| `fixed_date` | string | Target date (YYYY-MM-DD) |
| `email_template_id` | UUID | Template to use |
| `sender_name` | string | From name override |
| `sender_email` | string | From email override |
| `trigger_campaign_id` | UUID | Campaign to trigger |
#### Example Request
```bash
curl -X PATCH "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/steps/STEP_ID" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Book-ID: your-book-id" \
-d '{
"name": "Updated Step Name",
"delay_days": 5,
"active": true
}'
```
#### Response
Returns the full updated step object.
---
### Delete Step
Remove a step from a campaign.
```
DELETE /api/campaigns/{id}/steps/{sid}
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
| `sid` | UUID | Step ID |
#### Example Request
```bash
curl -X DELETE "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/steps/STEP_ID" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": {
"message": "Step deleted"
}
}
```
---
### Reorder Steps
Set the sort order of all steps in a campaign by providing the step IDs in the desired order.
```
PATCH /api/campaigns/{id}/steps/reorder
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `step_ids` | array of UUIDs | Yes | Step IDs in desired order |
#### Example Request
```bash
curl -X PATCH "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/steps/reorder" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Book-ID: your-book-id" \
-d '{
"step_ids": ["step-3-uuid", "step-1-uuid", "step-2-uuid"]
}'
```
#### Response
```json
{
"data": {
"message": "Steps reordered"
}
}
```
---
## Campaign Participants (Extended)
The public API supports [[Campaigns#Enroll Contacts|enrolling contacts by ID]]. The internal API adds listing, label-based enrollment, removal, and history.
### List Participants
Retrieve a paginated list of participants in a campaign.
```
GET /api/campaigns/{id}/participants
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
#### Query Parameters
| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| `page` | integer | 1 | - | Page number |
| `limit` | integer | 50 | 100 | Items per page |
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/participants?page=1&limit=25" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": [
{
"id": "participant-uuid",
"campaign_id": "campaign-uuid",
"contact_id": "contact-uuid",
"status": "active",
"enrolled_at": "2026-03-20T10:00:00Z",
"enrolled_by": "user-uuid",
"completed_at": null,
"contact_name": "Jane Cooper",
"contact_email": "
[email protected]"
}
],
"meta": {
"page": 1,
"limit": 25,
"total": 150
}
}
```
> [!note] Participant status values
> - `active` - Currently progressing through the campaign
> - `completed` - Finished all steps
> - `removed` - Manually removed or unsubscribed (excluded from listings)
---
### Enroll by Label
Enroll all contacts with a specific label into a campaign. Contacts already enrolled are silently skipped.
```
POST /api/campaigns/{id}/participants/enroll-label
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `label_id` | UUID | Yes | Label whose contacts should be enrolled |
#### Example Request
```bash
curl -X POST "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/participants/enroll-label" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Book-ID: your-book-id" \
-d '{"label_id": "hot-lead-label-uuid"}'
```
#### Response
```json
{
"data": {
"enrolled": 24
}
}
```
---
### Remove Participant
Remove a participant from a campaign. Sets their status to `removed` - they will no longer receive campaign emails.
```
DELETE /api/campaigns/{id}/participants/{pid}
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
| `pid` | UUID | Participant ID |
#### Example Request
```bash
curl -X DELETE "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/participants/PARTICIPANT_ID" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": {
"message": "Participant removed"
}
}
```
---
### Participant History
View the step completion history for a specific participant - which steps were executed, when, and whether they succeeded or failed.
```
GET /api/campaigns/{id}/participants/{pid}/history
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
| `pid` | UUID | Participant ID |
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/participants/PARTICIPANT_ID/history" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": [
{
"id": "completion-uuid",
"step_id": "step-uuid",
"step_name": "Welcome Email",
"step_type": "email",
"sort_order": 0,
"completed_at": "2026-03-20T10:15:00Z",
"result_type": "success",
"result_detail": ""
},
{
"id": "completion-uuid-2",
"step_id": "step-uuid-2",
"step_name": "Follow-Up Tips",
"step_type": "email",
"sort_order": 1,
"completed_at": "2026-03-23T10:15:00Z",
"result_type": "failed",
"result_detail": "No email address"
}
]
}
```
---
## Campaign Stats
Get aggregate statistics for a campaign, including per-step breakdowns of success and failure counts.
```
GET /api/campaigns/{id}/stats
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/stats" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": {
"summary": {
"total_participants": 150,
"active_count": 85,
"completed_count": 60,
"total_steps": 3,
"total_completions": 320,
"failed_count": 5
},
"steps": [
{
"step_id": "step-uuid-1",
"step_name": "Welcome Email",
"sort_order": 0,
"success_count": 145,
"failed_count": 3
},
{
"step_id": "step-uuid-2",
"step_name": "Follow-Up Tips",
"sort_order": 1,
"success_count": 110,
"failed_count": 2
},
{
"step_id": "step-uuid-3",
"step_name": "Check-In",
"sort_order": 2,
"success_count": 65,
"failed_count": 0
}
]
}
}
```
---
## Step Contacts
View which contacts completed a specific step, with optional filtering by result type.
```
GET /api/campaign-step-contacts/{id}/{sid}
```
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
| `sid` | UUID | Step ID |
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `result` | string | (all) | Filter by `success` or `failed` |
| `page` | integer | 1 | Page number |
| `limit` | integer | 50 | Items per page (max 100) |
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/campaign-step-contacts/CAMPAIGN_ID/STEP_ID?result=failed&page=1" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": [
{
"id": "completion-uuid",
"completed_at": "2026-03-23T10:15:00Z",
"result_type": "failed",
"result_detail": "No email address",
"contact_id": "contact-uuid",
"contact_name": "John Smith",
"contact_email": ""
}
],
"meta": {
"page": 1,
"limit": 50,
"total": 3
}
}
```
---
## Email Settings
Campaign email settings are configured per book. They control the sender identity, company address (for CAN-SPAM compliance), and SendGrid API key for email delivery.
### Get Email Settings
Retrieve the current email settings for the book. The SendGrid API key is masked (only the last 4 characters are shown).
```
GET /api/campaign-settings
```
**Auth:** JWT (Member+)
**Role:** Member or above
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/campaign-settings" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": {
"id": "settings-uuid",
"book_id": "book-uuid",
"sender_name": "Acme Team",
"sender_email": "
[email protected]",
"reply_to_email": "
[email protected]",
"company_name": "Acme Inc",
"address_line1": "123 Main St",
"address_line2": "",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US",
"sendgrid_api_key": "****abcd",
"created_at": "2026-03-20T10:00:00Z",
"updated_at": "2026-03-23T14:30:00Z"
}
}
```
> [!note] Empty settings
> If email settings have not been configured for the book, an empty object with only `book_id` set is returned.
---
### Save Email Settings
Create or update email settings for the book. Uses upsert - creates the record if it doesn't exist, updates it if it does.
```
PUT /api/campaign-settings
```
**Auth:** JWT (Owner only)
**Role:** Owner
#### Request Body
| Field | Type | Max Length | Description |
|-------|------|-----------|-------------|
| `sender_name` | string | 200 | Display name on sent emails |
| `sender_email` | string | 254 | From address (must be verified in SendGrid) |
| `reply_to_email` | string | 254 | Reply-to address |
| `company_name` | string | 200 | Company name for email footer |
| `address_line1` | string | 200 | Street address line 1 |
| `address_line2` | string | 200 | Street address line 2 |
| `city` | string | 100 | City |
| `state` | string | 100 | State or province |
| `postal_code` | string | 20 | ZIP or postal code |
| `country` | string | 100 | Country |
| `sendgrid_api_key` | string | - | SendGrid API key for sending |
#### Example Request
```bash
curl -X PUT "https://app.relaybook.io/api/campaign-settings" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Book-ID: your-book-id" \
-d '{
"sender_name": "Acme Team",
"sender_email": "
[email protected]",
"reply_to_email": "
[email protected]",
"company_name": "Acme Inc",
"address_line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US",
"sendgrid_api_key": "SG.xxxxx"
}'
```
#### Response
Returns the saved settings with the API key masked.
> [!tip] Preserving the API key
> If you send the masked value (`****abcd`) as the `sendgrid_api_key`, the existing key is preserved. This lets you update other fields without re-entering the key.
---
## Send Test Email
Send a test email using a template. The email subject is prefixed with `[TEST]` and merge fields are populated with sample data from the first contact in the book.
```
POST /api/campaign-templates/{id}/send-test
```
**Auth:** JWT (Member+)
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Template ID |
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | Yes | Recipient email address for the test |
#### Example Request
```bash
curl -X POST "https://app.relaybook.io/api/campaign-templates/TEMPLATE_ID/send-test" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Book-ID: your-book-id" \
-d '{"email": "
[email protected]"}'
```
#### Response
```json
{
"data": {
"message": "Test email sent to
[email protected]"
}
}
```
> [!note] Test data
> Test emails use data from the first contact in the book for merge field substitution. Custom fields without real data show a placeholder URL.
---
## Run Campaign Now
Trigger the campaign scheduler immediately for a specific campaign instead of waiting for the next scheduled run (every 15 minutes).
```
POST /api/campaigns/{id}/run
```
**Auth:** JWT (Member+) - requires paid Campaigns add-on (not trial)
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Campaign ID |
#### Example Request
```bash
curl -X POST "https://app.relaybook.io/api/campaigns/CAMPAIGN_ID/run" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": {
"message": "Campaign processing started"
}
}
```
> [!note] Requirements
> The campaign must be in **active** status to run. Processing happens asynchronously in the background.
#### Errors
| Status | Code | Cause |
|--------|------|-------|
| 400 | `INVALID_STATUS` | Campaign is not active |
| 403 | `ADDON_REQUIRED` | Paid Campaigns add-on required (trial accounts cannot run campaigns) |
| 404 | `NOT_FOUND` | Campaign doesn't exist in this book |
---
## Campaign Image Upload
Upload an image for use in email templates. Images are stored in S3 and served via a public URL.
```
POST /api/campaign-images
```
**Auth:** JWT (Member+)
**Content-Type:** `multipart/form-data`
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `image` | file | Yes | Image file (JPG, PNG, GIF, or WebP) |
#### Limits
- Maximum file size: **5 MB**
- Allowed formats: JPG, PNG, GIF, WebP (validated by content detection, not extension)
#### Example Request
```bash
curl -X POST "https://app.relaybook.io/api/campaign-images" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id" \
-F "image=@/path/to/email-banner.png"
```
#### Response
```json
{
"data": {
"url": "https://your-bucket.s3.us-east-1.amazonaws.com/campaign-images/book-id/1711234567890.png"
}
}
```
Use the returned URL in your email template HTML (e.g., `<img src="...">`).
---
## Contact Campaigns
List all campaigns a specific contact is enrolled in.
```
GET /api/contacts/{id}/campaigns
```
**Auth:** JWT (Viewer+)
#### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | UUID | Contact ID |
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/contacts/CONTACT_ID/campaigns" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response
```json
{
"data": [
{
"id": "participant-uuid",
"campaign_id": "campaign-uuid",
"campaign_name": "Onboarding Sequence",
"description": "Welcome new users",
"campaign_status": "active",
"participant_status": "active",
"enrolled_at": "2026-03-20T10:00:00Z",
"completed_at": null
}
]
}
```
---
## Check Duplicate Email
Check if an email address already exists in the book. Used during contact creation to warn about duplicates.
```
GET /api/contacts/check-email
```
**Auth:** JWT (Viewer+)
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `email` | string | Yes | Email address to check |
| `exclude` | UUID | No | Contact ID to exclude (for edit forms) |
#### Example Request
```bash
curl -X GET "https://app.relaybook.io/api/contacts/
[email protected]" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "X-Book-ID: your-book-id"
```
#### Response (duplicate found)
```json
{
"data": {
"exists": true,
"enforced": false,
"contact_id": "existing-contact-uuid",
"name": "Jane Cooper"
}
}
```
#### Response (no duplicate)
```json
{
"data": {
"exists": false,
"enforced": false
}
}
```
The `enforced` field indicates whether the book has unique email enforcement enabled. When `true`, contacts with duplicate emails cannot be created.
---
## Unsubscribe (Public)
These endpoints handle email unsubscribe flows. They are **public** - no authentication is required. They use a unique token generated when a campaign email is sent.
### Show Unsubscribe Page
```
GET /api/unsubscribe?token=UNSUBSCRIBE_TOKEN
```
Displays an HTML page listing the contact's active campaigns with options to unsubscribe from individual campaigns or all emails.
### Confirm Unsubscribe
```
POST /api/unsubscribe/confirm
```
#### Request Body (form or query params)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | string | Yes | Unsubscribe token |
| `scope` | string | No | `campaign` (default) or `all` |
| `campaign_id` | UUID | Conditional | Required when scope is `campaign` |
When `scope=all`, the contact is removed from all campaigns in the book and will not receive any future campaign emails.
---
## Stripe Campaign Checkout
Create a Stripe checkout session for purchasing the Campaigns add-on.
```
POST /api/stripe/create-campaign-checkout
```
**Auth:** JWT
**Rate limit:** 5 requests per minute per IP
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `plan` | string | Yes | Plan identifier |
| `currency` | string | Yes | Currency code (e.g., `usd`) |
#### Example Request
```bash
curl -X POST "https://app.relaybook.io/api/stripe/create-campaign-checkout" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"plan": "campaign_addon", "currency": "usd"}'
```
#### Response
```json
{
"data": {
"url": "https://checkout.stripe.com/c/pay/cs_live_..."
}
}
```
Redirect the user to the returned URL to complete the checkout flow.
---
**Back to:** [[Introduction|API Reference Introduction]]