Schedule & Send
Set schedule intent, then create an execution job for a future send or an immediate send.
Base URL: https://api.experiture.ai/public/v1
Set Schedule Intent
Stores when a draft campaign should send. This does not publish, schedule, or send the campaign.
The request body is channel-dependent: direct_mail campaigns use a batch-date scheduling model; all other channels (email, SMS, push) use a timestamp model.
PUT /campaigns/:id/schedule
Authorization: Bearer <token>
Content-Type: application/jsonRequired scope: campaigns:update
Schedule intent can only be updated while the campaign is in draft status.
Digital channels (email, sms, push)
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | "now" or "at". |
sendAt | string | null | Conditional | ISO 8601 timestamp. Required when mode is "at". |
timezone | string | null | No | IANA timezone string. Defaults to "UTC". |
Example — email, future send
curl -X PUT https://api.experiture.ai/public/v1/campaigns/4130bada-9264-465f-bc0c-a26bebcfcc81/schedule \
-H "Authorization: Bearer <your_access_token>" \
-H "Content-Type: application/json" \
-d '{
"mode": "at",
"sendAt": "2026-05-01T09:00:00.000Z",
"timezone": "America/New_York"
}'Direct mail
Direct mail uses a date-only schedule — the channel's fulfillment pipeline does not accept an intraday time.
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | Must be "batch_on_date". |
batchDate | string | Yes | Target fulfillment date in YYYY-MM-DD format. |
timezone | string | null | No | IANA timezone used to interpret the date. Defaults to tenant default, then "UTC". |
Example — direct mail
curl -X PUT https://api.experiture.ai/public/v1/campaigns/4130bada-9264-465f-bc0c-a26bebcfcc81/schedule \
-H "Authorization: Bearer <your_access_token>" \
-H "Content-Type: application/json" \
-d '{
"mode": "batch_on_date",
"batchDate": "2026-05-01",
"timezone": "America/New_York"
}'Response — 200 OK — same shape as Get Campaign. The schedule object in the response includes a kind discriminator: "digital_send" for digital channels or "direct_mail_batch" for direct mail. See Schedule object variants below.
Errors
| Code | HTTP | Meaning |
|---|---|---|
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_FOUND | 404 | Campaign not found. |
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_DRAFT | 409 | Campaign is not a draft. |
JOURNEY_API.SCHEDULE.SEND_AT_REQUIRED | 400 | mode is "at" but sendAt is missing. |
JOURNEY_API.SCHEDULE.SEND_AT_INVALID | 400 | sendAt is present but malformed or in the past. |
JOURNEY_API.SCHEDULE.BATCH_DATE_REQUIRED | 400 | mode is "batch_on_date" but batchDate is missing. |
JOURNEY_API.SCHEDULE.BATCH_DATE_INVALID | 400 | batchDate is present but malformed or in the past. |
JOURNEY_API.SCHEDULE.UNSUPPORTED_CHANNEL | 400 | Channel is not supported for the requested schedule mode. |
JOURNEY_API.AUTH.INSUFFICIENT_SCOPE | 403 | Token lacks campaigns:update. |
Schedule Object Variants
The schedule object in campaign responses uses a kind discriminator to indicate which scheduling model applies.
Digital channels
{
"kind": "digital_send",
"mode": "at",
"sendAt": "2026-05-01T09:00:00.000Z",
"timezone": "America/New_York",
"scheduled": true
}Direct mail
{
"kind": "direct_mail_batch",
"mode": "batch_on_date",
"batchDate": "2026-05-01",
"timezone": "America/New_York",
"timezoneSource": "request",
"scheduled": true
}timezoneSource indicates how the timezone was resolved:
| Value | Meaning |
|---|---|
"request" | Timezone was explicitly supplied in the request. |
"tenant_default" | Timezone was not supplied; tenant default was applied. |
"utc_fallback" | No request timezone and no tenant default; UTC was used. |
Create Schedule Job
Commits a draft campaign to a future execution. For digital campaigns, if sendAt is omitted the API uses the saved schedule intent. For direct mail campaigns, use batchDate.
POST /campaigns/:id/schedule-jobs
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: <uuid-v4> (optional, 24-hour deduplication)Required scope: campaigns:schedule
The campaign must be a draft with at least one included audience and a published template bound before a schedule job can be created.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
sendAt | string | null | No | ISO 8601 timestamp for digital channels. If omitted, uses saved schedule intent. Not used for direct mail. |
batchDate | string | null | No | YYYY-MM-DD fulfillment date for direct mail. If omitted, uses saved schedule intent. Not used for digital channels. |
timezone | string | null | No | IANA timezone string. Defaults to saved schedule timezone or "UTC". |
dryRun | boolean | No | If true, validates and returns 200 without committing execution artifacts. |
Direct Mail production override fields (optional)
For direct mail campaigns, the schedule job request can override the tenant's default production settings on a per-send basis. Each field is optional and falls through to the tenant's active default profile assignment when omitted.
| Field | Type | Description |
|---|---|---|
printProfileId | string | null | Override the tenant's default print profile for this send. |
postalProfileId | string | null | Override the tenant's default postal profile for this send. |
productionFormatKey | string | null | Explicit postal layout key (e.g. "letter_8.5x11", "letter_8.5x11_window_envelope"). Defaults to the layout selected on the bound template. |
mailClass | string | null | Override the mail class (e.g. "first_class", "marketing_mail"). |
processingCategory | string | null | Override the USPS processing category (e.g. "automation_letter", "retail_card_price_postcard"). |
artifactResolutionMode | enum | null | How to resolve execution artifacts: "latest_live" (default — use the most recent live artifact set), "approved_preparation_set" (pin to a specific approved set, requires executionArtifactPreparationSetId), or "none" (skip artifact preparation, e.g. for proof-only flows). |
executionArtifactPreparationSetId | string | null | UUID of an approved Execution Artifact Preparation Set. Required when artifactResolutionMode is "approved_preparation_set". |
Dry-run response — 200 OK
{
"success": true,
"data": {
"journeyId": "4130bada-9264-465f-bc0c-a26bebcfcc81",
"versionId": "11111111-1111-1111-1111-111111111111",
"status": "published",
"dryRun": true,
"warnings": [],
"bindings": [
{
"journey_version_id": "11111111-1111-1111-1111-111111111111",
"node_key": "send_1",
"manifest_id": "33333333-3333-3333-3333-333333333333",
"placeholder_manifest_id": null,
"touchpoint_id": 123,
"channel": "email"
}
]
}
}Async Finalization Response
Schedule and send jobs now finalize asynchronously by default. The API queues a finalization job, persists the schedule intent, and returns immediately with a 202 Accepted. The actual schedule binding (manifest, schedule, touchpoint) is created by the broadcast finalization worker; observe completion via GET /campaigns/:id/status.
Async response — 202 Accepted
{
"success": true,
"data": {
"journeyId": "4130bada-9264-465f-bc0c-a26bebcfcc81",
"versionId": "11111111-1111-1111-1111-111111111111",
"status": "finalizing",
"jobId": "9b7d4f8c-0e1a-4f00-9b8a-cccccccccccc",
"pollUrl": "/authoring/broadcasts/jobs/9b7d4f8c-0e1a-4f00-9b8a-cccccccccccc",
"warnings": [],
"bindings": [],
"sendAt": "2026-05-01T09:00:00.000Z",
"timezone": "America/New_York",
"schedule": {
"kind": "digital_send",
"mode": "at",
"sendAt": "2026-05-01T09:00:00.000Z",
"timezone": "America/New_York"
}
}
}| Field | Type | Description |
|---|---|---|
status | string | "finalizing" while the worker is processing; transitions to "published" once the schedule is bound. |
jobId | string | Tenant-scoped finalization job identifier. Stable for idempotent retries within the 24-hour idempotency window. |
pollUrl | string | Internal authoring tracking URL. Public consumers should poll GET /campaigns/:id/status instead. |
bindings | array | Empty during finalization. Populates with manifest/schedule/touchpoint refs after the worker completes. |
schedule | object | The normalized schedule intent the worker will apply. Channel-specific shape — see Schedule Object Variants. |
sendAt / batchDate | string | The resolved send time (digital) or fulfillment date (direct mail) the worker will use. |
Re-issuing a request while a finalization is already in flight returns 202 Accepted with the existing jobId and status: "finalizing" — the API never enqueues a duplicate job for the same campaign while a previous one is unresolved.
Synchronous Response (Dry Run only)
When dryRun: true is supplied, the API runs the full validation path inline and returns a 200 OK with status: "published" to indicate the inputs would have produced a valid binding. No execution artifacts are written.
Dry-run committed response shape — 200 OK (digital)
{
"success": true,
"data": {
"journeyId": "4130bada-9264-465f-bc0c-a26bebcfcc81",
"versionId": "11111111-1111-1111-1111-111111111111",
"status": "published",
"dryRun": true,
"warnings": [],
"bindings": [
{
"journey_version_id": "11111111-1111-1111-1111-111111111111",
"node_key": "send_1",
"manifest_id": "33333333-3333-3333-3333-333333333333",
"placeholder_manifest_id": null,
"touchpoint_id": 123,
"channel": "email"
}
],
"manifestId": "33333333-3333-3333-3333-333333333333",
"scheduleId": "44444444-4444-4444-4444-444444444444",
"touchpointId": 123,
"sendAt": "2026-05-01T09:00:00.000Z",
"timezone": "America/New_York"
}
}Dry-run committed response shape — 200 OK (direct mail)
{
"success": true,
"data": {
"journeyId": "4130bada-9264-465f-bc0c-a26bebcfcc81",
"versionId": "11111111-1111-1111-1111-111111111111",
"status": "published",
"dryRun": true,
"warnings": [],
"bindings": [
{
"journey_version_id": "11111111-1111-1111-1111-111111111111",
"node_key": "send_1",
"manifest_id": "33333333-3333-3333-3333-333333333333",
"placeholder_manifest_id": null,
"touchpoint_id": 123,
"channel": "direct_mail"
}
],
"manifestId": "33333333-3333-3333-3333-333333333333",
"scheduleId": "44444444-4444-4444-4444-444444444444",
"touchpointId": 123,
"batchDate": "2026-05-01",
"timezone": "America/New_York"
}
}Migration note (May 2026): Prior to this release, non-dry-run schedule and send jobs returned 201 Created synchronously after binding the schedule. Clients should now expect 202 Accepted with status: "finalizing" and poll /campaigns/:id/status for the bound state. Consumers parsing manifestId, scheduleId, or touchpointId from the immediate response should read those fields from the status endpoint instead. The on-the-wire response for dryRun: true is unchanged.
Create Send Job
Commits a draft campaign to immediate execution.
POST /campaigns/:id/send-jobs
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: <uuid-v4> (optional, 24-hour deduplication)Required scope: campaigns:send
Not supported for direct mail. This endpoint returns 400 JOURNEY_API.SCHEDULE.INVALID_FOR_CHANNEL when the campaign channel is direct_mail. Direct mail requires a fulfillment date and cannot be triggered as an immediate send. Use POST /schedule-jobs with a batchDate instead.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
dryRun | boolean | No | If true, validates and returns 200 without committing execution artifacts. |
Response — 202 Accepted when the finalization is queued, 200 OK for dry runs. Same shape as Create Schedule Job.
Execution Errors
| Code | HTTP | Meaning |
|---|---|---|
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_FOUND | 404 | Campaign not found. |
JOURNEY_API.PUBLIC.CAMPAIGNS.TYPE_UNSUPPORTED | 409 | Only broadcast campaigns can be scheduled or sent in v1. |
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_DRAFT | 409 | Campaign is not a draft. |
JOURNEY_API.PUBLIC.CAMPAIGNS.CONTENT.REQUIRED | 409 | Published template/content is not configured. |
JOURNEY_API.PUBLIC.CAMPAIGNS.AUDIENCE.REQUIRED | 409 | At least one included audience is required. |
JOURNEY_API.PUBLIC.CAMPAIGNS.VERSION_MISSING | 409 | Campaign has no current version. |
JOURNEY_API.SCHEDULE.SEND_AT_REQUIRED | 400 | Future schedule execution has no send time. |
JOURNEY_API.SCHEDULE.SEND_AT_INVALID | 400 | sendAt is malformed or in the past. |
JOURNEY_API.SCHEDULE.BATCH_DATE_REQUIRED | 400 | Direct mail schedule has no batch date. |
JOURNEY_API.SCHEDULE.BATCH_DATE_INVALID | 400 | batchDate is malformed or in the past. |
JOURNEY_API.SCHEDULE.INVALID_FOR_CHANNEL | 400 | Operation is not permitted for this campaign's channel (e.g. immediate send on direct mail). |
JOURNEY_API.SCHEDULE.UNSUPPORTED_CHANNEL | 400 | Channel is not supported for the requested schedule mode. |
JOURNEY_API.SCHEDULE.UNSUPPORTED_TYPE | 500 | Internal: unsupported schedule type encountered. Contact support if this persists. |
JOURNEY_API.BROADCASTS.SCHEDULE.PREFLIGHT_BLOCKERS | 400 | Preflight found blocking issues. |
JOURNEY_API.BROADCASTS.FINALIZATION_QUEUE.PUBLISH_FAILED | 500 | The finalization queue could not accept the job. The schedule intent is persisted; retry the request after a short delay. |
JOURNEY_API.BROADCASTS.FINALIZATION_JOB.INVALID_PAYLOAD | 500 | Internal: a malformed finalization job was rejected by the worker. Contact support. |
JOURNEY_API.AUTH.INSUFFICIENT_SCOPE | 403 | Token lacks the required scope. |
Schedule vs Send
POST /schedule-jobs | POST /send-jobs | |
|---|---|---|
| Scope | campaigns:schedule | campaigns:send |
| Timing | Future sendAt (digital) or batchDate (direct mail) | Immediate |
| Channels supported | All (email, sms, push, direct_mail, landing_page) | Digital only (email, sms, push) |
| Uses saved schedule intent | Yes, when sendAt/batchDate is omitted | No |
| dryRun supported | Yes | Yes |
| Idempotency | Yes | Yes |
See Also
- Preflight - validate readiness before execution
- Status & Metrics - observe execution after send
- Campaign Runs - inspect per-run packages and request additional runs against a published campaign (recurring drops use
POST /campaigns/:id/runs, not repeated/schedule-jobs) - Campaign Object - create and configure the campaign