Sending forms

Forms are declarative blocks in a notification's detail.layout. The recipient fills the form in Roundtrip; submitted values arrive in your signed webhook event.

Send a form

curl
curl -X POST https://api.roundtrip.sh/api/v1/notifications \
  -H "Authorization: Bearer ak_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "approvals",
    "push": { "title": "Refund request, order #4821" },
    "content": {
      "title": "Refund request, order #4821",
      "description": "Tap to review and decide."
    },
    "detail": {
      "title": "Review refund",
      "layout": [
        {
          "type": "form",
          "id": "refund-review",
          "fields": [
            {
              "type": "select",
              "name": "decision",
              "label": "Decision",
              "required": true,
              "options": [
                { "label": "Approve full refund", "value": "full" },
                { "label": "Approve partial", "value": "partial" },
                { "label": "Deny", "value": "deny" }
              ]
            },
            { "type": "textarea", "name": "note", "label": "Note to customer" }
          ],
          "submit": { "label": "Submit decision", "action": "submit-refund", "style": "primary" }
        }
      ]
    },
    "response": {
      "mode": "required",
      "behavior": "resolve",
      "webhook": "https://api.example.com/roundtrip/webhooks"
    },
    "metadata": { "orderId": "4821" }
  }'
SDK
import { detail, form, selectField, textareaField } from "@roundtrip/sdk";

const refundWorkflow = roundtrip.webhooks.workflow({
  name: "refund-review",
  actions: {
    review: ["submit-refund"] as const,
  },
});

const refundForm = form({
  type: "form",
  id: "refund-review",
  fields: [
    selectField(
      "decision",
      [
        { label: "Approve full refund", value: "full" },
        { label: "Approve partial", value: "partial" },
        { label: "Deny", value: "deny" },
      ],
      { label: "Decision", required: true }
    ),
    textareaField("note", { label: "Note to customer" }),
  ],
  submit: {
    label: "Submit decision",
    action: refundWorkflow.action("review", "submit-refund"),
    style: "primary",
  },
});

await roundtrip.notify({
  channel: "approvals",
  push: { title: "Refund request, order #4821" },
  content: {
    title: "Refund request, order #4821",
    description: "Tap to review and decide.",
  },
  detail: detail([refundForm], { title: "Review refund" }),
  response: {
    mode: "required",
    behavior: "resolve",
    webhook: "https://api.example.com/roundtrip/webhooks",
  },
  metadata: { orderId: "4821" },
});

Receive values

Submitted forms arrive as response.submitted events. data.values is keyed by the field name.

event.data
{
  "notificationId": "ntf_2",
  "kind": "form",
  "action": "submit-refund",
  "values": {
    "decision": "partial",
    "note": "Refunding shipping only."
  },
  "metadata": { "orderId": "4821" }
}
handler
const router = roundtrip.webhooks
  .workflow({
    name: "refund-review",
    actions: {
      review: ["submit-refund"] as const,
    },
  })
  .onAction("review", "submit-refund", async ({ data }) => {
    const { decision, note } = data.values ?? {};
    await applyRefund(data.metadata.orderId, decision, note);
  });

Verify the raw body first

Use webhooks.unwrap with the raw request body and Roundtrip-Signature header before trusting event.data.

Form fields

text / textarea / emailstringoptional

Single-line, multi-line, and email-shaped inputs.

numbernumberoptional

Numeric input.

select / multiselectoptionsoptional

Choice inputs with explicit { label, value } options.

switchbooleanoptional

A boolean toggle.

Next steps