Create a scheduled communication job
POST /api/communications/schedule-job
Success: 201 Created
- Products: Operator (
FOH) - Features: Front of House Generic Access (
FOHGENERIC)
Overview
Creates a communication job: a stored definition that sends an email, a push notification and/or an SMS to an audience, either once at a fixed moment or repeatedly on a schedule.
A job has three parts, and you set all three in this one call:
- Content — an email template (
templateId), a push notification (pushNotification), an SMS (smsNotification), or any combination. At least one is required. - Audience — the saved query that is run when the job fires to produce the recipient list. Use
isTestandtestDetails.overrideEmailsto send to fixed addresses instead while you are integrating. - Schedule —
runAtfor a single send, orrecurrencefor a repeating one. Exactly one of the two; sending both, or neither, is a400.
Two flows exist side by side. In the newer flow you send jobTypeId and queryId together, and the job type decides which channels the job may use. In the older flow you send only templateId and the audience comes from the email template itself, with queryId ignored. jobTypeId and queryId must both be positive or both be zero — mixing them is a 400.
A 201 means the job record was written, not that it is scheduled. The job is saved in a pending state and scheduled asynchronously; it moves to ready only once scheduling completes. If that never happens the job stays pending and never fires, and the 201 you already received will not tell you so.
Decide between runAt and recurrence before you create the job. Recurring jobs can be changed later with Update a scheduled communication job; one-off jobs cannot be changed at all and must be deleted and recreated.
The 201 body is a single field, id. Keep it — it is the {id} for the update and delete calls.
Endpoint
POST {base_url}/api/communications/schedule-job
{base_url} is the environment URL issued to your integration. See Authenticating with Gladstone APIs.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
templateId | integer (int32) | No | The email template the job renders and sends. Must be the id of an email template that already exists on the tenant, or the request fails with 400 Invalid template specified. Send 0, or omit it, for a job that sends only a push notification or an SMS — but then jobTypeId and queryId must both be positive, otherwise you get 400 Invalid Schedule Job parameters. Template Id must be a positive number.. When jobTypeId is positive the job type must permit the email channel, or the request fails with 400 Email notification isn't supported for the job type.. When jobTypeId is 0 this template also supplies the audience query, and queryId is ignored. |
description | string | Yes | Free-text label stored against the job; this is how the job is identified in listings. Must be present and non-blank, or the request fails with 400 Description must be specified. It doubles as the push notification template name when pushNotification.templateName is blank. |
runAt | string (date-time) | No | Turns the job into a single send at this one moment, rather than a repeating schedule. Supply exactly one of runAt and recurrence: sending both fails with 400 Recurrence cannot be specified if RunAt is specified. and sending neither fails with 400 Recurrence must be specified if RunAt is not specified.. The value is read as a time in the tenant's own time zone and converted to UTC before the send is scheduled, so an offset in the string does not control when it fires. |
isTest | boolean | Yes | Runs the job in test mode: the send goes to the addresses in testDetails.overrideEmails instead of the recipients the audience query would return. When true, testDetails must be supplied (400 If isTest is true then testDetails should be provided); when false, testDetails must be absent (400 If isTest is false then testDetails should not be provided). Omitting the field is the same as false. |
jobTypeId | integer (int32) | No | Selects the job type, which decides which channels (email, push, SMS) this job is allowed to use. An id that does not exist fails with 400 Invalid JobTypeId provided., and a job type with no channels mapped against it fails with 400 No communication channel supported for the job type.. Must be positive whenever queryId is positive and 0 whenever queryId is 0; any other combination fails with 400 Invalid Schedule Job parameters. Job Type ID and Query Id must be positive numbers. Sending 0 selects the older flow, in which templateId alone identifies the communication and no channel checks are applied. |
queryId | integer (int32) | No | Identifies the saved query — the audience — that is run at send time to produce the recipient list. Checked only when jobTypeId is positive, where an id that does not exist fails with 400 Invalid query id specified.. Must be positive whenever jobTypeId is positive and 0 whenever jobTypeId is 0, or you get 400 Invalid Schedule Job parameters. Job Type ID and Query Id must be positive numbers. When jobTypeId is 0 this field is ignored entirely and the audience is taken from the email template named by templateId. |
recurrence | object | No | Defines a repeating schedule, as an alternative to a single runAt send. Supply exactly one of the two. These fields are converted into a schedule on the server — there is no way to pass a cron string directly, and no way to read one back. All hours are interpreted in the tenant's own time zone. |
recurrence.startDate | string (date-time) | Yes | The date the schedule becomes active. Required whenever recurrence is supplied. Must not be earlier than today in UTC, or the request fails with 400 Start Date cannot be in the past. |
recurrence.endDate | string (date-time) | No | The date the schedule stops. Omit it, or send null, for a schedule that never expires. Must not be earlier than startDate (400 End Date cannot be before Start Date) and must not be in the past (400 End Date cannot be in the past). |
recurrence.startAtHour | integer (int32) | Yes | The hour at which each day's sending window opens, in the tenant's time zone. Must not be greater than endAtHour. See startAtMinutes and runEveryMinutes for the extra rules that tie these two hours together. |
recurrence.endAtHour | integer (int32) | Yes | The hour at which each day's sending window closes, in the tenant's time zone. Must not be less than startAtHour, or the request fails with 400 EndAtHour cannot be before StartAtHour. Two silent adjustments apply when the schedule is built: if runEveryMinutes is set the value is moved back by one hour (0 becomes 23), and if it is 0 it is replaced by startAtHour. |
recurrence.startAtMinutes | string | No | Sends at this exact minute past the hour. Sent as a string but must parse as an integer from 0 to 59, or the request fails with 400 startAtMinutes only accepts values between 0 and 59.. Mutually exclusive with runEveryMinutes — sending both fails with 400 Select either startAtMinutes or runEveryMinutes. The two fields cannot be used together.. When it is set, startAtHour must equal endAtHour unless runEveryHours is also set, or you get 400 StartAtHour must be equal to endAtHour when startAtMinutes field is populated.. Omit it, and omit runEveryMinutes, to send on every minute of the window. |
recurrence.runEveryMinutes | string | No | Repeats the send every N minutes inside the daily window. Sent as a string but must parse as an integer from 0 to 59 that divides exactly into 60 — anything else fails with 400 runEveryMinutes only accepts the following integers 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30.. Mutually exclusive with startAtMinutes. Unless both hours are 0, startAtHour must differ from endAtHour, or you get 400 StartAtHour can not be equal to endAtHour when runEveryMinutes field is populated.. Setting it also moves endAtHour back by one hour when the schedule is built. |
recurrence.runEveryHours | string | No | Repeats the send every N hours inside the window. Sent as a string but must parse as an integer from 0 to 23, or the request fails with 400 Invalid hour '...' specified in IntervalHours — note the message names the field IntervalHours, not runEveryHours. Omit it to use every hour in the window. |
recurrence.onMonths | string | No | Restricts the schedule to particular months: a comma-separated list of month numbers from 1 to 12, where any entry may instead be an ascending range such as 6-9 — for example 1,3,6-9. A number outside the range fails with 400 Invalid month '...' specified in OnMonths, a range with more than two ends fails with 400 Invalid month range '...' specified in OnMonths - only a start and end month is permitted in each range, and a range that does not ascend fails with ... range must ascend. Omit it for every month. |
recurrence.onDaysOfMonth | string | No | Restricts the schedule to particular days of the month: a comma-separated list of day numbers from 1 to 31, where any entry may instead be an ascending range such as 1-7. A number outside the range fails with 400 Invalid day '...' specified in OnDaysOfMonth, a range with more than two ends fails with 400 Invalid day range '...' specified in OnDaysOfMonth - only a start and end day is permitted in each range, and a range that does not ascend fails with ... range must ascend. Omit it for every day of the month. |
recurrence.onNamedDays | string | No | Restricts the schedule to particular weekdays: a comma-separated list drawn from sun, mon, tue, wed, thu, fri and sat. Matching is case-insensitive and the value is stored lower-cased; anything else fails with 400 Invalid named day of week '...'. Omit it for every day of the week. |
testDetails | object | No | The recipient overrides used when the job runs in test mode. Must be supplied when isTest is true and must be absent when isTest is false; either mismatch is rejected with a 400. |
testDetails.overrideEmails | array of string | Yes | The addresses the test send goes to, in place of the audience the query would return. At least one entry is required (400 There must be at least one email override.) and every entry must be a valid email address (400 The email {address} is not a valid one.). |
testDetails.sendOne | boolean | Yes | Caps the test at a single send: when true, the audience is trimmed to its first recipient before any communication goes out, so the job sends once rather than once per matching recipient. When false, the test runs against the full audience. |
sendPushNotification | boolean | No | Switches the push notification channel on. When true you must also supply pushNotification, or the request fails with 400 No push notification content provided., and when jobTypeId is positive the job type must permit push, or you get 400 Push notification isn't supported for the job type.. Omitting it is the same as false. |
pushNotification | object | No | The push notification this job sends. It is saved as a push notification template belonging to the job — there is no way to point the job at an existing template instead. |
pushNotification.typeId | integer (int32) | No | Selects the push notification type, which governs whether the notification may carry action buttons and which redirect targets are legal for it. Must be greater than 0 (400 The type ID: {n} for the push notification is invalid.) and must exist (400 The template type with id: {n} does not exist.). For a type keyed NO_ACTION, sending either action1RedirectTo or action2RedirectTo above 0 fails with 400 The template type expects no actions but one or more action redirects were provided.. |
pushNotification.templateName | string | No | The name stored against the push notification template the job creates. When blank or omitted, the job's description is used instead, so there is no error for leaving it out. |
pushNotification.action1RedirectTo | integer (int32) | No | Where the notification's first action button sends the user. Must be an existing redirect type id (400 The action1 redirect type: {n} does not exist in the database.) and, unless the push notification type is marked as custom, must be a combination the type allows (400 The provided typeId: {n} has no match for the Action1RedirectTo: {m}.). Setting it above 0 makes content[].action1Label mandatory on every entry (400 All contents' Action1Labels must have a value if the Action1RedirectTo field is populated.); leaving it unset or 0 forbids action1Label on every entry (400 The Action1RedirectTo field does not exist, but there are Action1Labels populated.). Omitting it is treated as 0, meaning no first action button. |
pushNotification.action2RedirectTo | integer (int32) | No | Where the notification's second action button sends the user, with the same rules as action1RedirectTo: it must be an existing redirect type id (400 The action2 redirect type: {n} does not exist in the database.) and a combination the typeId allows (400 The provided typeId: {n} has no match for the Action2RedirectTo: {m}.). Above 0 it makes content[].action2Label mandatory on every entry; unset or 0 it forbids action2Label on every entry. Omitting it is treated as 0, meaning no second action button. |
pushNotification.isActive | boolean | No | Saved on the push notification template the job creates. Omitting it or sending null saves the template as inactive rather than leaving it unset — send true explicitly. On update it is overwritten with false whenever sendPushNotification is false. |
pushNotification.content | array of object | No | One entry per language for the notification's text. Do not omit this array while sendPushNotification is true — a missing array returns 500, not 400. An empty array passes validation and stores a template with no text. |
pushNotification.content[].language | string | No | The language this entry is written in. Every entry must carry one (400 All contents of the template must contain a language.) and it must be a language configured on the tenant (400 The language: {code} is not valid.). |
pushNotification.content[].title | string | No | The notification's title in this entry's language. Must not be blank on any entry, or the whole request fails with 400 All contents' titles must not be null or empty.. |
pushNotification.content[].content | string | No | The notification's body text in this entry's language. Must not be blank on any entry, or the whole request fails with 400 All push contents must not be null or empty.. |
pushNotification.content[].action1Label | string | No | The caption on the first action button in this entry's language. Required on every entry when pushNotification.action1RedirectTo is above 0, and forbidden on every entry when it is not — both mismatches are rejected with a 400. |
pushNotification.content[].action2Label | string | No | The caption on the second action button in this entry's language. Required on every entry when pushNotification.action2RedirectTo is above 0, and forbidden on every entry when it is not — both mismatches are rejected with a 400. |
smsNotification | object | No | The SMS this job sends, saved as an SMS template belonging to the job. Supplying this object is what switches the SMS channel on; there is no equivalent of sendPushNotification for SMS. When jobTypeId is positive the job type must permit SMS, or the request fails with 400 SMS notification isn't supported for the job type.. |
smsNotification.isActive | boolean | No | Ignored. Every SMS template the job creates or updates is saved as active regardless of the value you send, on both create and update. |
smsNotification.templateName | string | No | The name stored against the SMS template the job creates. Must not be blank whenever smsNotification is supplied, or the request fails with 400 SMS templates' name must not be null or empty.. Unlike pushNotification.templateName it does not fall back to the job's description. |
smsNotification.content | array of object | No | One entry per language for the message text. Defaults to an empty collection when the property is left out, which passes validation and stores a template with no text; prefer omitting the property to sending an explicit null. |
smsNotification.content[].language | string | No | The language this entry is written in. Every entry must carry one (400 All SMS contents of the template must contain a language.) and it must be a language configured on the tenant (400 The SMS language: {code} is not valid.). |
smsNotification.content[].content | string | No | The message text in this entry's language. Must not be blank on any entry, or the whole request fails with 400 All SMS contents must not be null or empty.. |
placeholderVariable | object | No | Binds a set of site ids to the job. At send time they are substituted into the placeholder tokens in the audience query, which is how one saved query can be reused to target different sites. Omit the whole object for a job that does not need this. |
placeholderVariable.id | integer (int32) | No | Ignored on create. A new placeholder variable is always created with an id of its own; the value you send here is not read. |
placeholderVariable.linkedDataIds | string | No | The site ids the placeholder resolves to, as a single comma-separated string such as SITE1,SITE2. Must not be blank (400 LinkedDataIDs must not be null or empty.) and every id must be an existing site — if any one of them is unknown the whole request fails with 400 LinkedDataIds do not match the specified variableTypeId.. Whitespace around each id is trimmed before the value is stored. |
placeholderVariable.variableTypeId | integer (int32) | No | The type of placeholder this variable stands in for. Must be an existing variable type, or the request fails with 400 Invalid variableTypeId specified.. |
placeholderVariable.variableTypeName | string | No | Not used. Only variableTypeId and linkedDataIds are stored; whatever you send here is discarded, on both create and update. |
Sample request
curl -X POST '{base_url}/api/communications/schedule-job' \
-H 'Authorization: Bearer {accessToken}' \
-H 'X-Use-SSO: true' \
-H 'Content-Type: application/json' \
-d '{
"templateId": 12345,
"description": "string",
"runAt": "2026-08-14T19:00:00Z",
"isTest": true,
"jobTypeId": 12345,
"queryId": 12345,
"recurrence": {
"startDate": "2026-08-14T19:00:00Z",
"endDate": "2026-08-14T19:00:00Z",
"startAtHour": 1,
"endAtHour": 1,
"startAtMinutes": "string",
"runEveryMinutes": "string",
"runEveryHours": "string",
"onMonths": "string",
"onDaysOfMonth": "string",
"onNamedDays": "string"
},
"testDetails": {
"overrideEmails": [
"member@example.com"
],
"sendOne": true
},
"sendPushNotification": true,
"pushNotification": {
"typeId": 12345,
"templateName": "string",
"action1RedirectTo": 1,
"action2RedirectTo": 1,
"isActive": true,
"content": [
{
"language": "string",
"title": "string",
"content": "string",
"action1Label": "string",
"action2Label": "string"
}
]
},
"smsNotification": {
"isActive": true,
"templateName": "string",
"content": [
{
"language": "string",
"content": "string"
}
]
},
"placeholderVariable": {
"id": 12345,
"linkedDataIds": "string",
"variableTypeId": 12345,
"variableTypeName": "string"
}
}'Response fields
| Field | Type | Description |
|---|---|---|
id | integer | The identifier of the communication job that was created. Use this value as the {id} path parameter when you update or delete the job. |
A Location header is also set, pointing at the job listing.
Sample response
{
"id": 4821
}Errors
| Status | Meaning | When |
|---|---|---|
400 | Bad request | Schedule not resolvable: both runAt and recurrence sent (Recurrence cannot be specified if RunAt is specified.), or neither (Recurrence must be specified if RunAt is not specified.). |
400 | Bad request | Recurrence out of range: Start Date cannot be in the past, End Date cannot be before Start Date, End Date cannot be in the past, EndAtHour cannot be before StartAtHour, Invalid month …, Invalid day …, Invalid hour …, Invalid named day of week …, startAtMinutes only accepts values between 0 and 59., or runEveryMinutes only accepts the following integers 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30.. |
400 | Bad request | Minute fields conflict: Select either startAtMinutes or runEveryMinutes. The two fields cannot be used together., StartAtHour must be equal to endAtHour when startAtMinutes field is populated., or StartAtHour can not be equal to endAtHour when runEveryMinutes field is populated.. |
400 | Bad request | Job identity invalid: Description must be specified, Invalid Schedule Job parameters. Template Id must be a positive number., Invalid Schedule Job parameters. Job Type ID and Query Id must be positive numbers., Invalid template specified, Invalid JobTypeId provided., or Invalid query id specified.. |
400 | Bad request | No usable channel: No content for any communication channel was provided. when templateId is not positive and neither a push notification nor an SMS is supplied; or No push notification content provided. when sendPushNotification is true but pushNotification is absent. |
400 | Bad request | Channel not allowed for the job type: Email notification isn't supported for the job type., Push notification isn't supported for the job type., SMS notification isn't supported for the job type., or No communication channel supported for the job type.. |
400 | Bad request | Push notification rejected: The type ID: {n} for the push notification is invalid., The template type with id: {n} does not exist., The template type expects no actions but one or more action redirects were provided., The provided typeId: {n} has no match for the Action1RedirectTo: {m}., The action1 redirect type: {n} does not exist in the database., All contents' titles must not be null or empty., All push contents must not be null or empty., All contents of the template must contain a language., The language: {code} is not valid., All contents' Action1Labels must have a value if the Action1RedirectTo field is populated., or The Action1RedirectTo field does not exist, but there are Action1Labels populated. — and the matching Action2 variants. |
400 | Bad request | SMS rejected: SMS templates' name must not be null or empty., All SMS contents must not be null or empty., All SMS contents of the template must contain a language., or The SMS language: {code} is not valid.. |
400 | Bad request | Test mode rejected: If isTest is true then testDetails should be provided, If isTest is false then testDetails should not be provided, There must be at least one email override., or The email {address} is not a valid one.. |
400 | Bad request | Placeholder variable rejected: Invalid variableTypeId specified., LinkedDataIDs must not be null or empty., or LinkedDataIds do not match the specified variableTypeId.. |
500 | Internal server error | An unexpected server-side failure; the body is empty. The one case you can trigger with a valid-looking request body is sending sendPushNotification: true with pushNotification.content absent. |
503 | Service unavailable |