RunDossier API

Generate executive pre-meeting dossiers programmatically. The RunDossier API lets your own tools and AI agents create dossiers, poll their status, and fetch the finished briefing without a browser session.

  • Base URL https://rundossier.com/api/v1
  • Authentication Authorization: Bearer dsk_live_...
  • Format JSON request and response bodies, snake_case field names

Every endpoint is server-to-server, stateless, and pollable — the primary consumer of this API is an autonomous agent.

Getting started

  1. Open Settings → API Keys and create a key. The full key (starting with dsk_live_) is shown once at creation — copy it immediately; it cannot be retrieved again.
  2. Send it as a Bearer token on every request. A quick check against your credit balance:
bash
curl https://rundossier.com/api/v1/credits \
  -H "Authorization: Bearer dsk_live_your_key_here"
  1. Create your first dossier and poll it to completion — see the full example below.

Authentication

Every request must carry your key in the Authorization header:

HTTP
Authorization: Bearer dsk_live_...

Keys are scoped to your user account and draw from your own credit balance. A missing, malformed, unknown, or revoked key returns a single generic 401 unauthorized — the API never reveals which of those conditions failed.

Creating, listing, and revoking keys is a browser-only action (see Managing API keys); an API key can never manage keys.

The dossier lifecycle

A dossier moves through these states:

StatusMeaning
pendingAccepted and queued for generation.
processingThe worker is researching and writing the dossier.
awaiting_selectionSeveral LinkedIn profiles matched the subject. The response includes a candidate_profiles array; resolve it before generation continues.
completeFinished. result_markdown, google_doc_url, word_doc_url, and photo_url are populated.
errorGeneration failed. error_message explains why.

The lifecycle is pending → processing → (awaiting_selection) → complete | error. The terminal states are complete and error.

Polling. After creating a dossier, poll GET /dossiers/{id} every 5–10 seconds until status is complete or error. Most dossiers finish in about 90 seconds.

Handling awaiting_selection. When a dossier reaches awaiting_selection, inspect candidate_profiles, choose the correct url, and POST it to the select endpoint. Passing selected_url: null instead cancels generation and refunds the credit.

Credits and billing

  • Each dossier costs 1 credit, deducted when you call POST /dossiers — identical to a web generation.
  • Credits come from your own balance (a monthly allowance plus any top-ups). Check it with GET /credits.
  • At a zero balance, POST /dossiers returns 402 insufficient_credits.
  • Cancelling an awaiting_selection dossier (selected_url: null) refunds the credit.

Manage your balance in Settings → Billing.

Rate limits

