API reference
Hiring Signals API
JSON in and out. Three rails share one meter: 6 credits a company scan, plus 1 credit per role when you ask for descriptions. Reading a scan you already paid for is free.
Quickstart
One POST, synchronous. Discovery, the roles and the delta all come back together.
curl -X POST https://hiring.ounie.com/api/scans \
-H "Authorization: Bearer hrs_live_…" \
-H "Content-Type: application/json" \
-d '{"domain":"mercury.com"}'board_not_found means we could not reach a job board for that company. It is not a reading of zero open roles, and it is never billed. Roughly one company in five ends there.Authentication
- Your app key —
Authorization: Bearer hrs_live_…, minted at /dashboard/api-keys. Five active keys per account. - Your Ounie master key — an
ounie_live_…key from ounie.com works across the whole Ounie app fleet, once you switch it on in your ounie.com API-key settings. - The session cookie — the dashboard uses the same endpoints you do, signed in on
.ounie.com. - No auth at all — the x402 rail takes USDC on Base instead.
Hosts that cannot set an Authorization header (the Ounie AI Team’s manual MCP entry, for one) can pass ?api_key=hrs_live_… on any endpoint.
POST /api/scans
Costs 6 credits, or 6 + 1 per described role. Credits are reserved against the real role count, not a guess, and settled on what was produced.
| field | type | meaning |
|---|---|---|
| domain | string | The company's domain. A full URL is fine — we reduce it to the host. Required unless you supply platform + board_token. |
| platform | "greenhouse" | "lever" | "ashby" | Skip discovery by naming the board's platform. Must be sent with board_token. |
| board_token | string | The board's own label — `mercury` in job-boards.greenhouse.io/mercury. Requires platform. |
| include_descriptions | boolean (false) | Return each role's description text, the tools named in it, and Ashby compensation. Adds 1 credit per role. |
| max_role_details | integer ≤ 300 | Cap how many roles get descriptions so a large board cannot surprise you. |
The response
{
"ok": true,
"scan": {
"id": "…",
"domain": "mercury.com",
"company": "Mercury",
"outcome": "found",
"outcome_meaning": "We reached this company's live job board …",
"headline": "Mercury has 55 open roles. 3 opened, 2 closed since Jul 28, 2026.",
"platform": "greenhouse",
"board_token": "mercury",
"roles": [
{
"id": "8403127002",
"title": "Staff Software Engineer, Payments",
"team": "Engineering",
"function": "engineering",
"seniority": "staff",
"location": "San Francisco, CA",
"remote": null,
"employmentType": null,
"postedAt": "2026-07-14T09:04:24-05:00",
"url": "https://job-boards.greenhouse.io/mercury/jobs/8403127002",
"description": null,
"salary": null
}
],
"summary": {
"totalOpen": 55,
"byFunction": { "engineering": 21, "sales": 9, "design": 4 },
"bySeniority": { "senior": 14, "staff": 6, "unknown": 22 },
"locations": [{ "location": "San Francisco, CA", "count": 18 }],
"remote": { "yes": 0, "no": 0, "unknown": 55 },
"toolsMentioned": [],
"descriptionsIncluded": false
},
"delta": {
"since": "2026-07-28T11:02:19.441Z",
"previousTotal": 54,
"netChange": 1,
"opened": [{ "id": "…", "title": "Solutions Engineer", "function": "sales", "url": "…" }],
"closed": [{ "id": "…", "title": "Support Lead", "function": "support", "url": "…" }],
"newFunctions": ["data"],
"closedFunctions": []
},
"discovery": [
{ "platform": "greenhouse", "token": "mercury", "method": "careers-page",
"status": 200, "jobs": 55,
"note": "selected — most open roles of the boards that answered" }
],
"roles_detailed": 0,
"credits_spent": 6
}
}Board discovery
You give us a domain; we have to work out which applicant-tracking system that company uses. Three steps, in order:
- Their own careers page. We fetch
/careers,/jobs,/company/careers,/about/careersand a few more, and look for a board link in the markup. This alone resolves about half of companies. - A guess from the domain. Most companies name their board after themselves —
airtable.comisairtableon Greenhouse. This takes the hit rate to roughly four in five. - Probe every candidate in parallel and keep the fullest board. Never the first one that answers. Airtable and Mercury both return 200 on Greenhouse and on Ashby; their Ashby boards are empty shells while the real roles sit on Greenhouse. A first-match design reports zero for both.
Every candidate we probed comes back in discovery[] with its status, its role count and why it did or did not win — so a surprising answer is auditable rather than mysterious.
outcome: "board_not_found" and a full refund. If you know where they post, send platform + board_token and we skip discovery entirely.Outcomes
| outcome | meaning | billed |
|---|---|---|
| found | We read the company's live board. `roles` is complete for that board — including when it is empty, which is a real reading of a hiring freeze. | 6 cr |
| board_not_found | No reachable board on any supported platform. We have no reading. Do not render this as zero open roles. | 0 — refunded in full |
The distinction is the whole product. A sales team acting on a fabricated zero is worse off than one with no data at all, so a non-answer is always labelled and never charged for.
The delta
Every successful scan writes a snapshot of that board. The next scan of the same company diffs against it and returns delta.
| field | meaning |
|---|---|
| since | Timestamp of the snapshot we compared against. Always stated, never assumed. |
| previousTotal | How many roles were open in that earlier reading. |
| netChange | totalOpen − previousTotal. |
| opened[] | Roles present now and absent then. The buying signal. |
| closed[] | Roles present then and absent now — filled, or pulled. |
| newFunctions[] | Functions with roles now that had none before. The loudest field here. |
| closedFunctions[] | Functions that had roles and now have none. |
- The diff key is the platform’s own job id, so a retitled req is not mistaken for a new one, and two roles with the same title do not collapse into one.
- The first scan of a board has no delta. You get
delta: nulland adeltaNotesaying so — not a row of zeros that would read as “nothing changed”. - The snapshot corpus is per board, not per account. A job board is public, so every rail contributes to it and every rail benefits — including keyless x402 callers, who have no account to keep history against.
- We prefer a comparison snapshot at least an hour old, so two scans a minute apart do not return a meaningless empty delta. When the only earlier snapshot is more recent than that, we use it and
sincesays so.
Field caveats
Three applicant-tracking systems that agree on almost nothing get normalized into one shape. Where a platform genuinely does not publish something, the field is null and the scan carries a capabilities block saying so — we never infer a value to make the shape look complete.
| field | caveat |
|---|---|
| remote | A real boolean on Lever and Ashby, which publish a flag. NULL on every Greenhouse role — Greenhouse location is free text like “Remote - US”, and parsing that into a boolean would be right most of the time, which is not a property you can filter on. A uniform remote filter across all three is not possible. |
| salary | Ashby only, and only when include_descriptions is set. Greenhouse and Lever publish none. Some Ashby boards return the compensation object with every field null, so we return null rather than an empty shell that reads like a measurement. |
| toolsMentioned | Extracted from description text we actually fetched, against a closed vocabulary. Empty unless you bought descriptions — a company's stack is never guessed from job titles. |
| function | Our classification, from the title, so it is comparable across companies that call the same team different things. Deterministic — no model assigns it. |
| team | The platform's own department or team label, verbatim. Null when the platform does not give one. |
| seniority | From the level token in the title. `unknown` whenever there is no token — a bare “Engineer” is not assumed to be mid-level. |
| description | Plain text. Greenhouse ships it as entity-escaped HTML whose inner text carries its own entities, so it is decoded twice before the tags are stripped. |
Functions: engineering, product, design, data, security, it, sales, marketing, customer_success, support, finance, legal, people, operations, research, executive, other. Seniorities: intern, junior, mid, senior, staff, principal, lead, manager, director, vp, executive, unknown.
Platforms
Three ship. The property they all share is the one that matters: a board that does not exist returns a clean 404, which is what lets us tell “not on this platform” apart from “not hiring”.
| platform | endpoint | miss |
|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/<token>/departments | 404 with {"status":404,"error":"Job not found"} |
| Lever | api.lever.co/v0/postings/<token>?mode=json | 404 with {"ok":false,"error":"Document not found"} |
| Ashby | api.ashbyhq.com/posting-api/job-board/<token> | 404 with the bare body `Not Found` |
And the ones deliberately absent, with the reason for each:
| platform | why not |
|---|---|
| SmartRecruiters | A nonsense company id returns HTTP 200 with {"totalFound":0,"content":[]} — byte-identical to a real company with no openings. There is no way to tell 'not on this platform' from 'not hiring', so shipping it would mean guessing. |
| Workable | The widget endpoint discriminates 200/404, but its field shape was never verified against enough real boards to normalize honestly. Absent rather than half-mapped. |
| Recruitee | No keyless endpoint that answers reliably for an arbitrary company. |
| Workday | Each tenant runs its own host and its own path; there is no keyless, guessable endpoint. A per-tenant integration is not the same product. |
{"totalFound":0,"content":[]} — byte-identical to a real company with nothing open. There is no way to distinguish them, so shipping it would mean guessing, so it is not in the enum, the docs, or the marketing.Reading past scans
Free — you already paid for these.
GET https://hiring.ounie.com/api/scans?limit=20
GET https://hiring.ounie.com/api/scans?domain=mercury.com
GET https://hiring.ounie.com/api/scans/<scan_id>Filtering by domain is how you pull a company’s whole scan history and watch the board move over time.
MCP
Endpoint https://hiring.ounie.com/api/mcp (legacy SSE: /api/sse)
Auth Authorization: Bearer hrs_live_…
…or https://hiring.ounie.com/api/mcp?api_key=hrs_live_…| tool | cost | does |
|---|---|---|
| scan_company_hiring | 6 cr | Read a company's board and diff it against the last reading. |
| get_hiring_scan | free | Retrieve a scan by id. |
| list_hiring_scans | free | List past scans, optionally for one domain. |
| get_coverage | free · public | Which platforms are read, which are refused and why, how discovery works, and every field caveat. |
| get_credit_balance | free | Spendable Ounie credits. |
| get_pricing | free · public | Credit and x402 pricing. |
| whoami | free | The key's owner metadata. |
get_coverage before you interpret a result. An absent company is not a company that is not hiring, and an agent that does not know the difference will report one as the other.x402 — keyless, pay per call
Agents with no Ounie account pay in USDC on Base. Because the job boards cost us nothing to read, discovery runs before the quote.
curl -X POST https://hiring.ounie.com/api/x402/scan \
-H "Content-Type: application/json" \
-d '{"domain":"mercury.com"}'
# → 402
{ "x402Version": 1,
"accepts": [{ "scheme": "exact", "network": "base",
"maxAmountRequired": "72000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x…",
"extra": { "name": "USD Coin", "version": "2" } }],
"preview": { "company": "Mercury", "platform": "greenhouse",
"open_roles": 55, "credits_equivalent": 6 } }
curl -X POST https://hiring.ounie.com/api/x402/scan \
-H "X-Payment: <base64 signed payload>" \
-d '{"domain":"mercury.com"}'- A company with no reachable board is a 400, with no price in the body. You never sign an authorization for work that cannot be delivered. The response carries the full
discovery[]trail and a hint to retry with a board token. - The 402 quotes the exact final amount, descriptions included — not a ceiling. On this rail you cannot settle for less than you quoted (the
exactscheme compares the signed value, so a reduced settlement is refused after the work is done), and quoting exactly removes the failure mode. - Verify → run → settle. Your signature is checked off-chain first, the board is read, and only then does money move. Nothing is settled for a result we could not produce.
- The 402 also carries a free
preview: the company, the platform and the open-role count, so you can decide whether the answer is worth $0.072 before signing anything. - x402 scans still get a
delta. The snapshot corpus is keyed by board, not by account, so a keyless caller both reads from it and contributes to it.
Errors
| status | body | meaning |
|---|---|---|
| 400 | invalid_domain / unsupported_platform / board_token_required | Malformed request. Refused before any price is quoted. |
| 400 | board_not_found (x402 only) | No reachable board. Free, with the discovery trail attached, and no quote. |
| 401 | unauthorized | Missing or bad Bearer / cookie. Never a redirect — these routes are outside the proxy matcher. |
| 402 | insufficient_credits | Carries required_credits, balance_credits and buy_credits_url (https://ounie.com/dashboard/settings). |
| 402 | partial_delivery (x402 only) | Descriptions came back short of the quote after a retry. Nothing was settled. |
| 429 | too_many_running | More than 5 scans in flight for this account. |
| 502 | scan_failed | The scan could not be completed. Credits were refunded pool-exact; on x402 nothing was settled. |
Limits
| limit | value |
|---|---|
| Roles normalized per board | 1000 |
| Role descriptions per scan | 300 |
| Concurrent scans per account | 5 |
| Active API keys per account | 5 |
| Scan price | 6 credits · $0.072 on x402 |
| Role description price | 1 credit each |
We self-throttle and cache board discovery per domain. None of the three platforms publishes a rate-limit header, so being courteous is on us rather than on them.