> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Message Scheduling

> Schedule messages for future delivery with automatic compliance enforcement for marketing messages.

The Notify API allows you to schedule messages for delivery at a specific time in the future. This is particularly useful for marketing campaigns, time-sensitive announcements, and messages that need to be sent during business hours.

## Basic scheduling

Schedule a message by including the `schedule_at` parameter in your request:

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await client.notify.send({
    template_id: "template_01k8ap1btqf5r9fq2c8ax5fhc9",
    to: "+33612345678",
    schedule_at: "2024-12-25T10:00:00Z",
    variables: { 
      promotion: "Holiday Sale" 
    },
  });

  console.log(response.schedule_at); // When the message will actually be sent
  ```

  ```go Go theme={null}
  response, err := client.Notify.Send(context.TODO(), prelude.NotifySendParams{
    TemplateID: prelude.F("template_01k8ap1btqf5r9fq2c8ax5fhc9"),
    To: prelude.F("+33612345678"),
    ScheduleAt: prelude.F(time.Date(2024, 12, 25, 10, 0, 0, 0, time.UTC)),
    Variables: prelude.F(map[string]string{
      "promotion": "Holiday Sale",
    }),
  })

  fmt.Println(response.ScheduleAt) // When the message will actually be sent
  ```

  ```python Python theme={null}
  response = client.notify.send(
      template_id="template_01k8ap1btqf5r9fq2c8ax5fhc9",
      to="+33612345678",
      schedule_at="2024-12-25T10:00:00Z",
      variables={
          "promotion": "Holiday Sale"
      }
  )

  print(response.schedule_at)  # When the message will actually be sent
  ```
</CodeGroup>

The `schedule_at` parameter must be:

* In **RFC3339 format** with timezone
* In the **future** (past times will be rejected)
* Within the allowed scheduling window (see below)

<Info>
  The API response will include a `schedule_at` field showing when the message will actually be sent, in **RFC3339 format with timezone offset**. For example: `"2025-10-24T04:58:02-04:00"` (includes the `-04:00` timezone offset).
</Info>

## Scheduling windows

Marketing messages can be scheduled up to **90 days** in advance, giving you flexibility for planning long-term campaigns.

```json Example: Schedule a campaign 30 days in advance theme={null}
{
  "template_id": "template_01k8ap1btqf5r9fq2c8ax5fhc9",
  "to": "+33612345678",
  "schedule_at": "2025-01-15T14:00:00Z",
  "variables": {
    "campaign": "New Year Sale"
  }
}
```

## Automatic compliance enforcement

When scheduling marketing messages, Prelude automatically enforces compliance rules at delivery time. If your scheduled time falls outside allowed hours or on public holidays, messages will be automatically rescheduled to the next available time slot.

<Info>
  For detailed information about time window restrictions, public holiday handling, and automatic scheduling behavior, see the [Marketing Compliance](/notify/v2/documentation/marketing-compliance#time-window-restrictions) documentation.
</Info>

## Timezone handling

Scheduling is based on **UTC time** in the API, but compliance rules are enforced based on the **recipient's local timezone**.

<Steps>
  <Step title="You provide UTC time">
    Send your scheduled time in UTC or with a timezone offset
  </Step>

  <Step title="Prelude determines recipient timezone">
    The recipient's timezone is determined from their phone number
  </Step>

  <Step title="Compliance rules are checked">
    Time windows and holiday restrictions are checked in the recipient's local time
  </Step>

  <Step title="Automatic adjustment if needed">
    If the time violates rules, it's automatically moved to the next valid slot
  </Step>
</Steps>

### Example with timezone conversion

```javascript theme={null}
// Your server is in PST, recipient is in France (CET)
const response = await client.notify.send({
  template_id: "template_marketing_abc123",
  to: "+33612345678",
  // 7pm PST = 4am CET (next day)
  schedule_at: "2024-12-15T19:00:00-08:00",
  variables: { message: "Good morning!" }
});

// Automatically adjusted to 8am CET (allowed time)
console.log(response.schedule_at); // "2024-12-16T08:00:00+01:00"
```

## Expiration and scheduling

You can combine `schedule_at` with `expires_at` to set both when a message should be sent and when it should no longer be attempted:

```javascript theme={null}
const response = await client.notify.send({
  template_id: "template_01k8ap1btqf5r9fq2c8ax5fhc9",
  to: "+33612345678",
  schedule_at: "2024-12-25T09:00:00Z",  // Send at 9am
  expires_at: "2024-12-25T18:00:00Z",   // Give up after 6pm
  variables: { promo: "One Day Sale" }
});
```

<Warning>
  Make sure `expires_at` is **after** `schedule_at`. The expiration window starts from the scheduled delivery time, not from when you make the API call.
</Warning>

## Cancelling scheduled messages

Currently, scheduled messages cannot be cancelled through the API once submitted. Plan your campaigns carefully and test with small batches first.

<Note>
  If you need to cancel a large scheduled campaign, contact Prelude support immediately.
</Note>

## Monitoring scheduled messages

Scheduled messages appear in your webhook events when they are actually sent:

1. **Immediate**: `transactional.message.created` - Message accepted and scheduled
2. **At submission**: `transactional.message.submitted` - Message successfully submitted by Prelude to one of our providers
3. **At delivery time**: `transactional.message.pending_delivery` - Message being sent
4. **After delivery**: `transactional.message.delivered` or `transactional.message.failed`

The `correlation_id` field in webhooks helps you track messages across their lifecycle.

## Error codes

| Error Code            | Description                                         |
| --------------------- | --------------------------------------------------- |
| `invalid_date_format` | The `schedule_at` timestamp format is invalid       |
| `schedule_at_in_past` | The scheduled time is in the past                   |
| `schedule_at_too_far` | Scheduled time exceeds the maximum window (90 days) |

<Note>
  Time window violations for marketing messages don't generate errors - they result in automatic rescheduling.
</Note>
