# Run it by > Get structured data or a decision from a person outside the chat. Your agent sends a link; the person answers on their phone with no sign-up; validated JSON comes back to your webhook. - MCP server (Streamable HTTP): https://runitby.app/mcp. MCP clients that support OAuth sign in on their own: add the URL with no key (Claude Code: `claude mcp add --transport http runitby https://runitby.app/mcp`, then /mcp to authenticate; Codex: `codex mcp add runitby --url https://runitby.app/mcp`, then `codex mcp login runitby`). Otherwise send header "Authorization: Bearer ". - REST: https://runitby.app/v1, bearer API key. OpenAPI 3.1: https://runitby.app/openapi.json - API keys: https://runitby.app/app ## Tools - createRequest: mode "form" (schema) or "approval" (approval.reason), audience { kind: "open", maxSubmissions (default 1) } or { kind: "named", recipients: [{ name, email?, phone? }] } (1 to 50). Returns url or links[] (one per person), expiresAt. Each link in links[] has email: "sent", "failed" (sending didn't work; the request still exists, so call remind later or send the link yourself), "no address" or "off" (sendEmail not set). expiresIn: "30m", "48h", "7d" or seconds; default 7d, max 90d. Optional: sendEmail (true emails each named recipient that has an email their link; default false), notifyEmail ("off" default, "each" or "complete": emails the account owner), requester (how your agent introduces itself), locale ("en" or "es", or with a region like "es-AR" for dates), webhookUrl (https), metadata. - previewRequest: the form or approval fields of createRequest (no audience, expiresIn, webhookUrl or metadata), plus viewports and sampleData. Returns screenshots at 390 and 1280 px plus lint[]. Preview before creating when the person will see the form more than once. - getStatus: status (pending | partial | complete | expired | closed), verdict for approvals, each recipient's answer. wait (seconds, up to 25; REST ?wait=25) holds the call until someone answers or the status changes, then returns at once. - remind: emails the link to pending named recipients that have an email, at most once per 24 h each (the sendEmail email counts). recipientIds limits it to some of them. skipped[] says why: no email, reminded in the last 24 h, or sending failed. - listRequests, updateRequest, closeRequest. updateRequest changes title, schema, sections, reason or theme only until the first response; addRecipients, expiresIn (a new expiry counted from now) and notifyEmail work until the request is closed, expired or complete. Added recipients aren't emailed; send their links or call remind with their recipientIds. ## Getting the link to people Showing the link to your user isn't delivery. For named recipients with an email, pass sendEmail: true: each one gets their own link by email, sent in your account's name. Send every other link yourself over a channel you have (email, chat, SMS); if you have none, ask your user for the person's email, or hand them each link and say who it's for. Use remind to chase anyone who hasn't answered. Approvals need named recipients: each person gets their own link, so the decision is attributed to them. ## Hearing back Pick one or combine several: - webhookUrl: your server gets every answer as it happens (see Webhooks). - notifyEmail: the account owner gets an email with a summary and a dashboard link: "each" on every answer (the first 100 of a request, then only the end; best for small audiences), "complete" once when the request completes or expires. Approval emails include the decision and comment; form answers stay in the dashboard. Use it when there's no webhook and your session may be over when people answer. - getStatus with wait: stay in the conversation and return as soon as someone answers; call again to keep waiting. - MCP push (Pro plan): each request is the resource runitby://requests/{requestId} (createRequest links it; reading it returns the same as getStatus, on every plan). On Pro, clients on the 2026-07-28 revision can subscribe with subscriptions/listen { resourceSubscriptions: [uri] } and get notifications/resources/updated on every answer or status change, with up to 3 subscriptions open per account. On Free and Builder, subscriptions/listen answers with an error; use the other ways. - getStatus later, when your user asks. Act on an approval only once verdict is "approved"; "declined" and "expired" mean no. ## What to use it for - One person's details (a shipping address, client intake, a booking slot, support details): mode "form", named audience with one recipient. - Surveys, polls, feedback and sign-ups from anyone: mode "form", audience { kind: "open", maxSubmissions }. Post the one url anywhere (a newsletter, Slack, a QR code); each answer is its own response, and getStatus returns them all for you to summarise. - RSVPs, availability, shift cover or check-ins from a known group: mode "form", named audience (up to 50), so every answer is attributed and remind chases the rest. - Approvals, sign-offs and votes: mode "approval" with a policy. For money, access or deletion add verify: "email". ## Schema subset Plain JSON Schema, type "object" at the top. Fields: string (format email | date | time | date-time | tel | uri | duration, minLength, maxLength, pattern), number, integer (minimum, maximum), boolean (const: true makes a consent box that must be ticked), enum, array of primitives or of an enum (minItems, maxItems). required[] is honoured. Nested objects (except the location widget), oneOf, anyOf, allOf and $ref are rejected with a message. Fields that ask for passwords, PINs, card numbers, CVVs, security codes, SSNs or one-time codes are refused. At most 40 fields. A field's title is its label and description the help under it. Per-field hints go in "x-runitby": { widget: textarea | radio | select | slider | rating | chips | slots | daterange | location | color | country | language | timezone | yesno | rank, placeholder, help, columns: 1 | 2, step, currency }. slider needs a number or integer with minimum and maximum; rating needs a number or integer; chips needs an enum or an array of enum. Group fields with sections: [{ title, fields: [...] }]. Premade fields (the answer each returns): - date: format "date" → "2026-11-12". time: format "time" → "14:30", with x-runitby.step (minutes between choices, 1-60). date and time: format "date-time" → "2026-11-12T14:30:00-08:00". duration: format "duration" → "PT1H30M". - slots: array, items { type: "string", format: "date-time", enum: [times to pick from] }, widget "slots" → the picked times. - daterange: array, items { type: "string", format: "date" }, minItems 2, maxItems 2, widget "daterange" → ["2026-11-12", "2026-11-15"] (end on or after start). - location: { type: "object", x-runitby: { widget: "location" } } → { latitude, longitude, accuracy (metres) }. - string with widget color → "#C2410C"; country → "US" (ISO 3166-1); language → "es-AR" (BCP 47); timezone → "America/New_York". Add enum to restrict country or language. - money: number (minimum, maximum) with x-runitby.currency "USD" (ISO 4217) → 1200. - boolean with widget yesno → true | false; boolean with const: true is a consent box that must be ticked. - rank: array, items { enum: [options] }, widget "rank" → every option in the person's order. ## Theme theme: { accent, background, surface, text (hex), font and headingFont (system, Figtree, Geist, Inter, DM Sans, Manrope, Work Sans, Space Grotesk, IBM Plex Sans, Fraunces, Lora, Source Serif 4), radius (sharp | soft | round), density (compact | comfortable), layout (stacked | single), logoUrl, heroText, thankYou }. The default is ink on warm white, Figtree with Fraunces headings, round. Example 1, clean default with a greeting: ```json { "heroText": "Hi Jordan, I need a few details to ship your replacement part.", "thankYou": "Thanks! It ships within 2 business days." } ``` Example 2, warm and round, one question at a time: ```json { "accent": "#B4532A", "background": "#F5EFE6", "surface": "#FFFCF7", "text": "#2B2420", "radius": "round", "layout": "single" } ``` Example 3, dark and sharp: ```json { "accent": "#C8F169", "background": "#111315", "surface": "#1A1D21", "text": "#F2F3F4", "font": "Space Grotesk", "headingFont": "Space Grotesk", "radius": "sharp", "density": "compact" } ``` ## Three-person approval ```json { "mode": "approval", "title": "Refund #4821", "requester": "Ava (the Acme refunds agent)", "audience": { "kind": "named", "recipients": [{ "name": "John Smith", "email": "john@acme.com" }, { "name": "Martin Carpenter", "email": "martin@acme.com" }, { "name": "Emily Lee", "email": "emily@acme.com" }] }, "approval": { "reason": "Refund order #4821, $1,200", "detail": "Customer reported a damaged item.", "policy": "majority", "showOthers": "names", "verify": "email" }, "sendEmail": true, "webhookUrl": "https://example.com/hooks/intake", "metadata": { "taskId": "abc" } } ``` policy: any | all (default) | majority | { n }. showOthers: false (default) | "names" | "counts". allowComment defaults to true. verify: "email" makes each recipient confirm a 6-digit code sent to their own address before the buttons work, so a forwarded link can't decide for them; use it for money, access or deletion. options: two to four labels, the first one counts as approval; decisions come back as the label in snake_case ("Approve with changes" → "approve_with_changes"), default "approve" and "decline". verify: "email" needs an email for every recipient. The verdict is "approved", "declined", or "expired" when nobody decided in time; never a silent yes. ## Webhooks POST JSON with X-Runitby-Signature (hex HMAC-SHA256 of the raw body with your account's signing secret) and X-Runitby-Timestamp. Events: response.created (each submission or decision) and request.completed (every named person answered, maxSubmissions reached, an approval verdict, or expired). metadata comes back untouched. Any 2xx counts as delivered; the timeout is 10 s. Up to 4 retries, waiting 1 min, 5 min, 30 min and 2 h after each failed attempt; after that the response stays readable through getStatus. ## Plans and limits - Price: Free $0, Builder $9 a month, Pro $29 a month. - Requests created a month: 100 on Free, 2,000 on Builder, 10,000 on Pro (fair use). Past the limit createRequest returns 429 until the period resets or the account upgrades; links already sent keep working. - Open links (maxSubmissions): at most 25 on Free, 10,000 on Builder, unlimited on Pro. - API keys: 1 on Free, 5 on Builder, unlimited on Pro. Apps signed in over OAuth (Claude, Codex) count apart, with the same limits. - MCP push (subscriptions/listen): Pro only, up to 3 open subscriptions per account. - REST POST /v1/requests: 60 a minute per API key. title up to 120 characters, approval.reason 300, approval.detail 1,000, metadata under 4 KB. You are the data controller for what you collect. Answers are deleted 30 days (Free) or 90 days (Builder) after delivery, or after submission when there's no webhook; Pro keeps them.