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
- 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. - Send it as a Bearer token on every request. A quick check against your credit balance:
curl https://rundossier.com/api/v1/credits \
-H "Authorization: Bearer dsk_live_your_key_here"- 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:
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:
| Status | Meaning |
|---|---|
pending | Accepted and queued for generation. |
processing | The worker is researching and writing the dossier. |
awaiting_selection | Several LinkedIn profiles matched the subject. The response includes a candidate_profiles array; resolve it before generation continues. |
complete | Finished. result_markdown, google_doc_url, word_doc_url, and photo_url are populated. |
error | Generation 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 /dossiersreturns402 insufficient_credits. - Cancelling an
awaiting_selectiondossier (selected_url: null) refunds the credit.
Manage your balance in Settings → Billing.
Rate limits
| Scope | Limit | Applies to |
|---|---|---|
| Per IP (pre-auth) | 30 requests / 60s | Every /api/v1/* request, before authentication |
| Per key — writes | 10 requests / minute | POST /dossiers, POST /dossiers/{id}/select |
| Per key — reads | 60 requests / minute | GET /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:
{
"error": {
"code": "insufficient_credits",
"message": "You have no credits remaining. Purchase more to continue."
}
}| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | A body or query parameter is missing or invalid. |
| 400 | invalid_state | The dossier is not in a state that allows this action (e.g. selecting on a dossier that is not awaiting_selection). |
| 401 | unauthorized | The API key is missing, malformed, unknown, or revoked. |
| 402 | insufficient_credits | Your credit balance is zero. |
| 404 | not_found | No dossier with that id belongs to you. |
| 429 | rate_limited | Too many requests; see Retry-After. |
| 500 | internal_error | Unexpected server error; safe to retry. |
Endpoints
Create a dossier
POST /dossiersDeducts one credit, creates the dossier, and dispatches it for generation. Returns immediately with status pending.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
subject_name | string | Yes | The person to research. 1–200 characters. |
context | string | No | Extra context to focus or disambiguate the research (role, company, etc.). Up to 1000 characters. |
{
"subject_name": "Jane Doe",
"context": "VP of Engineering at Acme"
}Response 201 Created
{
"id": "b3f1c2a4-6d7e-4f89-a012-3456789abcde",
"status": "pending"
}Errors 400 invalid_request, 401 unauthorized, 402 insufficient_credits, 429 rate_limited.
Retrieve a dossier
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
{
"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:
{
"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
POST /dossiers/{id}/selectResolves 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
| Field | Type | Required | Description |
|---|---|---|---|
selected_url | string | null | Yes | A candidate url to proceed with, or null to cancel and refund. |
{
"selected_url": "https://www.linkedin.com/in/janedoe"
}Response 200 OK — selecting a profile returns processing; cancelling (null) returns error.
{
"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
GET /dossiersLists your dossiers, newest first. Cursor-paginated.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Maximum results to return. Capped at 50. |
before | string | — | ISO 8601 timestamp; returns dossiers created strictly before it. Pass the previous response's next_before to page. |
Response 200 OK
{
"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
GET /creditsReturns your current credit balance.
Response 200 OK
{
"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:
| Method | Path | Description |
|---|---|---|
| GET | /api/api-keys | List your keys (active and revoked). |
| POST | /api/api-keys | Create a named key; returns the plaintext once. Max 10 active. |
| POST | /api/api-keys/{id}/revoke | Revoke 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
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)
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)
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:
GET /api/v1/openapi.jsonUse 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.