Docs · REST
REST API
Everything the app and the MCP server can do is a JSON endpoint under https://api.raisebook.org/v1. Use it for scripts, syncs and your own tools. The MCP tools call the same code, so the rules match.
Authentication#
Send a workspace API key as Authorization: Bearer rb_live_… or X-API-Key: rb_live_…. The key decides the workspace; there is nothing else to select. Owners create keys in Settings → API keys (scopes). Requests made with a key are recorded as agent actions: add X-Agent-Name and X-Agent-Model to label them on the timeline.
curl https://api.raisebook.org/v1/rounds/seed/summary \ -H "Authorization: Bearer $RAISEBOOK_API_KEY"curl -X POST https://api.raisebook.org/v1/investors \ -H "Authorization: Bearer $RAISEBOOK_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Agent-Name: weekly-sync" \ -d '{"name": "Dana Whitfield", "firm": "Harbor Light Ventures", "email": "dana@example.com", "type": "vc"}'The web app uses a session cookie instead, with an X-CSRF-Token header on every write and X-Workspace: <slug> to pick the workspace. You do not need either with a key.
Conventions#
- JSON in and out. CSV exports are
text/csv; charset=utf-8attachments. - Ids are UUIDs.
{round}in a path accepts the round id or its slug. - Timestamps are RFC 3339 in UTC. Dates are
YYYY-MM-DDin the workspace timezone. Amounts are integers in whole units of the round's currency. - PATCH: a missing key leaves the field alone;
nullclears it. - Lists take
?limit=(default 50, max 200) and?cursor=, and return{data, next_cursor}. - Request bodies are capped at 1 MB, or 6 MB for imports. Every response carries
X-Request-ID. - The OpenAPI document is at
/v1/openapi.json.
Errors#
Errors share one envelope. The message is a full sentence that says what to do next, so it can be shown to a person or handed to an agent as is.
{"error": {"code": "not_in_round", "message": "Dana Whitfield is not in round Seed. Add them with POST /v1/rounds/seed/participants.", "details": {"investor_id": "…", "round": "seed"}, "request_id": "req_01J…"}}| HTTP | Code | When |
|---|---|---|
| 400 | bad_request, workspace_required | Malformed request; a session with several workspaces and no X-Workspace header. |
| 401 | unauthorized, invalid_api_key, session_invalid | No credentials, or bad ones. |
| 403 | csrf_failed, insufficient_scope, forbidden_role | The key or role cannot do this. details.required says what is needed. |
| 404 | not_found, workspace_not_found | Missing, or in another workspace. Raisebook never says which. |
| 409 | duplicate | Unique conflict. details.existing holds the existing investor or round. |
| 409 | ambiguous_reference | A reference matched several records. details.candidates lists up to 10. |
| 409 | stage_in_use, last_owner, not_in_round, already_member, invite_mismatch, transfer_ownership_first, not_deletable | The current state does not allow the change. The message says what to do. |
| 413 | payload_too_large | Body over 1 MB (6 MB for imports), or an inline export over its limit. |
| 422 | validation_error | details lists each field: {loc, msg, type}. |
| 422 | round_required, stage_not_found, invalid_stage_config, limit_reached, confirmation_required, import_invalid | The request is well formed but cannot be applied. |
| 429 | rate_limited | Wait Retry-After seconds. details.retry_after_s says the same. |
| 500 | internal_error | Unexpected. The message carries the request id to report. |
An id or slug from another workspace returns 404 not_found, never 403, so Raisebook does not reveal that it exists.
Rate limits#
| Bucket | Limit |
|---|---|
| API key, reads (GET, MCP read tools) | 600 / min |
| API key, writes (POST, PATCH, PUT, DELETE, MCP write tools) | 120 / min |
| Signed-in user, all requests | 1,200 / min |
| Imports (preview and commit), per workspace | 30 / hour |
| Exports, per workspace | 60 / hour |
| Sign-in links | 5 / 15 min per email, 30 / hour per IP |
| Invites sent, per workspace | 50 / day |
| Unauthenticated requests, per IP | 120 / min |
| MCP protocol messages that are not tool calls, per key | 300 / min |
Limited responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. A 429 also carries Retry-After.
Endpoints#
Auth is the scope a key needs (read, write, admin), or a session rule for actions that only a signed-in person can take.
Auth and account#
Session endpoints used by the web app. They take no X-Workspace header. API keys cannot call them.
POST/v1/auth/magic-linknone
Request: {email, redirect_path?, invite_token?}
Response: 202 {sent: true}. Never reveals whether an account exists.
POST/v1/auth/verifynone
Request: {token}
Response: {user, csrf_token, redirect_path?, workspaces} and the session cookies. 400 invalid_token.
GET/v1/auth/sessionsession
Response: {user, csrf_token, workspaces}
POST/v1/auth/signoutsession
Response: 204. The session is revoked.
GET/v1/mesession
Response: {id, email, name?, created_at}
PATCH/v1/mesession
Request: {name?}
Response: User
DELETE/v1/mesession
Request: {confirm_email}
Response: 204. Deletes workspaces where you are the only member. 409 transfer_ownership_first if you are the only owner of a shared workspace.
Workspaces, members and keys#
GET/v1/workspacessession
Response: {data: Workspace[]} with your role
POST/v1/workspacessession
Request: {name, timezone?, default_currency?, sample?}
Response: 201 Workspace. sample: true creates a sandbox with fictional data.
GET/v1/workspaceread
Response: Workspace + {you: {type, role?, scopes, agent_name?, key_prefix?}, limits}
PATCH/v1/workspaceadmin
Request: {name?, timezone?, default_currency?}
Response: Workspace
DELETE/v1/workspaceowner session
Request: {confirm: "<slug>"}
Response: 204. Everything is deleted at once. 422 confirmation_required on a mismatch.
GET/v1/membersread
Response: {data: Member[]}
PATCH/v1/members/{user_id}owner session
Request: {role}
Response: Member. 409 last_owner.
DELETE/v1/members/{user_id}owner session, or self
Response: 204. 409 last_owner.
GET/v1/invitesadmin
Response: {data: Invite[]}
POST/v1/invitesowner session
Request: {email, role}
Response: 201 Invite. 409 already_member.
DELETE/v1/invites/{id}owner session
Response: 204 (revoked)
GET/v1/invites/lookup?token=none
Response: {workspace_name, inviter_name?, email_hint, role, expired}
POST/v1/invites/acceptsession
Request: {token}
Response: Workspace. 409 invite_mismatch.
GET/v1/api-keysadmin
Response: {data: ApiKey[]}
POST/v1/api-keysowner session
Request: {name, agent_name?, scopes, expires_at?}
Response: 201 {api_key, secret: "rb_live_…"}. The secret is shown once.
DELETE/v1/api-keys/{id}owner session
Response: 204 (revoked)
Rounds#
{round} in a path is a round id or slug.
GET/v1/rounds?status=open|closed|allread
Response: {data: Round[]}, open rounds first
POST/v1/roundswrite
Request: {name, target_amount?, currency?, instrument?, valuation_cap?, pre_money_valuation?, target_close_on?, notes?, stages?}
Response: 201 Round. 409 duplicate returns the existing round.
GET/v1/rounds/{round}read
Response: Round, with a count on each stage
PATCH/v1/rounds/{round}write
Request: Any round field, status
Response: Round. Closing sets closed_on to today unless given.
DELETE/v1/rounds/{round}admin
Request: {confirm: "<name or slug>"}
Response: 204
PUT/v1/rounds/{round}/stageswrite
Request: {stages: StageInput[], reassign?: {removed_key: target_key}}
Response: Round. 409 stage_in_use, 422 invalid_stage_config.
GET/v1/rounds/{round}/summaryread
Response: RoundSummary
Investors in a round#
A participant is an investor in a round, addressed by investor_id. stage accepts a key or a label; an unknown one returns 422 stage_not_found with the valid keys.
GET/v1/rounds/{round}/participantsread
Request: q, stage, fit_min, type, tag, industry, follow_up=due|week|none, owner, sort, order, limit (≤1000), cursor
Response: {data: Participant[], next_cursor, total}
POST/v1/rounds/{round}/participantswrite
Request: {investor_id, stage?, fit?, probability?, amount_soft?, next_step?, follow_up_on?, follow_up_note?, owner_user_id?}
Response: 201 Participant, or 200 if already in the round (fields applied)
POST/v1/rounds/{round}/participants/bulkwrite
Request: {investor_ids (≤500), stage?}
Response: {added, already}
GET/v1/rounds/{round}/participants/{investor_id}read
Response: Participant
PATCH/v1/rounds/{round}/participants/{investor_id}write
Request: {stage?, note?, fit?, probability?, amount_soft?, amount_committed?, committed_on?, follow_up_on?, follow_up_note?, next_step?, owner_user_id?}
Response: Participant. One timeline entry per kind of change.
POST/v1/rounds/{round}/participants/{investor_id}/stagewrite
Request: {stage, note?}
Response: {participant, changed, from, to}
POST/v1/rounds/{round}/participants/{investor_id}/follow-upwrite
Request: {on? | in_days? | clear?, note?}
Response: {participant, changed}
POST/v1/rounds/{round}/participants/{investor_id}/commitmentwrite
Request: {amount, committed_on?, note?, move_to_won?}
Response: {participant, previous_amount, moved_stage}
DELETE/v1/rounds/{round}/participants/{investor_id}write
Response: 204. Logged as removed_from_round.
GET/v1/follow-ups?round=&within_days=0&include_overdue=true&limit=read
Response: {today, data: FollowUpItem[], counts: {overdue, today, upcoming}}
Investors and activity#
GET/v1/investorsread
Request: q, type, tag, industry, round, not_in_round, sort, order, limit, cursor
Response: {data: Investor[], next_cursor, total}
POST/v1/investorswrite
Request: Investor fields + on_duplicate?: return_existing | fill_empty | create_anyway
Response: 201 {investor, created: true}, or 200 {investor, created: false, matched_on}
GET/v1/investors/resolve?ref=read
Response: {investor}. 404, or 409 ambiguous_reference (MCP reference rules).
GET/v1/investors/{id}read
Response: Investor, with rounds
PATCH/v1/investors/{id}write
Request: Investor fields + add_tags?, remove_tags?, append_notes?
Response: Investor. 409 duplicate on an email or LinkedIn collision.
DELETE/v1/investors/{id}write
Response: 204. Hard delete, with their round entries and timeline.
GET/v1/activitiesread
Request: investor_id, round, kind, actor_type, since, until, limit, cursor
Response: {data: Activity[], next_cursor}, newest first
POST/v1/activitieswrite
Request: {investor_id, kind: note|email|call|meeting, body, occurred_at?, round?, direction?, idempotency_key?}
Response: 201 Activity, or 200 with duplicate: true on a repeated key
DELETE/v1/activities/{id}write
Response: 204. Notes, emails, calls and meetings only; 409 not_deletable otherwise.
Imports and exports#
POST/v1/imports/previewwrite
Request: ImportRequest
Response: ImportPreview. Writes nothing.
POST/v1/importswrite
Request: ImportRequest
Response: ImportResult. One transaction; error rows are skipped and reported.
GET/v1/importsread
Response: {data: ImportRun[]} (last 50)
GET/v1/export?format=jsonread
Response: The whole workspace as JSON
GET/v1/investors/export?format=csvread
Response: All investors as CSV
GET/v1/rounds/{round}/export?format=csv|jsonread
Response: One round as CSV or JSON
Shapes#
The objects the endpoints return. A field with ? may be absent or null.
type Stage = { key: string; label: string; color: StageColor; kind: "open" | "active" | "won" | "lost"; position: number; count?: number };type StageColor = "slate" | "violet" | "cobalt" | "teal" | "amber" | "orange" | "green" | "gray" | "olive" | "sky";type Actor = { type: "user" | "agent" | "system"; label: string; user_id?: string; api_key_id?: string; agent_model?: string; via: "web" | "api" | "mcp" | "import" | "system" };type Round = { id; slug; name; status: "open" | "closed"; instrument: "safe" | "priced" | "convertible_note" | "other"; currency; target_amount: number; valuation_cap?; pre_money_valuation?; target_close_on?; closed_on?; notes?; stages: Stage[]; committed_amount: number; participant_count: number; created_at; updated_at };type InvestorCompact = { id; name; firm?; title?; type; email?; linkedin_url?; tags: string[] };type Investor = InvestorCompact & { website_url?; x_url?; location?; check_min?; check_max?; check_note?; stage_focus: string[]; industries: string[]; notes?; source?; warm_intro_by?; rounds: { round_id; round_slug; round_name; stage: Stage; fit; follow_up_on?; amount_committed? }[]; created_at; updated_at };type Participant = { round_id; round_slug; investor: InvestorCompact; stage: Stage; fit: number; priority: "hot" | "warm" | "maybe" | "low"; probability?; amount_soft?; amount_committed?; committed_on?; follow_up_on?; follow_up_note?; follow_up_state?: "overdue" | "today" | "soon" | "later"; next_step?; owner?: { user_id; name?; email }; stage_changed_at; last_activity_at?; created_at; updated_at };type Activity = { id; investor_id; investor_name; round_id?; round_slug?; kind; occurred_at; body?; data: object; actor: Actor; created_at; deletable: boolean };type FollowUpItem = { investor: InvestorCompact; round: { id; slug; name }; stage: Stage; follow_up_on; follow_up_state; days_overdue: number; follow_up_note?; next_step?; last_activity_at? };