SRED Desk — API

Draft SR&ED project documentation from your own tools — with the evidence still checkable.

API tokens Back to the app

Generate SR&ED write-ups from your own pipeline

Send the raw work evidence plus the work items you parsed from it, and get back one JSON object: the items grouped into projects, each judged against the CRA two-part test (sredable, classification, why), eligible projects written up in the submission format (description and goals at 100 words maximum, uncertainties each with experiments, results and evidence links), plus unassigned items with reasons, gaps and next_steps. The output is mechanically checkable: the app's own sredkit.js verifies every URL and number against the input and that every item ref lands in exactly one place — and your pipeline can do the same. Wire it into an HR or finance workflow to draft every engineer's claim from a gh pr list export. Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#; pick a language once and the whole page follows. Drafting aid, not tax advice.

Basics

Base URL https://api.skillsafe.ai/v1/app-api, app slug sred-desk. Send X-App-Slug: sred-desk and Authorization: Bearer <token> on every call. Responses use one envelope: {"ok":true,"data":...} on success, {"ok":false,"error":{"code","message","details"}} on failure.

POST /guest GET /me POST /estimate POST /run GET /runs/{id} POST /run-stream POST /data/summaries/query
codeHTTPmeaningdo
unauthorized401Missing/expired tokenMint a guest token or sign in; see tokens
payment_required402Balance below min_creditsTop up, or accept a truncated run
forbidden403Token belongs to another appMint a token for sred-desk
not_found404Bad path or job idCheck the endpoint and the job_id
validation_error400Input shape wrongRead error.details; match the schema below
rate_limited429Too many callsBack off; /similar is 30/min per IP
server_error5xxPlatform hiccupRetry with the same Idempotency-Key

Step 1 — a token

The quickest lane is a guest token: POST /guest with the slug, no body, no account. Guests can call /me and /estimate; paid runs need a personal token — get one on the token page (sign in there, then copy it; never paste tokens into shared code). Put the token in the Authorization header as Bearer YOUR_TOKEN.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
  -H "X-App-Slug: sred-desk" -H "Content-Type: application/json" \
  -d '{"slug":"sred-desk"}'
# {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}

Step 2 — who am I, and can I afford a run

GET /me is free and returns subject_type (guest or user) and credits. Compare the balance against the estimate's hold_credits before submitting — the app itself refuses to enable its run button on a short balance, and a polite client does the same.

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "X-App-Slug: sred-desk" -H "Authorization: Bearer YOUR_TOKEN"

Step 3 — the input, and what it costs

POST /estimate is free, charges nothing, starts no job. It validates the input and returns model, hold_credits (the reserve, an upper bound — not the price) and min_credits. The input object is the same for /estimate, /run and /run-stream. Parse your evidence into items the way the app does (one item per line carrying a URL, a date, a ticket key or a bullet; stable refs w1..wN) — sredkit.js in this bundle is the reference parser.

fieldtypenotes
subjectstringwhose work this is; may be empty
claim_yearstringe.g. 2026; window Feb 1 prev year to Jan 31
companystringoptional product/company context
focusstringoptional emphasis
include_incidentsboolfalse keeps INC- items out of eligible projects
work_textstringthe raw paste; clip long text from the middle, never the tail
work_clippedbooltrue when work_text is an excerpt
itemsarray[{ref, line, title, url, date, kind, incident}] — kind is pr|ticket|doc|note; refs are authoritative for coverage
prescanobject{item_count, pr_count, ticket_count, doc_count, note_count, url_count, undated_count, incident_count, out_of_window_count, window}
current_datetimestringcaller-local timestamp with offset and weekday
cat > input.json <<'EOF'
{"subject":"Noah Tran","claim_year":"2026","company":"Relay, a log analytics platform",
 "focus":"","include_incidents":false,
 "work_text":"- Adaptive trace sampling: bias correction for burst traffic https://github.com/relayhq/relay-db/pull/2481 (merged 2025-04-18)\n- REL-1203 Investigate why tail-based sampling under-counts rare errors (closed 2025-04-10)",
 "work_clipped":false,
 "items":[{"ref":"w1","line":1,"title":"Adaptive trace sampling: bias correction for burst traffic","url":"https://github.com/relayhq/relay-db/pull/2481","date":"2025-04-18","kind":"pr","incident":false},
          {"ref":"w2","line":2,"title":"REL-1203 Investigate why tail-based sampling under-counts rare errors","url":"","date":"2025-04-10","kind":"ticket","incident":false}],
 "prescan":{"item_count":2,"pr_count":1,"ticket_count":1,"doc_count":0,"note_count":0,"url_count":1,"undated_count":0,"incident_count":0,"out_of_window_count":0,"window":{"from":"2025-02-01","to":"2026-01-31"}},
 "current_datetime":"2026-08-10T09:00:00+08:00 (Monday)"}
