What do I need first?
- In the app, open Settings, then API Keys, and create a key with the write scope. A read key can list fixes and read proofs, but a POST with it gets 403. Copy the key straight away: it is shown only once.
- Find your brand_id. GET /v1/brands with the same key lists your brands and their ids.
- Call the API from a server, never a browser, so the key stays private. Every request sends it as Authorization: Bearer followed by the key.
How do I plan and start a fix?
Two calls, so nothing runs without a Yes, the same rule as Ask Proof in the app. The first call sends ask, what you want fixed in plain words (up to 2,000 characters). It returns the plan: what the fix will do, where it makes changes, how it can be undone, and a plan_id. Nothing has started.
curl -s https://app.proofsource.co/v1/missions \
-H "Authorization: Bearer $PROOFSOURCE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"brand_id":"brd_123","ask":"Get us named when people ask for the best CRM for small agencies"}'
# 200: nothing has started yet
# {"state":"planned","plan_id":"act_456","brand_id":"brd_123",
# "what":"...","where":"...","undo":"...","plan":{...},"next":"..."}Read the plan. To start it, send the plan_id back. The fix holds its credits first, then starts exactly that plan and answers 201 with the new fix's mission_id. A plan can be answered once: sending the same plan_id again gets 409.
curl -s https://app.proofsource.co/v1/missions \
-H "Authorization: Bearer $PROOFSOURCE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: start-act_456" \
-d '{"brand_id":"brd_123","plan_id":"act_456"}'
# 201: the fix is running
# {"state":"done","plan_id":"act_456",
# "result":{"mission_id":"msn_789","state":"running","steps_total":4,
# "live_fix_path":"/fixes/live/msn_789"}}Is it safe to retry a request?
Yes, with an Idempotency-Key header, which every write accepts. If a request times out, send it again with the same key: you get the first response back, marked Idempotent-Replay: true, and the fix never starts twice. A retry that arrives while the first is still running waits for it. Keys are remembered for 24 hours. A request that failed is not remembered, so the same key can run again once you fix the cause. Use a new key for each new request.
How do I follow a fix and read its proof?
GET /v1/missions lists a brand's fixes, newest first, with each one's state (planned, running, waiting_for_you, sleeping, needs_help, proven, flat, cancelled or rolled_back) and goal. Add status=proven to see only the fixes that worked. Each fix carries proof_url, its receipt: the outcome, the questions it was measured on, the readings before and after, and the verdict. Until a fix has a result, the receipt answers 409 with status not_ready.
curl -s "https://app.proofsource.co/v1/missions?brand_id=brd_123&status=proven" \
-H "Authorization: Bearer $PROOFSOURCE_API_KEY"
curl -s https://app.proofsource.co/v1/missions/msn_789/receipt \
-H "Authorization: Bearer $PROOFSOURCE_API_KEY"How do I hear when a fix worked?
- In the app, open Integrations, then Alerts & webhooks, and choose Add a webhook with your HTTPS URL.
- Tick Milestones. A proven fix is a milestone, sent as type mission.proven.
- Copy the signing secret shown when you save, or set your own. Send test checks your endpoint.
Each delivery is a POST with a JSON body and three headers: proofsource-event (the type), proofsource-delivery (one id per delivery) and proofsource-signature, which reads t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 of <t>.<raw body> with your signing secret. Compute it over the raw bytes before parsing, compare in constant time and refuse a t more than five minutes old.
{
"id": "evt_...",
"type": "mission.proven",
"api_version": "2026-10-03",
"created_at": "2026-11-21T09:00:00Z",
"org_id": "org_...",
"data": {
"brand_id": "brd_123",
"brand_name": "Acme",
"title": "A fix worked for Acme",
"summary": "Fix: ... The goal was reached and the result is saved with its proof.",
"dashboard_url": "https://app.proofsource.co/fixes/live/msn_789",
"kind": "mission_proven",
"mission_id": "msn_789",
"items": [{ "kind": "mission_proven", "title": "A fix worked for Acme", "detail": "..." }]
}
}// Node (Express): verify before trusting the body.
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/fixes", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("proofsource-signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const ok = fresh && parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === "mission.proven") {
// De-duplicate on event.id: delivery is at least once.
console.log("Fix worked:", event.data.mission_id, event.data.title);
}
res.sendStatus(200);
});Answer any 2xx within 10 seconds. A timeout, 408, 409, 425, 429 or 5xx is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. Delivery is at least once, so de-duplicate on id, then fetch the proof with data.mission_id.
What do errors look like?
Every error is application/problem+json with a stable type and a sentence in detail. Branch on type, never on the wording.
| Status | Type | When |
|---|---|---|
| 400 | validation_failed | brand_id is missing, both or neither of ask and plan_id were sent, the ask is a question rather than a fix, or status is not a fix state. |
| 401 | unauthenticated | No key, or the key is unknown or revoked. |
| 403 | forbidden | A read key on a POST. |
| 403 | approver_required | The key's owner cannot approve fixes. Owners and admins can; a member needs the approve permission. |
| 404 | not_found | The brand, fix or plan is not in your workspace. |
| 409 | conflict | The plan was already answered, or a retry with the same Idempotency-Key is still running. |
| 503 | upstream_unreachable | The fix could not be planned or started right now. Nothing started; try again. |
The full contract, with every field and the webhook, is in the OpenAPI spec at app.proofsource.co/v2/openapi.json, so you can generate a client in any language.