# 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]]