Idempotency
Idempotency makes a write safe to retry. If your worker crashes, a queue retries, or a network call times out after Roundtrip already accepted the write, sending the same idempotency key prevents a duplicate operation.
Use it on every write that might be retried:
- creating a notification
- replacing the main content or a named slot
- appending history
- sending a follow-up push
- canceling a notification
SDK shortcut
With a notification handle, pass the semantic part as idempotency. The SDK adds
the notification id, operation, and slot for you.
await notification.content.replace(
"decision",
{
content: {
title: "Approve rollback?",
status: "warning",
},
},
{
idempotency: incident.decisionVersion,
}
);That becomes a key scoped like:
ntf_123:replace:decision:7Use a stable business value. Good choices are a domain revision, queue job id, event id, step sequence, or milestone name.
Initial sends
For the first notification, there is no notification id yet. Use your domain id and operation name.
const notification = await roundtrip.notify({
channel: "on-call",
push: { title: "Checkout latency is high" },
content: { title: "Checkout latency", status: "warning" },
metadata: { incidentId: incident.id },
idempotency: ["incident", incident.id, "open"],
});Common writes
await notification.content.append(
{
content: {
title: "Database checked",
status: "success",
},
},
{ idempotency: "database-checked" }
);
await notification.content.replace(
"progress",
{
content: {
title: "Import progress",
description: `${step.done}/${step.total} rows processed`,
status: "info",
},
},
{ idempotency: step.sequence }
);
await notification.push(
{
title: "Rollback decision needed",
priority: "high",
},
{ idempotency: "rollback-decision" }
);
await notification.cancel({ idempotency: "incident-closed" });Explicit keys
Use idempotencyKey when you already have the exact key, or when calling the
HTTP API directly.
curl -X POST https://api.roundtrip.sh/api/v1/notifications/ntf_123/content/append \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"content": { "title": "Database checked", "status": "success" },
"idempotencyKey": "incident:inc_42:append:database-checked"
}'The SDK still exposes the raw helper:
const key = notification.idempotency.replace(
"decision",
incident.decisionVersion
);What not to use
Do not use a random UUID, timestamp, or attempt number for a retryable write. Those values change on retry, so Roundtrip cannot recognize the duplicate.
For append history, use a key that names the specific timeline entry. Reusing the same append key for different history entries will dedupe the later write.
Mental model
| Write | SDK option | Scoped as |
|---|---|---|
| Create | ["incident", id, "open"] | incident:<id>:open |
| Replace main | "status-v7" | ntf_123:replace:main:status-v7 |
| Replace slot | decisionVersion | ntf_123:replace:decision:<version> |
| Append | "database-checked" | ntf_123:append:database-checked |
| Push | "rollback-decision" | ntf_123:push:rollback-decision |
| Cancel | "incident-closed" | ntf_123:cancel:incident-closed |
The key only needs to be stable for the write you want to dedupe.