ScopeLimitApplies to
Per IP (pre-auth)30 requests / 60sEvery /api/v1/* request, before authentication
Per key — writes10 requests / minutePOST /dossiers, POST /dossiers/{id}/select
Per key — reads60 requests / minuteGET /dossiers, GET /dossiers/{id}, GET /credits

Exceeding a limit returns 429 rate_limited with a Retry-After header giving the number of seconds to wait. Honor it before retrying.

Errors

Every error uses the same envelope:

JSON
{
  "error": {
    "code": "insufficient_credits",
    "message": "You have no credits remaining. Purchase more to continue."
  }
}
StatuscodeMeaning
400invalid_requestA body or query parameter is missing or invalid.
400invalid_stateThe dossier is not in a state that allows this action (e.g. selecting on a dossier that is not awaiting_selection).
401unauthorizedThe API key is missing, malformed, unknown, or revoked.
402insufficient_creditsYour credit balance is zero.
404not_foundNo dossier with that id belongs to you.
429rate_limitedToo many requests; see Retry-After.
500internal_errorUnexpected server error; safe to retry.

Endpoints

Create a dossier

HTTP
POST /dossiers

Deducts one credit, creates the dossier, and dispatches it for generation. Returns immediately with status pending.

Request body

FieldTypeRequiredDescription
subject_namestringYesThe person to research. 1–200 characters.
contextstringNoExtra context to focus or disambiguate the research (role, company, etc.). Up to 1000 characters.
JSON
{
  "subject_name": "Jane Doe",
  "context": "VP of Engineering at Acme"
}

Response 201 Created

JSON
{
  "id": "b3f1c2a4-6d7e-4f89-a012-3456789abcde",
  "status": "pending"
}

Errors 400 invalid_request, 401 unauthorized, 402 insufficient_credits, 429 rate_limited.

Retrieve a dossier

HTTP
GET /dossiers/{id}

Returns the current state of one of your dossiers. Poll this to track progress. word_doc_url is a temporary signed download link valid for 5 minutes — request the dossier again to get a fresh one.

Response 200 OK

JSON
{
  "id": "b3f1c2a4-6d7e-4f89-a012-3456789abcde",
  "status": "complete",
  "subject_name": "Jane Doe",
  "status_message": null,
  "error_message": null,
  "result_markdown": "# Jane Doe\n\n## Quick Facts\n...",
  "google_doc_url": "https://docs.google.com/document/d/abc123/edit",
  "word_doc_url": "https://dossier-bot-photos.s3.amazonaws.com/...&X-Amz-Expires=300",
  "photo_url": "https://dossier-bot-photos.s3.amazonaws.com/photos/jane-doe.jpg",
  "created_at": "2026-07-02T14:11:05.000Z",
  "duration_ms": 88213
}

When status is awaiting_selection, the response also includes a candidate_profiles array:

JSON
{
  "id": "b3f1c2a4-6d7e-4f89-a012-3456789abcde",
  "status": "awaiting_selection",
  "subject_name": "Jane Doe",
  "status_message": "Multiple profiles found",
  "error_message": null,
  "result_markdown": null,
  "google_doc_url": null,
  "word_doc_url": null,
  "photo_url": null,
  "created_at": "2026-07-02T14:11:05.000Z",
  "duration_ms": null,
  "candidate_profiles": [
    {
      "url": "https://www.linkedin.com/in/janedoe",
      "title": "Jane Doe - VP Engineering - Acme",
      "headline": "VP Engineering at Acme",
      "location": "San Francisco, CA",
      "current_role": "VP Engineering",
      "current_company": "Acme"
    }
  ]
}

Within a candidate, fields other than url and title are omitted when unknown.

Errors 401 unauthorized, 404 not_found, 429 rate_limited.

Resolve a disambiguation

HTTP
POST /dossiers/{id}/select

Resolves a dossier in the awaiting_selection state. Provide the url of the chosen candidate to continue generation, or null to cancel and refund the credit.

Request body

FieldTypeRequiredDescription
selected_urlstring | nullYesA candidate url to proceed with, or null to cancel and refund.
JSON
{
  "selected_url": "https://www.linkedin.com/in/janedoe"
}

Response 200 OK — selecting a profile returns processing; cancelling (null) returns error.

JSON
{
  "id": "b3f1c2a4-6d7e-4f89-a012-3456789abcde",
  "status": "processing"
}

Errors 400 invalid_request (body invalid, or selected_url is not one of the candidate URLs), 400 invalid_state (dossier is not awaiting selection), 401 unauthorized, 404 not_found, 429 rate_limited.

List dossiers

HTTP
GET /dossiers

Lists your dossiers, newest first. Cursor-paginated.

Query parameters

ParameterTypeDefaultDescription
limitinteger20Maximum results to return. Capped at 50.
beforestringISO 8601 timestamp; returns dossiers created strictly before it. Pass the previous response's next_before to page.

Response 200 OK

JSON
{
  "dossiers": [
    {
      "id": "b3f1c2a4-6d7e-4f89-a012-3456789abcde",
      "status": "complete",
      "subject_name": "Jane Doe",
      "error_message": null,
      "created_at": "2026-07-02T14:11:05.000Z",
      "duration_ms": 88213
    }
  ],
  "next_before": "2026-07-02T14:11:05.000Z"
}

next_before is null when there are no more pages.

Errors 400 invalid_request (invalid before), 401 unauthorized, 429 rate_limited.

Get credit balance

HTTP
GET /credits

Returns your current credit balance.

Response 200 OK

JSON
{
  "monthly": 42,
  "topup": 10,
  "total": 52
}

monthly is your plan's recurring allowance, topup is purchased credits, and total is the sum — the pool a new dossier draws from.

Errors 401 unauthorized, 429 rate_limited.

Managing API keys

Keys are created, listed, and revoked from the browser only — an API key can never manage keys. These endpoints require a signed-in session (not a Bearer key) and are documented here for completeness:

MethodPathDescription
GET/api/api-keysList your keys (active and revoked).
POST/api/api-keysCreate a named key; returns the plaintext once. Max 10 active.
POST/api/api-keys/{id}/revokeRevoke a key immediately. Idempotent.

Create and revoke keys in Settings → API Keys.

Full example

A complete create → poll → fetch-result loop, including awaiting_selection handling.

curl

bash
export DOSSIER_API_KEY="dsk_live_your_key_here"

# 1. Create a dossier
curl -X POST https://rundossier.com/api/v1/dossiers \
  -H "Authorization: Bearer $DOSSIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject_name": "Jane Doe", "context": "VP of Engineering at Acme"}'
# → 201 { "id": "b3f1c2a4-...", "status": "pending" }

# 2. Poll until the status is complete or error
curl https://rundossier.com/api/v1/dossiers/b3f1c2a4-... \
  -H "Authorization: Bearer $DOSSIER_API_KEY"
# → { "status": "processing", ... }  then eventually
# → { "status": "complete", "result_markdown": "# Jane Doe...", ... }

JavaScript (fetch)

JavaScript
const BASE = "https://rundossier.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.DOSSIER_API_KEY}`,
  "Content-Type": "application/json",
};

// 1. Create
const created = await fetch(`${BASE}/dossiers`, {
  method: "POST",
  headers,
  body: JSON.stringify({ subject_name: "Jane Doe", context: "VP of Engineering at Acme" }),
});
if (!created.ok) throw new Error(`Create failed: ${created.status}`);
const { id } = await created.json();

// 2. Poll until a terminal state
async function poll(id) {
  while (true) {
    const res = await fetch(`${BASE}/dossiers/${id}`, { headers });
    const d = await res.json();
    if (d.status === "complete") return d;
    if (d.status === "error") throw new Error(d.error_message ?? "Generation failed");
    if (d.status === "awaiting_selection") {
      // Pick the best candidate (or surface d.candidate_profiles to a human)
      await fetch(`${BASE}/dossiers/${id}/select`, {
        method: "POST",
        headers,
        body: JSON.stringify({ selected_url: d.candidate_profiles[0].url }),
      });
    }
    await new Promise((r) => setTimeout(r, 5000));
  }
}

const dossier = await poll(id);
console.log(dossier.result_markdown);

Python (requests)

Python
import os, time, requests

BASE = "https://rundossier.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['DOSSIER_API_KEY']}"}

# 1. Create
resp = requests.post(
    f"{BASE}/dossiers",
    headers=HEADERS,
    json={"subject_name": "Jane Doe", "context": "VP of Engineering at Acme"},
)
resp.raise_for_status()
dossier_id = resp.json()["id"]

# 2. Poll until a terminal state
while True:
    d = requests.get(f"{BASE}/dossiers/{dossier_id}", headers=HEADERS).json()
    if d["status"] == "complete":
        break
    if d["status"] == "error":
        raise RuntimeError(d.get("error_message", "Generation failed"))
    if d["status"] == "awaiting_selection":
        requests.post(
            f"{BASE}/dossiers/{dossier_id}/select",
            headers=HEADERS,
            json={"selected_url": d["candidate_profiles"][0]["url"]},
        )
    time.sleep(5)

print(d["result_markdown"])

OpenAPI specification

A machine-readable OpenAPI 3.1 description of this API is served at:

HTTP
GET /api/v1/openapi.json

Use it to generate clients, validate requests, or feed the API to tooling. This reference is also available as raw markdown at /docs/api.md for ingestion by AI agents.