Send scheduled notifications

Schedule notifications to reach customers at specific future times—subscription renewal reminders, appointment confirmations, promotional campaigns, or payment due dates. Send to one recipient or broadcast to thousands with a single API call. Messages deliver at or shortly after the scheduled time without requiring you to maintain background workers or cron jobs.


How scheduling works

Scheduled Chimes become eligible for delivery at a future timestamp you specify. One schedule can reach one or many recipients. The API resolves saved customers and validates every inline phone number or email address before accepting the request. A malformed or unresolved recipient rejects the entire schedule; later transmission failures remain isolated to the affected recipient.

Recipients receive messages through SMS or email. Inline recipients use type plus a matching phone.number or email.address; saved-customer recipients use transport plus customer_id. All recipients in a single schedule must resolve to the same transport, so create separate schedules when a campaign needs both SMS and email delivery.

Email schedules use either a top-level email object or a stored email message_template object. SMS schedules use either full_message or a stored SMS message_template object. Do not include SMS and email content in the same schedule; the stored template channel must match the recipient transport. Email content is safety-scanned before the schedule is accepted.

{
  "recipients": [{ "customer_id": "cu_gloria_k", "transport": "email" }],
  "email": {
    "subject": "Your subscription renews tomorrow",
    "text": "Your subscription renews tomorrow. Update billing at https://yourstore.example/billing.",
    "from": {
      "address": "[email protected]"
    }
  },
  "send_after": "2025-12-17T09:00:00Z"
}

Schedule a notification

Call POST /chimes/schedule with your message content, recipient list, and delivery timestamp. The API returns a schedule ID immediately—actual delivery happens at the specified time.

Schedule notification

POST
/chimes/schedule
curl https://api.zebo.dev/chimes/schedule \
  -H "Authorization: Bearer $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "type": "phone", "phone": { "number": "+233544998605" } },
      { "type": "phone", "phone": { "number": "+233501234567" } },
      { "type": "phone", "phone": { "number": "+233208765432" } }
    ],
    "full_message": "Reminder: Your subscription renews tomorrow on Dec 18. Update payment method at https://pages.zebo.dev/invoices/billing if needed or contact support.",
    "send_after": "2025-12-17T09:00:00Z",
    "sender_id": "YourBrand",
    "purpose": "subscription_reminder",
    "request_meta": {
      "idempotency_key": "sched_renewal_batch_dec_2025"
    }
  }'

The response confirms your schedule with an ID, recipient count, send time, and creation timestamp. Store the schedule ID if you need to track delivery later or reconcile sent messages with your records.


Recipient validation

Commerce resolves and validates every recipient during request processing. Inline phone and email contacts must be well formed, while saved-customer references must exist and contain a valid contact for the selected transport. If any recipient fails these checks, correct the recipient and submit the schedule again.

Acceptance does not guarantee later delivery. After execution, transmission failures are recorded per recipient and do not change whether the other accepted recipients can be sent.


Delivery timing

send_after controls when a schedule becomes eligible to execute; it is not a delivery-time guarantee. Allow for processing and network delays, and do not rely on sub-minute precision.

If you need immediate delivery, send a Chime instead. For time-critical notifications like password resets or purchase confirmations, send immediately rather than scheduling.


Broadcast to many recipients

Schedule one message to thousands of recipients with a single API call. Each recipient receives an individual Chime—no shared message threads or group texts. Track each delivery independently through the Chime API.

Broadcast campaign

POST
/chimes/schedule
curl https://api.zebo.dev/chimes/schedule \
  -H "Authorization: Bearer $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "type": "phone", "phone": { "number": "+233544998605" } },
      { "type": "phone", "phone": { "number": "+233501234567" } },
      { "type": "phone", "phone": { "number": "+233208765432" } },
      { "type": "phone", "phone": { "number": "+233277654321" } },
      { "type": "phone", "phone": { "number": "+233248888888" } }
    ],
    "full_message": "Flash Sale! 30% off all items today only. Shop now: https://shop.example.com/sale Use code: FLASH30",
    "send_after": "2025-12-17T08:00:00Z",
    "sender_id": "ShopBrand",
    "purpose": "marketing_campaign"
  }'

The API currently enforces that recipients contains at least one item but does not publish a fixed maximum. Apply your own bounded batch size and monitor each schedule independently.


Managing scheduled notifications

Tracking execution and delivery

Check schedule status, view delivered message IDs, and identify failed recipients using POST /schedules/lookup. Returns the schedule details plus IDs of all created Chimes after execution. The errors array includes failure reasons for any recipients that didn't receive messages.

Track schedule status

POST
/schedules/lookup
curl https://api.zebo.dev/schedules/lookup \
  -H "Authorization: Bearer $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule_id": "sch_kPvqTrqGsopu07wfC7ttoWqmfwt48ZW7BGvYUWk"
  }'

Before execution, executed_at is absent and chime_ids may be omitted. After execution, chime_ids, when present, contains IDs of successfully created Chimes. Use Lookup a Chime with these IDs to check individual delivery status and transmission details. The optional errors array lists per-recipient execution failures.

Use this to debug broadcast campaigns: if 485 out of 500 messages delivered, the errors array tells you exactly which 15 recipients failed and why. Correct the invalid addresses and create a new schedule for just those recipients.

Canceling pending schedules

Cancel schedules before execution using POST /schedules/cancel. Prevents delivery if called before send_after time. Returns an error if the schedule already executed or was previously canceled.

Cancel schedule

POST
/schedules/cancel
curl https://api.zebo.dev/schedules/cancel \
  -H "Authorization: Bearer $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-schedule-kPvqTrqGsopu07wf" \
  -d '{
    "schedule_id": "sch_kPvqTrqGsopu07wfC7ttoWqmfwt48ZW7BGvYUWk"
  }'

Common cancellation scenarios: customer opts out of marketing before campaign sends, you discover an error in the message content, or a scheduled event gets postponed. Cancel the original schedule and create a new one with corrected details.

Cancellation only works on pending schedules. Repeating a new cancellation after the schedule is canceled returns an error. A network retry is safe only when it reuses the same Idempotency-Key, which replays the original cancellation result.


Using idempotency keys

Prevent duplicate schedules during network failures or retries by providing request_meta.idempotency_key or the Idempotency-Key header. If your server crashes after scheduling but before recording the schedule ID, retry with the same key—the API returns the original schedule instead of creating a duplicate.

const idempotencyKey = 'ship_notif_order_12345'
const payload = {
  recipients: customerPhones,
  full_message: 'Your order ships tomorrow',
  send_after: tomorrowAt9AM,
  request_meta: {
    idempotency_key: idempotencyKey,
  },
}

async function createSchedule() {
  const response = await fetch('https://api.zebo.dev/chimes/schedule', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.COMMERCE_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify(payload),
  })
  if (!response.ok) {
    throw new Error(`Commerce API request failed: ${response.status}`)
  }
  return response.json()
}

// First attempt succeeds, but the connection fails before the ID is saved.
await createSchedule()

// Retrying the identical request with the same key returns the first schedule.
const { scheduled_chime } = await createSchedule()

// Save schedule ID to your database
await db.orders.update(orderId, {
  notification_schedule_id: scheduled_chime.id,
})

Construct idempotency keys from stable identifiers like order IDs, campaign IDs, or subscription IDs plus the notification type—ship_notify_order_${orderId} or renewal_remind_sub_${subId}.


Next steps

Was this page helpful?