API Reference
Campaigns
Schedule & Send

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/json

Required scope: campaigns:update

⚠️

Schedule intent can only be updated while the campaign is in draft status.

Digital channels (email, sms, push)

FieldTypeRequiredDescription
modestringYes"now" or "at".
sendAtstring | nullConditionalISO 8601 timestamp. Required when mode is "at".
timezonestring | nullNoIANA 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.

FieldTypeRequiredDescription
modestringYesMust be "batch_on_date".
batchDatestringYesTarget fulfillment date in YYYY-MM-DD format.
timezonestring | nullNoIANA 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

CodeHTTPMeaning
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_FOUND404Campaign not found.
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_DRAFT409Campaign is not a draft.
JOURNEY_API.SCHEDULE.SEND_AT_REQUIRED400mode is "at" but sendAt is missing.
JOURNEY_API.SCHEDULE.SEND_AT_INVALID400sendAt is present but malformed or in the past.
JOURNEY_API.SCHEDULE.BATCH_DATE_REQUIRED400mode is "batch_on_date" but batchDate is missing.
JOURNEY_API.SCHEDULE.BATCH_DATE_INVALID400batchDate is present but malformed or in the past.
JOURNEY_API.SCHEDULE.UNSUPPORTED_CHANNEL400Channel is not supported for the requested schedule mode.
JOURNEY_API.AUTH.INSUFFICIENT_SCOPE403Token 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:

ValueMeaning
"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

FieldTypeRequiredDescription
sendAtstring | nullNoISO 8601 timestamp for digital channels. If omitted, uses saved schedule intent. Not used for direct mail.
batchDatestring | nullNoYYYY-MM-DD fulfillment date for direct mail. If omitted, uses saved schedule intent. Not used for digital channels.
timezonestring | nullNoIANA timezone string. Defaults to saved schedule timezone or "UTC".
dryRunbooleanNoIf 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.

FieldTypeDescription
printProfileIdstring | nullOverride the tenant's default print profile for this send.
postalProfileIdstring | nullOverride the tenant's default postal profile for this send.
productionFormatKeystring | nullExplicit postal layout key (e.g. "letter_8.5x11", "letter_8.5x11_window_envelope"). Defaults to the layout selected on the bound template.
mailClassstring | nullOverride the mail class (e.g. "first_class", "marketing_mail").
processingCategorystring | nullOverride the USPS processing category (e.g. "automation_letter", "retail_card_price_postcard").
artifactResolutionModeenum | nullHow 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).
executionArtifactPreparationSetIdstring | nullUUID 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"
    }
  }
}
FieldTypeDescription
statusstring"finalizing" while the worker is processing; transitions to "published" once the schedule is bound.
jobIdstringTenant-scoped finalization job identifier. Stable for idempotent retries within the 24-hour idempotency window.
pollUrlstringInternal authoring tracking URL. Public consumers should poll GET /campaigns/:id/status instead.
bindingsarrayEmpty during finalization. Populates with manifest/schedule/touchpoint refs after the worker completes.
scheduleobjectThe normalized schedule intent the worker will apply. Channel-specific shape — see Schedule Object Variants.
sendAt / batchDatestringThe 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

FieldTypeRequiredDescription
dryRunbooleanNoIf 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

CodeHTTPMeaning
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_FOUND404Campaign not found.
JOURNEY_API.PUBLIC.CAMPAIGNS.TYPE_UNSUPPORTED409Only broadcast campaigns can be scheduled or sent in v1.
JOURNEY_API.PUBLIC.CAMPAIGNS.NOT_DRAFT409Campaign is not a draft.
JOURNEY_API.PUBLIC.CAMPAIGNS.CONTENT.REQUIRED409Published template/content is not configured.
JOURNEY_API.PUBLIC.CAMPAIGNS.AUDIENCE.REQUIRED409At least one included audience is required.
JOURNEY_API.PUBLIC.CAMPAIGNS.VERSION_MISSING409Campaign has no current version.
JOURNEY_API.SCHEDULE.SEND_AT_REQUIRED400Future schedule execution has no send time.
JOURNEY_API.SCHEDULE.SEND_AT_INVALID400sendAt is malformed or in the past.
JOURNEY_API.SCHEDULE.BATCH_DATE_REQUIRED400Direct mail schedule has no batch date.
JOURNEY_API.SCHEDULE.BATCH_DATE_INVALID400batchDate is malformed or in the past.
JOURNEY_API.SCHEDULE.INVALID_FOR_CHANNEL400Operation is not permitted for this campaign's channel (e.g. immediate send on direct mail).
JOURNEY_API.SCHEDULE.UNSUPPORTED_CHANNEL400Channel is not supported for the requested schedule mode.
JOURNEY_API.SCHEDULE.UNSUPPORTED_TYPE500Internal: unsupported schedule type encountered. Contact support if this persists.
JOURNEY_API.BROADCASTS.SCHEDULE.PREFLIGHT_BLOCKERS400Preflight found blocking issues.
JOURNEY_API.BROADCASTS.FINALIZATION_QUEUE.PUBLISH_FAILED500The 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_PAYLOAD500Internal: a malformed finalization job was rejected by the worker. Contact support.
JOURNEY_API.AUTH.INSUFFICIENT_SCOPE403Token lacks the required scope.

Schedule vs Send

POST /schedule-jobsPOST /send-jobs
Scopecampaigns:schedulecampaigns:send
TimingFuture sendAt (digital) or batchDate (direct mail)Immediate
Channels supportedAll (email, sms, push, direct_mail, landing_page)Digital only (email, sms, push)
Uses saved schedule intentYes, when sendAt/batchDate is omittedNo
dryRun supportedYesYes
IdempotencyYesYes

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