EOF
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "X-App-Slug: sred-desk" -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"input\": $(cat input.json)}"

hold_credits prices the full output cap and is almost always far above the settled charge. If the balance sits between min_credits and hold_credits the run still executes with a reduced cap and the job carries "truncated": true — treat that as “cut short, top up for the whole summary”, not as a complete answer.

Step 4 — run and poll

POST /run starts a billed job and returns a job_id; poll GET /runs/{id} until status is terminal. Always send an Idempotency-Key header derived from the input — a network retry with the same key can never double-bill.

JOB=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
  -H "X-App-Slug: sred-desk" -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sd-$(shasum <<< "$(cat input.json)" | cut -c1-16)" \
  -d "{\"input\": $(cat input.json)}" | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['job_id'])")
curl -s "https://api.skillsafe.ai/v1/app-api/runs/$JOB" \
  -H "X-App-Slug: sred-desk" -H "Authorization: Bearer YOUR_TOKEN"

Step 5 — stream instead of polling

POST /run-stream is the same call with an SSE response: job events carry the id, delta events carry output text as it is written, and a final done event carries the terminal job including charged_credits. The app itself uses this lane to advance its progress stages as output keys appear in the stream.

curl -sN -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
  -H "X-App-Slug: sred-desk" -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" -H "Accept: text/event-stream" \
  -H "Idempotency-Key: sd-stream-1" \
  -d "{\"input\": $(cat input.json)}"

The output contract

The job's output.output is one JSON object: {title, subject, claim_year, summary, projects, unassigned, gaps, next_steps}. Each project carries id, name, sredable, classification (basic research | applied research | experimental development | not eligible), why, description and goals (non-empty and at most 100 words each when eligible, empty strings otherwise), uncertainties ([{title, description, experiments[], results[], links[]}], at least one per eligible project, empty for ineligible ones) and item_refs. unassigned is [{ref, reason}].

The contract's teeth are mechanical: every input item ref appears in exactly one project's item_refs or in unassigned; every URL and number in the output exists somewhere in the input; when include_incidents is false no incident-flagged ref sits inside an eligible project. The app re-runs these checks client-side after every run and prints failures; a pipeline consuming this API should do the same before letting a write-up anywhere near a claim — sredkit.js in this bundle is the reference implementation.

Step 6 — history: the summaries collection

Signed-in write-ups are stored in the app's summaries collection (per-user read/write). List them, filter with the query DSL, or search semantically — /similar is rate-limited to 30/min per IP and costs about 10x a filter, so prefer where when an exact match would do.

curl -s -X POST "https://api.skillsafe.ai/v1/app-api/data/summaries/query" \
  -H "X-App-Slug: sred-desk" -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"where":{"claim_year":{"eq":"2026"}},"sort":{"field":"ran_at","dir":"desc"},"limit":10}'

Semantic search: POST /data/summaries/similar with {"text": "the sampling uncertainty year", "limit": 8} returns records scored by meaning over the embedded title, subject and summary fields. Records come back as {record_id, doc: {...}} — the fields are under doc, never flat on the record.

Fair use

Runs are billed to the calling account at the model's rates plus the app's 10% markup; /estimate, /me and /guest are free. Do not poll jobs faster than every 2 seconds, reuse one token rather than minting a guest per call (guest subjects each own their records — a fresh guest sees an empty collection), and keep Idempotency-Key on every run so retries are safe. This page and the app obey the same contract; when in doubt, read sredkit.js and app.js in this bundle — they are the reference client. Output is a drafting aid, not tax advice.