Run it by
Ask people outside the chat for answers or approvals by link: forms, surveys and sign-offs they answer on their phone, returned to your agent as JSON.
Hosted MCP Server
npx add-mcp 'https://runitby.app/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
Ask anyone from your agent.
Your agent describes what it needs (a form, a survey, or a yes or no) and gets back a link for one person, each person in a group, or anyone you share it with. People open it on their phone, with no sign-up, and their answers come back to you as validated JSON.
- Create a request. Call
createRequestwith a JSON Schema for a form, or a reason for an approval. - Send the link. Pass
sendEmail: trueand we email it to each named person, send it over a channel you already have (email, chat or SMS), or post an open link anywhere. - Get the answer. On Builder and Pro, your MCP client is pushed every answer the moment it lands. It also arrives at your webhook, by email with
notifyEmail, or read it withgetStatus.
The same tools are available two ways: an MCP server for agents that speak MCP, and a REST API for everything else. Agents can read this guide in llms.txt.
Quickstart
From an API key to a person's answer in four steps.
1Get an API key
Sign in with your email and copy the key from the dashboard. It starts with sk_live_; keep it on your server. Using Claude Code, Codex or another MCP client? Skip this: add the server by URL and sign in from the client.
2Create a request
This one asks Jordan for a shipping address and a delivery date. The schema is plain JSON Schema; see Form schema for what's supported.
3Send the link
The response has one link per person in links[] (or a single url for an open link). With "sendEmail": true we email each named person who has an email their own link, in your account's name. Each link's email says what happened: "sent", "failed" (the request still exists; call remind later), "limit" (the plan's email limits are reached), "no address" or "off". Send the rest however you like: email, chat or SMS.
4Get the answer
When Jordan submits, your webhookUrl receives a response.created event with the data, already checked against your schema. No webhook? Call getStatus with the requestId, set notifyEmail and we email you when it's done, or on Builder and Pro subscribe over MCP and your agent is told as soon as Jordan answers. See Hearing back.
Authentication
Every MCP and REST call sends an API key as a bearer token. The same key works for both. A missing or revoked key gets a 401. There are two ways to get one.
Sign in from your MCP client
Add https://runitby.app/mcp with no key and the client signs you in over OAuth: a browser opens, you enter your email, and the sign-in link brings you back to a page that asks you to allow the app. First time? The same link creates your account. The app then holds its own key, named after it, which you can revoke under Keys.
- Claude Code: run the command on the right, then
/mcpin Claude Code and choose to authenticate. - Codex: run
codex mcp add, thencodex mcp login. - Claude (web and desktop): Settings, Connectors, Add custom connector, and paste the URL.
- Other clients that support MCP authorization: add the URL with no headers.
Create a key in the dashboard
For the REST API, scripts, and clients that only take a header: create and revoke keys in the dashboard and send the key in the Authorization header.
Plans count the two apart. Free includes one key and one connected app, Builder five of each, Pro as many as you need.
Framework examples
Building your own agent? Start from one file. Each example connects to the MCP server with an API key and has the agent ask Jordan for a shipping address, email her the link and wait for her answer. Set RUNITBY_API_KEY and ANTHROPIC_API_KEY, install the packages on its first lines, and run it.
Tools and endpoints
Each MCP tool has a REST endpoint with the same input and output. The API reference lists every field.
| MCP tool | REST | What it does |
|---|---|---|
createRequest | POST /requests | Create a form or approval and get the link(s), emailed for you if you ask. |
previewRequest | POST /requests/preview | Screenshots and lint of what the person will see. Creates nothing. |
getStatus | GET /requests/{id} | Status, verdict and every answer so far. |
listRequests | GET /requests | Your requests, newest first. Filter by mode or status; page with cursor. |
updateRequest | PATCH /requests/{id} | Change content, add recipients or set a new expiry. |
remind | POST /requests/{id}/remind | Email the link to people who haven't answered. |
closeRequest | DELETE /requests/{id} | Stop accepting answers. |
Forms and approvals
Every request has a mode:
"form"
Collect data. You send a schema; the person gets a form built from it, and you get back an object that matches it.
"approval"
Get a decision. You send approval.reason; each person gets a card with Approve and Decline (or your own labels) and an optional comment. You get each decision and an overall verdict.
Both take a title (up to 120 characters), an optional requester line saying who's asking, a locale for the page and its emails ("en", "es", "pt", "fr", "de", "it", "ja" or "zh", or with a region like "es-AR" for dates), and metadata that comes back untouched in every webhook and status. Write the title and questions in the same language.
Who answers
audience decides who gets a link.
| kind | You get | Use it for |
|---|---|---|
"named" | links[], one per person in recipients (1 to 50, each { name, email?, phone? }) | Answers you need to attribute to someone. Approvals must be named. An email enables sendEmail, reminders and email codes. |
"open" | One url anyone can open | Sign-ups and surveys. maxSubmissions caps the answers (default 1). |
What to use it for
A mode and an audience cover most things you'd otherwise do with a form builder, a poll, a scheduling app or an email thread.
| You want | Request |
|---|---|
| One person's details: a shipping address, client intake, a booking slot | "form", named, one recipient |
| A survey, poll, feedback or sign-ups from anyone | "form", open with maxSubmissions. Post the one url anywhere; getStatus returns every answer for your agent to summarise. |
| RSVPs, availability, shift cover or check-ins from a known group | "form", named (up to 50), so every answer is attributed and remind chases the rest |
| Approvals, sign-offs and votes | "approval" with a policy; add verify: "email" for money, access or deletion |
Status and expiry
| status | Means |
|---|---|
pending | Nobody has answered yet. |
partial | Some named people have answered, others haven't. |
complete | Everyone answered, maxSubmissions was reached, or an approval has its verdict. |
expired | The link ran out of time. |
closed | You called closeRequest. |
Expiry. expiresIn takes "30m", "48h", "7d" or seconds. The default is 7 days and the maximum 90.
Changing a request. updateRequest changes the title, schema, sections, reason or theme until the first answer arrives. Adding recipients, setting a new expiry (counted from now) and changing notifyEmail work until the request is complete, expired or closed. Added recipients aren't emailed; send their links or call remind with their recipientIds.
Reminders. remind emails the link to named people who haven't answered and have an email, at most once every 24 hours each. The sendEmail email counts as the first.
Hearing back. Use one or combine several:
webhookUrl
Your server gets every answer as it happens. See Webhooks.
notifyEmail
We email your account a summary and a link to the request in the dashboard: "each" on every answer (until the plan's email limits are reached, then only when it ends), or "complete" once when it completes or expires. Approval emails include the decision and comment; form answers stay in the dashboard. Off by default; updateRequest can change it.
getStatus with wait
Holds the call for up to 25 seconds (REST: ?wait=25) and returns as soon as someone answers or the status changes. Call it again to keep waiting.
MCP push (Builder and Pro)
Each request is the MCP resource runitby://requests/{requestId}, linked from createRequest; reading it returns the same as getStatus. On Builder and Pro, clients on the 2026-07-28 protocol can subscribe with subscriptions/listen and get notifications/resources/updated on every answer or status change, with one subscription open at a time on Builder and up to three on Pro. On Free subscriptions/listen answers with an error naming the other ways to hear back.
Form schema
Plain JSON Schema with type: "object" at the top and up to 40 fields. required[] is enforced, and title and description become the label and help text.
| Type | Keywords |
|---|---|
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 | A fixed list of choices |
array | Of primitives or of an enum; minItems, maxItems |
Hints. Put per-field options in "x-runitby": widget, placeholder, help, columns (1 or 2), step and currency.
Sections. Group fields with sections: [{ title, fields: [...] }] next to the schema.
Nested objects (except the location field), oneOf, anyOf, allOf and $ref are rejected with a message saying what to change. Fields asking for passwords, PINs, card numbers, CVVs, security codes, SSNs or one-time codes are refused.
Field types
Ready-made fields for things people find awkward to type. Each one is ordinary JSON Schema plus a widget, so the answer validates like any other field.
| Field | Schema | Answer |
|---|---|---|
| Date | string, format date | "2026-11-12" |
| Time | string, format time; step = minutes (1–60) | "14:30" |
| Date and time | string, format date-time | "2026-11-12T14:30:00-08:00" |
| Duration | string, format duration | "PT1H30M" |
| Time slots | array of date-time enum, widget slots | the picked times |
| Date range | array of 2 dates, widget daterange | ["2026-11-12", "2026-11-15"] |
| Location | object, widget location | { latitude, longitude, accuracy } |
| Money | number, currency "USD" | 1200 |
| Slider, rating | number or integer; slider needs minimum and maximum | 7 |
| Chips, radio, select | enum (chips also takes an array of enum) | "M" |
| Rank | array of enum, widget rank | every option, in the person's order |
| Yes or no | boolean, widget yesno | true |
| Colour | string, widget color | "#C2410C" |
| Country | string, widget country | "US" (ISO 3166-1) |
| Language | string, widget language | "es-AR" (BCP 47) |
| Time zone | string, widget timezone | "America/New_York" |
Add an enum to a country or language field to limit the choices.
Preview
previewRequest takes the same form or approval fields as createRequest (no audience, expiry, webhook or metadata) and returns screenshots at 390 px and 1280 px wide, plus lint[] with anything that will read badly. Nothing is created.
Pass sampleData to see the form filled in, and viewports to get only one size. Preview first when the same form will be sent many times.
Themes
theme makes the page look like your product. Without it, pages are ink on warm white, Figtree with Fraunces headings, round corners.
| Key | Values |
|---|---|
accent, background, surface, text | Hex colours |
font, 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 (all fields on one page) or single (one question at a time) |
logoUrl | Your logo |
heroText, thankYou | The greeting above the form and the message after sending |
Approval options
Approvals need a named audience: each person gets their own link, so every decision is attributed to them. The verdict is "approved", "declined", or "expired" when nobody decided in time; never a silent yes.
| approval field | Default | Notes |
|---|---|---|
reason required | What you're asking for, up to 300 characters. | |
detail | Context under the reason, up to 1,000 characters. | |
policy | "all" | "any", "all", "majority" or { n }: how many approvals it takes. |
options | Approve, Decline | Two to four labels; the first counts as approval. Decisions come back in snake_case: "Approve with changes" → "approve_with_changes". |
allowComment | true | Let people add a comment to their decision. |
showOthers | false | "names" or "counts" shows each person where the others stand. |
verify | false | "email": each person confirms a 6-digit code sent to their own address before the buttons work, so a forwarded link can't decide for them. Every recipient needs an email. Use it for money, access or deletion. |
Webhooks
Set webhookUrl (https) on a request and we POST JSON to it for two events:
response.created
Each submission or decision, with the answer in response.
request.completed
Every named person answered, maxSubmissions was reached, an approval has its verdict, or the request expired.
Verify the signature. X-Runitby-Timestamp is the send time in Unix seconds. X-Runitby-Signature is the hex HMAC-SHA256 of the timestamp, a dot and the raw body, with your account's signing secret from the dashboard. Reject timestamps more than 5 minutes old, so a captured delivery can't be replayed later.
Retries. Any 2xx within 10 seconds counts as delivered. Otherwise we retry 4 times, 1 min, 5 min, 30 min and 2 h after each failure. After that the answer is still there in getStatus.
Errors
Errors are JSON with a readable message and, for invalid input, problems[] pointing at each field to change. Agents can usually fix the request from the message alone.
| Status | When |
|---|---|
400 | The input or schema isn't valid. |
401 | Missing or revoked API key. |
404 | No request with that id on your account. |
409 | The change isn't allowed any more, e.g. editing after the first answer or reminding a closed request. |
429 | Over 60 creations a minute, or the plan's monthly requests are used up. |
Plans and limits
| Limit | Free | Builder | Pro |
|---|---|---|---|
| Requests a month | 250 | 2,000 | 10,000 |
| Answers per open link | 25 | 10,000 | Unlimited |
| API keys | 1 | 5 | Unlimited |
| Connected apps (OAuth) | 1 | 5 | Unlimited |
| Answers kept after delivery | 30 days | 90 days | Kept |
| Emails a month | 500 | 4,000 | 20,000 |
| Emails per request | 10 | 10 | 10 |
| MCP push (open subscriptions) | — | 1 | 3 |
| Price a month | $0 | $9 | $29 |
Past the monthly limit createRequest returns 429 until the period resets or you upgrade (on Pro, the fair-use limit, write to us for more); links already sent keep working. Without a webhook, answers are kept from submission instead. See pricing.
The emails we send for you (links, reminders and notifyEmail updates) count against both email limits. Past either one, links come back as "limit" for you to send yourself and "each" updates stop. The email saying a request ended always goes out, and approval codes don't count.
On every plan: 60 creations a minute per API key, title up to 120 characters, approval.reason 300, approval.detail 1,000 and metadata under 4 KB.
You are the data controller for what you collect. Ask only for what the task needs.