# Receiving answers

> For a form's owner: send each answer to your email, a webhook or Slack, and check a webhook's Cube-Signature in Node.

Make a form at `https://ui.usecube.io/forms/new` (sign in with an email code). Its page has three tabs: Answers, Share and Settings. Under Settings you choose where each answer goes. All three can be on at once.

An answer a person sends goes out as it arrives. An answer an agent sends goes out only after you press Do on it; a Skip sends nothing.

## Email

Enter an address. Cube sends it a confirmation first, and nothing else goes there until you press Confirm in that email (the link works once, for 24 hours). Then each answer arrives as an email with the answers, who sent them and a link back to Answers.

## Webhook

Enter an `https` address. Cube shows the signing secret (`whsec_...`) once, when you connect; keep it. Each answer is a POST with a JSON body:

```json
{
  "form": { "id": "f_abc", "title": "Vendor intake" },
  "submission": {
    "id": "s_mgk2x1a4f3c9",
    "at": "2026-10-09T17:03:11.000Z",
    "channel": "mcp",
    "agent": { "name": "Claude", "model": "claude-sonnet-4-5" },
    "on_behalf_of": "Jane Doe",
    "answers": { "name": "Jane Doe", "resume": { "name": "resume.pdf", "type": "application/pdf", "size": 48213, "url": "https://.../files/f_abc/<sha256>?sig=..." } },
    "files": [{ "name": "resume.pdf", "type": "application/pdf", "size": 48213, "url": "https://.../files/f_abc/<sha256>?sig=..." }]
  },
  "status": "completed"
}
```

`channel` is `page`, `prefill`, `http`, `mcp` or `webmcp`. `agent` and `on_behalf_of` appear only when the sender gave them. A file's `url` is a signed link that works while the webhook stays connected. A test send from Settings sends the same shape with `"test": true`.

Headers: `Cube-Signature: t=<unix seconds>,v1=<hex>` and `Cube-Delivery: <submission id>`. `v1` is the HMAC-SHA256, with your secret, of `<t>.<raw body>`; this is Stripe's scheme. Check it against the raw body, before parsing it:

```js
import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1 ?? '')
  return a.length === b.length && timingSafeEqual(a, b)
}
```

Answer with any `2xx` within 10 seconds; the body is not read. Redirects are not followed. A failed send is tried 3 times (right away, after 1 second, after 3 more). If the last try fails, the row says Failing with the reason, and the next success turns it back On. The address must be public `https`; private and loopback addresses are refused. Use `Cube-Delivery` to ignore a repeat.

## Slack

Paste a Slack incoming webhook (`https://hooks.slack.com/services/...`). Each answer is a short message: the first four answers, who sent it and how, and an Open answers link. The address is never shown back.
