Requesting approvals
Approvals are just notifications with actions and a response config. The publish call returns immediately; the decision arrives later as a signed webhook event.
One thing to swap
Replace ak_xxx with a workspace API key and use a channel that already
exists in your workspace.
Send the approval
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
-H "Authorization: Bearer ak_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "ops",
"push": {
"title": "Deploy api@1.4.2 to production?",
"priority": "high"
},
"content": {
"title": "Deploy api@1.4.2 to production?",
"description": "12 commits since the last release.",
"actions": [
{ "id": "deploy", "label": "Deploy", "style": "primary" },
{ "id": "hold", "label": "Hold", "style": "destructive" }
]
},
"response": {
"mode": "required",
"behavior": "resolve",
"webhook": "https://api.example.com/roundtrip/webhooks"
},
"metadata": { "deploymentId": "deploy_8f21" }
}'const deployWorkflow = roundtrip.webhooks.workflow({
name: "deploy",
actions: {
decision: ["deploy", "hold"] as const,
},
});
const notification = await roundtrip.notify({
channel: "ops",
push: {
title: "Deploy api@1.4.2 to production?",
priority: "high",
},
content: {
title: "Deploy api@1.4.2 to production?",
description: "12 commits since the last release.",
actions: deployWorkflow.actions("decision", {
deploy: { label: "Deploy", style: "primary" },
hold: { label: "Hold", style: "destructive" },
}),
},
response: {
mode: "required",
behavior: "resolve",
webhook: "https://api.example.com/roundtrip/webhooks",
},
metadata: { deploymentId: "deploy_8f21" },
});
console.log(notification.id);The send call does not block
notify returns when the notification is created and push delivery has been
queued. It does not wait for the person. Correlate the later webhook by
notificationId and your own metadata.
Receive the decision
Roundtrip sends a signed event envelope:
{
"id": "evt_rsp_1",
"type": "response.submitted",
"apiVersion": "2026-06-21",
"createdAt": "2026-06-21T18:04:11.000Z",
"data": {
"notificationId": "ntf_1",
"channel": "ops",
"userId": "user_42",
"kind": "action",
"action": "deploy",
"metadata": { "deploymentId": "deploy_8f21" },
"submittedAt": "2026-06-21T18:04:11.000Z"
}
}The signature is in Roundtrip-Signature, with the form
t=<unix_seconds>,v1=<hex>. Verify the raw body before parsing JSON. The
workflow router accepts generated action ids from the SDK and raw curl action
ids when the action map has one clear match.
import { webhooks } from "@roundtrip/sdk";
const router = webhooks
.workflow({
name: "deploy",
actions: {
decision: ["deploy", "hold"] as const,
},
})
.onResponse(async ({ data }) => {
await saveDecision(data.notificationId, data.action);
})
.onAction("decision", "deploy", async ({ data }) => {
await deploy(data.metadata.deploymentId);
})
.onAction("decision", "hold", async ({ data }) => {
await holdDeploy(data.metadata.deploymentId);
});
export async function POST(request: Request) {
await router.handle({
rawBody: await request.text(),
headers: request.headers,
secret: process.env.ROUNDTRIP_WEBHOOK_SECRET!,
});
return new Response(null, { status: 204 });
}Named decision slots
Use a named slot when the approval is part of a longer workflow. The slot can be replaced as the recommendation changes, while old revisions and submissions stay auditable.
const incidentWorkflow = roundtrip.webhooks.workflow({
name: "incident",
actions: {
decision: ["rollback", "wait"] as const,
},
});
await notification.content.replace(
"decision",
{
content: {
title: "Approve rollback?",
description: "Rollback candidate is ready.",
status: "warning",
actions: incidentWorkflow.actions("decision", {
rollback: { label: "Rollback", style: "destructive" },
wait: { label: "Wait", style: "secondary" },
}),
},
response: {
mode: "required",
behavior: "resolve",
webhook: "https://api.example.com/roundtrip/webhooks",
},
},
{ idempotency: "rollback-decision" }
);Webhook callbacks for slots include targetSlot and targetRevision, so a late
submission can be understood against the exact UI the person saw.