Skip to content

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.

Two requests
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-8 attachments.
  • Ids are UUIDs. {round} in a path accepts the round id or its slug.
  • Timestamps are RFC 3339 in UTC. Dates are YYYY-MM-DD in the workspace timezone. Amounts are integers in whole units of the round's currency.
  • PATCH: a missing key leaves the field alone; null clears 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
{"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…"}}
Error codes
HTTPCodeWhen
400bad_request, workspace_requiredMalformed request; a session with several workspaces and no X-Workspace header.
401unauthorized, invalid_api_key, session_invalidNo credentials, or bad ones.
403csrf_failed, insufficient_scope, forbidden_roleThe key or role cannot do this. details.required says what is needed.
404not_found, workspace_not_foundMissing, or in another workspace. Raisebook never says which.
409duplicateUnique conflict. details.existing holds the existing investor or round.
409ambiguous_referenceA reference matched several records. details.candidates lists up to 10.
409stage_in_use, last_owner, not_in_round, already_member, invite_mismatch, transfer_ownership_first, not_deletableThe current state does not allow the change. The message says what to do.
413payload_too_largeBody over 1 MB (6 MB for imports), or an inline export over its limit.
422validation_errordetails lists each field: {loc, msg, type}.
422round_required, stage_not_found, invalid_stage_config, limit_reached, confirmation_required, import_invalidThe request is well formed but cannot be applied.
429rate_limitedWait Retry-After seconds. details.retry_after_s says the same.
500internal_errorUnexpected. 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#

Rate limits
BucketLimit
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 requests1,200 / min
Imports (preview and commit), per workspace30 / hour
Exports, per workspace60 / hour
Sign-in links5 / 15 min per email, 30 / hour per IP
Invites sent, per workspace50 / day
Unauthenticated requests, per IP120 / min
MCP protocol messages that are not tool calls, per key300 / 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.

types
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? };