{"openapi":"3.1.0","info":{"title":"Slash Trace API","version":"1.0.0","summary":"Track company careers pages and read the roles on them.","description":"Slash Trace watches the careers pages an account chooses to track — including JavaScript-rendered and bot-blocked boards and the major applicant-tracking systems — and exposes the boards and the roles on them.\n\n**Two doors, one access model.** The REST endpoints under `/v1` take an API key (`Authorization: Bearer st_live_…`), created self-serve in Settings, API keys. The MCP endpoint at `/mcp` takes an OAuth 2.0 bearer token with the scopes declared in `components.securitySchemes.oauth2`. Both see exactly the boards the account tracks.\n\n**Rate limit.** 60 requests per sliding minute, per account, across every key. A 429 carries `error.retryAfter` in seconds. `getStatus` reports the remaining budget.\n\n**Plans.** Creating and using MCP is free with a verified account and needs no card. The REST API remains available on the Pro and Ultra plans; a Free account gets `plan_required` (403) for API-key requests.\n\n**Webhooks (Ultra).** Instead of polling listJobs, register a URL in Settings, Developers and Slash Trace POSTs `job.found`, `job.archived` and `board.failed` events to it, signed with HMAC-SHA256. The payloads are described under `webhooks` below; the guide is at https://slashtrace.com/docs/api/webhooks.\n\nHuman documentation: https://slashtrace.com/docs/api. Machine index: https://slashtrace.com/llms.txt.","termsOfService":"https://slashtrace.com/privacy","contact":{"name":"Slash Trace","url":"https://slashtrace.com","email":"hello@slashtrace.com"}},"externalDocs":{"description":"API documentation","url":"https://slashtrace.com/docs/api"},"servers":[{"url":"https://api.slashtrace.com","description":"Production"}],"security":[{"apiKey":[]}],"tags":[{"name":"Status","description":"Is the key working, and what is it allowed to do."},{"name":"Boards","description":"The careers pages this account tracks."},{"name":"Jobs","description":"The roles read from those pages."},{"name":"MCP","description":"The Model Context Protocol endpoint for agent clients."},{"name":"Webhooks","description":"Events Slash Trace sends to a URL the account registers."}],"paths":{"/v1/status":{"get":{"operationId":"getStatus","tags":["Status"],"summary":"Check the key, the plan, and the rate-limit budget","description":"Everything a client needs to know about itself in one call: is the service up, which plan the account is on, how much of the board limit is spent, which key is being used, and how much of this minute’s budget remains. Call this first when other requests fail.","responses":{"200":{"description":"The account and key this request authenticated as.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Status"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/boards":{"get":{"operationId":"listBoards","tags":["Boards"],"summary":"List tracked boards","description":"Every careers page on this account’s list, with job counts and when it was last read. Use it to find a `boardId` for listJobs.","parameters":[{"name":"status","in":"query","required":false,"description":"Only boards in one lifecycle state: `pending` (discovery running), `scraping`, `ready`, or `failed`. Default `all`.","schema":{"default":"all","type":"string","enum":["pending","scraping","ready","failed","all"]}}],"responses":{"200":{"description":"The boards, in the order they were added.","content":{"application/json":{"schema":{"type":"object","required":["boards","total"],"properties":{"boards":{"type":"array","items":{"$ref":"#/components/schemas/Board"}},"total":{"type":"integer","minimum":0}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"addBoard","tags":["Boards"],"summary":"Track a company","description":"Start tracking a company’s careers page from a domain or a URL; the server finds the board. **202** means discovery started and the board is `pending` — jobs arrive within about a minute. **201** means the board was already known and fresh. **409** means it is already on this account’s list; nothing is added. `locked: true` on a success means the board is over the plan’s limit and will not be read until there is room.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":500,"description":"A careers URL, a board URL, or just the company domain, e.g. `acme.com`.","examples":["acme.com","https://boards.greenhouse.io/acme"]},"companyName":{"type":"string","maxLength":120,"description":"Display name to use until a scrape discovers the real one."}},"required":["url"],"description":"A company domain, a careers page URL, or an ATS board URL. The server resolves it."}}}},"responses":{"201":{"description":"Added. The board was already known and is ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddBoardResult"}}}},"202":{"description":"Added. Discovery is running; poll listBoards for `status: ready`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddBoardResult"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Already tracked. `error.details.boardId` names the existing board.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/boards/{id}":{"delete":{"operationId":"deleteBoard","tags":["Boards"],"summary":"Stop tracking a board","description":"Removes the board from this account’s list. The board and its history survive; adding it again restores everything.","parameters":[{"name":"id","in":"path","required":true,"description":"A board `id` from listBoards.","schema":{"type":"string","pattern":"^[a-f0-9]{24}$"}}],"responses":{"204":{"description":"Removed from the list."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/jobs":{"get":{"operationId":"listJobs","tags":["Jobs"],"summary":"List roles across tracked boards","description":"Roles on every board this account tracks, filtered by board, first-seen date, status, title text or location. To poll for new roles, pass the previous response’s `polledAt` back as `addedAfter`.","parameters":[{"name":"boardId","in":"query","required":false,"description":"Only roles on this board. An `id` from listBoards.","schema":{"type":"string","minLength":24,"maxLength":24,"pattern":"^[a-f0-9]{24}$"}},{"name":"addedAfter","in":"query","required":false,"description":"Only roles first seen after this ISO 8601 instant. Pass back the `polledAt` of the previous response to poll for new roles without gaps; a role may then appear twice, never zero times.","schema":{"type":"string","format":"date-time","examples":["2026-08-24T09:00:00Z"]}},{"name":"status","in":"query","required":false,"description":"Roles that are `open` (default), `closed`, or `all`.","schema":{"default":"open","type":"string","enum":["open","closed","all"]}},{"name":"search","in":"query","required":false,"description":"Case-insensitive text match on the job title.","schema":{"type":"string","maxLength":200}},{"name":"location","in":"query","required":false,"description":"Case-insensitive text match on the location.","schema":{"type":"string","maxLength":200}},{"name":"limit","in":"query","required":false,"description":"Page size, 1 to 100. Default 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":100}},{"name":"skip","in":"query","required":false,"description":"Rows to skip, for paging. Default 0.","schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"A page of roles, newest first.","content":{"application/json":{"schema":{"type":"object","required":["jobs","total","polledAt"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"total":{"type":"integer","minimum":0,"description":"Matches across all pages."},"polledAt":{"type":"string","format":"date-time","description":"Stamped before the query ran. Pass it back as `addedAfter` on the next poll."}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/mcp":{"post":{"operationId":"mcp","tags":["MCP"],"summary":"Model Context Protocol endpoint (Streamable HTTP)","description":"JSON-RPC 2.0 over the MCP Streamable HTTP transport. Tools: `list_boards`, `search_jobs`, `list_updates`, `add_board`, `remove_board`. An unauthenticated request answers 401 with `WWW-Authenticate: Bearer resource_metadata=\"…\"` pointing at the RFC 9728 document; follow it to discover the authorization server. Server card: `/.well-known/mcp/server-card.json`.","security":[{"oauth2":["boards:read","boards:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 request, e.g. `{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}`.","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","integer","null"]},"method":{"type":"string"},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"A JSON-RPC response, as JSON or as a server-sent event stream.","content":{"application/json":{"schema":{"type":"object"}},"text/event-stream":{"schema":{"type":"string"}}}},"401":{"description":"No or invalid token. `WWW-Authenticate` names the protected-resource metadata URL.","headers":{"WWW-Authenticate":{"schema":{"type":"string"}}}},"403":{"description":"Authenticated, but the plan does not include the connector."}}}}},"webhooks":{"job.found":{"post":{"operationId":"webhookJobFound","summary":"Roles appeared on a board","description":"Delivered after a check finds roles on a tracked board that were not there before. One delivery per board per check, carrying every new role from that check in the listJobs shape.","tags":["Webhooks"],"security":[],"parameters":[{"name":"X-SlashTrace-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256>` over `${t}.${raw body}` with the webhook’s secret. Verify against the raw bytes before parsing, and reject a `t` more than five minutes old."},{"name":"X-SlashTrace-Event","in":"header","required":true,"schema":{"type":"string"},"description":"The event type, same as `type` in the body."},{"name":"X-SlashTrace-Delivery","in":"header","required":true,"schema":{"type":"string"},"description":"The delivery id, same as `id` in the body."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookJobEvent"}}}},"responses":{"2XX":{"description":"Received. Anything else is retried: after 1, 5 and 30 minutes, then 2 and 12 hours, then given up."}}}},"job.archived":{"post":{"operationId":"webhookJobArchived","summary":"Roles went from a board","description":"Delivered once roles have been missing from a board across enough healthy checks to count as closed — never on a single absence, so a board that failed to load does not close everything on it.","tags":["Webhooks"],"security":[],"parameters":[{"name":"X-SlashTrace-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256>` over `${t}.${raw body}` with the webhook’s secret. Verify against the raw bytes before parsing, and reject a `t` more than five minutes old."},{"name":"X-SlashTrace-Event","in":"header","required":true,"schema":{"type":"string"},"description":"The event type, same as `type` in the body."},{"name":"X-SlashTrace-Delivery","in":"header","required":true,"schema":{"type":"string"},"description":"The delivery id, same as `id` in the body."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookJobEvent"}}}},"responses":{"2XX":{"description":"Received. Anything else is retried: after 1, 5 and 30 minutes, then 2 and 12 hours, then given up."}}}},"board.failed":{"post":{"operationId":"webhookBoardFailed","summary":"A board could not be read","description":"Delivered when a check of a tracked board fails. A receiver that has heard nothing about a board for a while can tell from this whether that is quiet or broken.","tags":["Webhooks"],"security":[],"parameters":[{"name":"X-SlashTrace-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256>` over `${t}.${raw body}` with the webhook’s secret. Verify against the raw bytes before parsing, and reject a `t` more than five minutes old."},{"name":"X-SlashTrace-Event","in":"header","required":true,"schema":{"type":"string"},"description":"The event type, same as `type` in the body."},{"name":"X-SlashTrace-Delivery","in":"header","required":true,"schema":{"type":"string"},"description":"The delivery id, same as `id` in the body."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookBoardFailed"}}}},"responses":{"2XX":{"description":"Received. Anything else is retried: after 1, 5 and 30 minutes, then 2 and 12 hours, then given up."}}}},"ping":{"post":{"operationId":"webhookPing","summary":"A test delivery","description":"Sent when someone presses Test on a webhook in Settings, so a receiver can be checked end to end before any real event. Not retried, and not subscribable.","tags":["Webhooks"],"security":[],"parameters":[{"name":"X-SlashTrace-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"`t=<unix seconds>,v1=<hex HMAC-SHA256>` over `${t}.${raw body}` with the webhook’s secret. Verify against the raw bytes before parsing, and reject a `t` more than five minutes old."},{"name":"X-SlashTrace-Event","in":"header","required":true,"schema":{"type":"string"},"description":"The event type, same as `type` in the body."},{"name":"X-SlashTrace-Delivery","in":"header","required":true,"schema":{"type":"string"},"description":"The delivery id, same as `id` in the body."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPing"}}}},"responses":{"2XX":{"description":"Received. Anything else is retried: after 1, 5 and 30 minutes, then 2 and 12 hours, then given up."}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"st_live_…","description":"A Slash Trace API key, created self-serve in Settings, API keys. Keys are whole-account: they see and change everything the account can. Used by every `/v1` endpoint."},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authorization code with PKCE (S256), for the MCP endpoint. Discovery: https://api.slashtrace.com/.well-known/oauth-authorization-server (RFC 8414) and https://api.slashtrace.com/.well-known/oauth-protected-resource (RFC 9728). Dynamic client registration (RFC 7591) and client ID metadata documents are both accepted; public clients authenticate with `none`.","flows":{"authorizationCode":{"authorizationUrl":"https://api.slashtrace.com/oauth/authorize","tokenUrl":"https://api.slashtrace.com/oauth/token","refreshUrl":"https://api.slashtrace.com/oauth/token","scopes":{"boards:read":"List tracked boards, search roles, and read what is new.","boards:write":"Add a board to the list or remove one from it.","offline_access":"Receive a refresh token, so the connection outlives the access token."}}}}},"schemas":{"Board":{"type":"object","description":"A careers page this account tracks.","required":["id","company","domain","careersUrl","status","tracking","openJobs","closedJobs","lastCheckedAt","lastSuccessAt","addedAt"],"properties":{"id":{"type":"string","description":"Stable identifier. Use it in listJobs and deleteBoard."},"company":{"type":["string","null"],"description":"Company name, once a scrape has identified it. Null until then, never a guess."},"domain":{"type":["string","null"],"description":"Company domain, once identified."},"careersUrl":{"type":"string","format":"uri","description":"The careers page being read."},"status":{"type":"string","enum":["pending","scraping","ready","failed"],"description":"Lifecycle of the board itself: discovery, scraping, ready, or failed."},"tracking":{"type":"string","enum":["active","locked"],"description":"Lifecycle of this account's subscription to the board. `locked` means it is on the list but past the plan's board limit, so it is not read and returns no jobs."},"openJobs":{"type":"integer","minimum":0},"closedJobs":{"type":"integer","minimum":0},"lastCheckedAt":{"type":["string","null"],"format":"date-time"},"lastSuccessAt":{"type":["string","null"],"format":"date-time"},"addedAt":{"type":["string","null"],"format":"date-time"}}},"Job":{"type":"object","description":"One role on a tracked board.","required":["id","boardId","company","title","location","department","url","status","firstSeenAt","lastSeenAt","closedAt"],"properties":{"id":{"type":"string"},"boardId":{"type":"string","description":"The board this role was read from."},"company":{"type":["string","null"],"description":"Denormalised from the board, so no join is needed."},"title":{"type":"string"},"location":{"type":["string","null"]},"department":{"type":["string","null"]},"url":{"type":["string","null"],"format":"uri","description":"The posting on the employer’s site."},"status":{"type":"string","enum":["open","closed"]},"firstSeenAt":{"type":["string","null"],"format":"date-time","description":"When Slash Trace first saw the role — not when the employer posted it. This is what `addedAfter` filters on."},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"closedAt":{"type":["string","null"],"format":"date-time"}}},"Status":{"type":"object","required":["status","account","key","rateLimit","polledAt"],"properties":{"status":{"type":"string","enum":["ok"]},"account":{"type":"object","required":["plan","boards"],"properties":{"plan":{"type":"string","enum":["free","pro","ultra"]},"boards":{"type":"object","required":["tracked","limit","locked"],"properties":{"tracked":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":0,"description":"The plan's board limit."},"locked":{"type":"integer","minimum":0,"description":"Tracked but past the limit, so not being read."}}}}},"key":{"type":["object","null"],"description":"The key this request used, as shown in Settings. Never the secret.","properties":{"name":{"type":"string"},"hint":{"type":"string"},"lastUsedAt":{"type":["string","null"],"format":"date-time"}}},"rateLimit":{"type":"object","description":"The per-account budget: 60 requests per sliding minute. `remaining` already counts this call.","properties":{"limit":{"type":"integer"},"remaining":{"type":"integer"},"resetAt":{"type":["string","null"],"format":"date-time"}}},"polledAt":{"type":"string","format":"date-time"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","email_not_verified","plan_required","rate_limited","invalid_request","not_found","already_tracked","internal_error"],"description":"Stable, branch on this. `unauthorized`: no or invalid key. `email_not_verified`: confirm the account email. `plan_required`: the plan has no API access. `rate_limited`: wait `retryAfter` seconds. `invalid_request`: see `details`. `not_found`: no such board on this account. `already_tracked`: the board is already on the list. `internal_error`: ours, retry."},"message":{"type":"string","description":"Human-readable. May be reworded at any time; the code may not."},"details":{"description":"Validation issues, on `invalid_request`."},"retryAfter":{"type":"integer","description":"Seconds to wait, on `rate_limited`."}}}}},"WebhookBoard":{"type":"object","description":"The board the event is about. A subset of Board — enough to name it without a second request.","required":["id","company","domain","careersUrl"],"properties":{"id":{"type":"string","description":"The board id, as listBoards returns it."},"company":{"type":["string","null"]},"domain":{"type":["string","null"]},"careersUrl":{"type":["string","null"],"format":"uri"}}},"WebhookJobEvent":{"type":"object","description":"Roles that appeared on (`job.found`) or went from (`job.archived`) one board, in one check. One delivery per board per check, however many roles it carries.","required":["id","type","createdAt","board","jobs"],"properties":{"id":{"type":"string","description":"The delivery id. Also sent as `X-SlashTrace-Delivery`. Retries reuse it; dedupe on it."},"type":{"type":"string","enum":["job.found","job.archived"]},"createdAt":{"type":"string","format":"date-time","description":"When the check that found this ran."},"board":{"$ref":"#/components/schemas/WebhookBoard"},"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"},"description":"The same object listJobs returns, so a receiver and a poller see one shape."},"truncated":{"type":"integer","description":"Present only when more than 100 roles changed at once: how many were left out. Fetch the rest from listJobs."}}},"WebhookBoardFailed":{"type":"object","description":"A tracked board could not be read. Sent so a receiver can tell silence from breakage.","required":["id","type","createdAt","board","error"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["board.failed"]},"createdAt":{"type":"string","format":"date-time"},"board":{"$ref":"#/components/schemas/WebhookBoard"},"error":{"type":["string","null"],"description":"Why, in the words the scraper used."}}},"WebhookPing":{"type":"object","description":"Sent by the Test button in Settings. Never retried, and not something a webhook subscribes to.","required":["id","type","createdAt","webhook"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["ping"]},"createdAt":{"type":"string","format":"date-time"},"webhook":{"type":"object","required":["id","events"],"properties":{"id":{"type":"string"},"events":{"type":"array","items":{"type":"string"}}}}}},"AddBoardResult":{"type":"object","required":["board","locked"],"properties":{"board":{"$ref":"#/components/schemas/Board"},"locked":{"type":"boolean","description":"True when the board is over the plan’s limit: it is listed but not read until there is room."}}}},"responses":{"InvalidRequest":{"description":"A parameter is wrong. `error.details` lists the issues.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"No key, a malformed header, or a key that does not resolve.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Authenticated, but the email is unverified (`email_not_verified`) or the plan has no API access (`plan_required`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such board on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Over 60 requests in the last minute. Wait `error.retryAfter` seconds.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}