Respond to Proposal
Accept, counter, or reject a proposal. - **accept**: Requires `selected_slot_id`. Proposal transitions to `accepted` when ALL required participants accept. - **counter**: Requires `counter_slots`. Current proposal transitions to `countered`, a new proposal is created in the same thread. - **reject**: Any required participant rejecting transitions the whole proposal to `rejected`. ## For browser embeds: use the widget, not this endpoint If you are embedding the recipient-response flow in a third-party app, use [`@vennio/proposals-widget`](https://www.npmjs.com/package/@vennio/proposals-widget) (vanilla JS, 2 lines of HTML) or [`<VennioProposal>` from `@vennio/react`](https://www.npmjs.com/package/@vennio/react). The widget embeds `vennio.app/p/:token` in an iframe and surfaces accept / counter / reject as `postMessage` events / React callbacks. Your app never calls this endpoint directly — which means it cannot accidentally call it with the organizer's API key and hit `403 organizer_cannot_respond`. Call this endpoint directly only for server-side flows where there is no browser context (webhooks, schedulers, integration adapters). ## Authentication Either a Bearer JWT / API key in the `Authorization` header, **or** a magic-link `token` in the request body. JWT wins when both are present (the token is left intact for later out-of-band use). When the body-token path is used, the token is consumed atomically with the status flip; if the saga rolls back, the token remains valid for retry. **The proposal organizer cannot respond to their own proposal.** Responses come from recipients, not from the principal who created the proposal. To test end-to-end with a single API key, create the proposal with the key and then call this endpoint with the recipient's magic-link `token` in the body — your own Bearer auth on the response side will be rejected with 403. ## Error codes - `400 poll_mode_only_supports_accept` — `mode='poll'` proposals only accept `action: accept` (vote). `counter` and `reject` are rejected. - `401 missing_credentials` — no `Authorization` header and no `token` in the body. One of the two is required. - `401 invalid_token` — body-token is malformed, expired, or already consumed - `401 participant_unresolved` — the body-token authenticated but did not resolve to a participant row (open token still missing the self-identify fields the API expects, or named token with a deleted participant). - `401 token_proposal_mismatch` — body-token is valid but belongs to a different proposal than `:id` - `403 not_a_participant` — caller authenticated via JWT/API key but is not a participant of this proposal. - `403 organizer_cannot_respond` — caller is the proposal's organizer. Organizers cannot respond to their own proposals; use the magic-link token path or call this endpoint as a different principal. - `409 you_have_already_responded` — the resolved participant row has a non-`pending` response. Each participant gets one response; reconsideration is not supported. - `409 consent_revoked` — the recipient revoked `booking:propose` consent between create and accept; proposal is expired - `409 calendar_disconnected_recipient` — accepting recipient lost their calendar connection between create and accept; reconnect and retry - `409 token_already_consumed` — the magic-link token was consumed by a concurrent request - `400 participant_calendar_not_pinned` — at accept time, the responding participant has no calendar to write the event to. They must connect (or reconnect) a calendar and retry. Distinct from `409 calendar_disconnected_recipient` (create-time pre-flight); this fires when the participant connected post-create but lazy pin resolution still couldn't find a primary calendar. - `429 rate_limited` — per-link rate limit exceeded. Open-poll shareable tokens are multi-use; the API applies a per-token rate limit (default 20 votes per 60 seconds, env-tunable via `OPEN_VOTE_RATE_LIMIT_MAX` / `OPEN_VOTE_RATE_LIMIT_WINDOW_MS`) as a basic anti-flood floor. Builders are responsible for stricter voter identity / dedup on open polls. - `502 calendar_unavailable` — upstream calendar provider failed during event creation; saga rolled back, safe to retry
Auth required: Yes
id (path, string) (required)Idempotency-Key (header, string) — Unique key for idempotent POST requests (1-256 chars: alphanumeric, `_`, `-`, `:`, `.`).
Same key + same body within 24h replays the cached response.
action: string (required)selected_slot_id: string — Required for accept action — ID of the slot to selectcounter_slots: array — Required for counter actionmessage: stringtoken: string — Magic-link token from a proposal-invitation email. Provide this **instead of** an `Authorization` header to authenticate as the participant the token was minted for. Ignored when a Bearer/API-key credential is also present.
voter_name: string — **Open-poll only.** Self-supplied display name for the voter. Stored on the participant row created at vote time. Optional and never required — if both `voter_name` and `voter_email` are absent the row is recorded as `Anonymous`. Ignored for named-mode tokens (identity comes from the bound participant row).
voter_email: string — **Open-poll only.** Self-supplied email for the voter. When present, the API deduplicates on `(proposal, lower(email))` — a second vote from the same email **revises** the prior vote (UPDATEs the existing row's `selected_slot_id`). When absent, every vote creates a new participant row (the no-identity floor — see endpoint description). Ignored for named-mode tokens.
200: Response recorded400: Validation error401: Authentication required or invalid404: Resource not found409: Resource conflict422: Idempotency-Key was already used with a different request body429: Too many requests502: Upstream calendar provider unavailable. Safe to retry.Requires authentication. Pass a Bearer token (Supabase JWT) or an API key (`Authorization: Bearer vennio_sk_live_*`) in the request headers.
Base URL: https://api.vennio.app