Blog · Product

Transactional push notifications: orders, bookings and alerts

Which events deserve a push, how to tie them to each user, how to write them, and how to set TTL, urgency and idempotency so they arrive on time and only once.

Leer en español

A transactional notification tells users about something they set in motion themselves, such as an order, a booking, a payment or a change to their account, at the moment it is useful. It works when it targets one specific person, arrives on time and says what happened and what to do next. It fails when it arrives late, arrives twice or turns into advertising.

In short:

  • It is transactional if it responds to the user's own action or a change in their state; if nobody would miss it, it is not.
  • Link each subscription to your user ID and send to that user, not to a segment.
  • Put the fact in the title, the next step in the body, and a URL that opens the exact detail page.
  • Match TTL and urgency to the type of alert, and use one idempotency key per business event.
  • Schedule reminders in your backend and re-check state right before sending.

What counts as a transactional notification

The line between a useful alert and a promotional one has nothing to do with the channel or the tone. It depends on who triggered it:

AreaAlerts people appreciateNot transactional
OrdersConfirmed, shipped, out for delivery, delivered, problem with delivery"Complete your purchase and get 10% off"
Bookings and appointmentsConfirmation, reminder, time change, cancellation"Book again this weekend?"
Payments and accountPayment declined, upcoming renewal, new sign-in"Discover the Premium plan"
Alerts the user set upPrice drop, back in stock, threshold reachedAlerts nobody asked for

A quick test: would the user complain if this alert never arrived? If yes, it is transactional. The distinction has legal weight too, since promotional messages need specific consent; see GDPR and push notifications.

An order update goes to one person, so the subscription has to know which account it belongs to. With the castly SDK you do that when subscribing or at sign-in:

// When subscribing from a click
await Castly.subscribe({ userId: 'u_1842' });

// At sign-in, if this browser was already subscribed
await Castly.identify('u_1842', { lang: 'en' });

From your server you then target that user, and the notification reaches every device where they subscribed, say their work laptop and their phone:

await fetch('https://castly.sh/v1/notifications', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PUSH_SECRET_KEY}`, // sk_live_…
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-8841-shipped',
  },
  body: JSON.stringify({
    to: { type: 'users', userIds: ['u_1842'] },
    title: 'Your order has shipped',
    body: 'Arriving Thursday. Track it live.',
    url: 'https://shop.example/orders/8841',
    ttl: 86400,
  }),
});

Use your internal ID, never the email address, so no unnecessary personal data travels around. On shared computers, unsubscribe the browser at sign-out; otherwise the next person could see alerts meant for the previous account. Every field of that request is described in the API reference.

How to write a transactional alert

  • Title = the fact: "Your order has shipped," "Payment declined," "Your table is ready."
  • Body = the useful detail and the next step: "Arriving Thursday. Track it live."
  • URL = the exact page: order 8841, not the homepage or the order list.
  • Context without sensitive data: "Your appointment is tomorrow at 10:00" is enough; the reason for the appointment does not belong on a lock screen.
AvoidBetter
"Update on your order""Your order has shipped: arriving Thursday"
"You have a new notification""Payment declined: update your card so your order can ship"
"News about your booking!""Your Friday appointment moved to 5:30 pm"

The encrypted payload cannot exceed roughly 4 KB, but the screen is the real limit: as a rule of thumb, the title should make sense on its own and the body should fit in two lines. Some browsers, Safari among them, do not replace earlier notifications about the same order, as explained in web push in Safari on macOS, so send only the status changes that matter.

TTL and urgency by type of alert

A push that arrives late can be worse than none: "Your driver is 5 minutes away" read an hour later only confuses people. The TTL tells the push service how long to keep the message while the device is offline, and urgency tells it whether to deliver right away or wait for a more battery-friendly moment; both are defined in RFC 8030. These values are rules of thumb, not standards:

AlertTTLUrgency
Driver arriving, table ready10–15 minutes (600–900 s)high
New sign-in1 hour (3600 s)high
Appointment reminderUntil the appointment startshigh
Order shipped or delivered, payment declined24 hours (86400 s)normal
Price drop, back in stock6–24 hoursnormal

For a reminder, compute the TTL at send time as the seconds left until the appointment. That way, if the phone is off, the alert will not show up after the appointment has already happened:

const ttl = Math.max(60, Math.floor((appointment.start.getTime() - Date.now()) / 1000));

No duplicates, and reminders you can trust

One idempotency key per business event

Order systems retry: your payment provider's webhook arrives twice, or a worker restarts halfway through a job. If every attempt calls the send API, the customer gets two "Your order has shipped" alerts. The fix is to derive the Idempotency-Key header from the event, not the attempt: order-8841-shipped is always the same key, and repeating the request with it within 24 hours does not create another notification.

You schedule the reminders

castly sends at the moment you call the API; it does not schedule sends. To remind someone two hours before an appointment, store appointments with their start time and run a periodic job, such as a cron every 5 minutes or a delayed queue, that finds the ones starting soon and sends the alert. Re-read the state right before sending: if the appointment was cancelled or moved, do not send the stale reminder. Use a key such as appt-552-reminder-2h so two overlapping cron runs cannot duplicate it.

Measure delivery and keep a fallback

For critical alerts you need to know whether they arrived. castly logs every device as sent, delivered and clicked, which you can query with GET /v1/notifications/:id, and it can notify your server through webhooks such as notification.delivered and notification.clicked. That is enough to build a fallback:

  1. You send the "Payment declined" push.
  2. If no delivery is recorded for that user after 30 minutes (a rule of thumb), you send an email from your own system.
  3. If you receive subscriber.expired, you note that the device no longer gets push and prefer email for that user.

Webhooks are best effort, so confirm with the API before acting on a missing event.

Frequently asked questions

What is the difference between a transactional and a marketing notification?

A transactional one responds to something the user did or configured and concerns only them; a marketing one tries to trigger a new action and usually goes to a group. The legal basis and the tolerance differ too: a shipping update is welcome, a daily offer gets blocked.

Can I send transactional alerts to people who declined notifications?

Not by push: without browser permission there is no subscription and no way to deliver the message. For those users, rely on email or in-app messages on your site.

What happens if a user has several devices?

If you send by user ID, the alert reaches every device where they subscribed. For orders and bookings that is what you want: they see it wherever they are.

When should I ask for permission for transactional alerts?

When the value is obvious: right after an order or booking is confirmed, with copy like "Want us to tell you when it ships?" Asking the moment someone lands on your site, with no context, usually ends in a refusal.

How many alerts per order is too many?

As a rule of thumb, send the status changes the customer would otherwise check on their own: confirmed, shipped, out for delivery and delivered, plus any problem. Internal states such as "being prepared" are usually noise.

Put it into practice with castlyWeb and PWA push notifications for developers: an SDK under 3 KB, a REST API and delivery logs for every device. Free up to 10,000 pushes a month.
Create a free account →

Send your first push notification today.

Create a project, add the SDK and send with one API call. Free to start.

Create a free account →