Update a scheduled communication job
PUT /api/communications/schedule-job/{id}
- Products: Operator (
FOH) - Features: Front of House Generic Access (
FOHGENERIC)
Overview
Replaces the configuration of an existing recurring communication job — its content, audience, schedule and test settings — keeping the job's id and its history. A communication job is a stored definition that sends an email, a push notification and/or an SMS to an audience on a repeating schedule; see Create a scheduled communication job for what the fields mean.
This is a replacement, not a patch. The job is rebuilt from the body you send, so send the complete definition every time, including the parts you are not changing. Three omissions are actively destructive:
- omitting
placeholderVariabledetaches the site variable from the job; - setting
isTesttofalsedeletes the override email addresses stored against the job; - omitting
recurrence.endDateclears the job's end date.
One-off jobs cannot be updated. A job created with runAt is rejected here with 400 Hangfire background jobs cannot be updated once it has been scheduled, and any attempt to convert a job between one-off and recurring is rejected with 400 A one-off scheduled job cannot be changed to a recurring scheduled job or 400 A recurring scheduled job cannot be changed to a one-off scheduled job. To change a one-off job, delete it and create a replacement.
An unknown job id comes back as 400, not 404. A missing job returns 400 Invalid job {id} specified, so the 404 documented below is never the response for a missing job.
Success is 204 No Content with an empty body. As on create, the response confirms only that the job was saved: the schedule change is applied asynchronously, shortly afterwards.
Endpoint
PUT {base_url}/api/communications/schedule-job/{id}
{base_url} is the environment URL issued to your integration. See Authenticating with Gladstone APIs.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | integer (int32) | Yes | The job to update, as returned in the id field of the create response. Must be 1 or greater, or the request fails with 400 Id must be a positive integer. Any value you put in the body is overwritten by this path value. An id that does not exist is rejected with 400 Invalid job {id} specified, not a 404. |
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 | Effectively unusable on this endpoint. A job created with runAt cannot be updated at all: once it has been scheduled the request fails with 400 Hangfire background jobs cannot be updated, and the same error can also be returned at a later stage of the update. Sending runAt for a job that was created with recurrence fails with 400 A recurring scheduled job cannot be changed to a one-off scheduled job, and omitting it for a job that was created with runAt fails with 400 A one-off scheduled job cannot be changed to a recurring scheduled job. To change a one-off job, delete it and create a replacement. |
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. Switching an existing test job to false deletes the override addresses stored against it. |
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. On update the recurrence is replaced wholesale, so send every field you want the job to keep. Omitting the object entirely fails with 400 Recurrence must be specified if RunAt is not specified.. |
recurrence.startDate | string (date-time) | Yes | The date the schedule becomes active. Required whenever recurrence is supplied. Unlike on create, a date in the past is accepted — but only if the job's stored start date is also in the past, in which case your value is discarded and the stored one is kept. If the job's stored start date is in the future and you send one in the past, the request fails with 400 Job's start date: {stored} is in the future and the start date in the request: {sent} is 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). Omitting it on update clears any end date the job already had. |
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.). On update the stored list is replaced by exactly what you send, so include addresses you want to keep. |
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. On update, sending false for a job that already has a push notification does not remove it — the stored template is flagged inactive instead, and pushNotification in your body is ignored. |
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. On update, the job's existing push notification template is overwritten with what you send: its content entries are all replaced, so send the full set of languages every time. |
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.. On update, a job that already has an SMS template has it overwritten in place, with all its content entries replaced. Omitting the object leaves the existing template untouched rather than removing it. For a job with no SMS template yet, a new one is created only if templateName is non-blank. |
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. On update this field is destructive by omission: leaving the object out detaches the variable from the job entirely. Send it back on every update you want the binding to survive. |
placeholderVariable.id | integer (int32) | No | Identifies which placeholder variable to update. Send 0 to update whatever variable the job is already linked to, or the job's own existing variable id to update that same variable. A positive id that does not belong to any job fails with 400 Invalid variableId specified., and a positive id that exists but is not this job's leaves the job unchanged and silently detaches its variable. |
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 PUT '{base_url}/api/communications/schedule-job/{id}' \
-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"
}
}'Errors
| Status | Meaning | When |
|---|---|---|
400 | Bad request | Id must be a positive integer — the {id} path segment is 0 or negative. |
400 | Bad request | Invalid job {id} specified — no job exists with that id. This is the response for an unknown job, in place of a 404. |
400 | Bad request | Hangfire background jobs cannot be updated — the job was created with runAt and has been scheduled. One-off jobs can never be updated. |
400 | Bad request | A recurring scheduled job cannot be changed to a one-off scheduled job when you send runAt for a job created with recurrence; A one-off scheduled job cannot be changed to a recurring scheduled job when you omit runAt for a job created with it. |
400 | Bad request | Job's start date: {stored} is in the future and the start date in the request: {sent} is in the past — the job has not started yet and you sent a start date that has. |
400 | Bad request | Every validation the create call applies also applies here: recurrence ranges, minute-field conflicts, job identity, channel permissions, push notification and SMS content, test details and placeholder variables. See the create endpoint's error table for the full list of messages. |
404 | Not found | Not reachable for a missing job, which returns 400 instead. It is returned only when something the job refers to no longer exists: The SMSTemplate with ID: {n} could not be found. or Variable {n} cannot be found. |
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 |