API docs

Every Reqbeat hiring-signal endpoint, the field dictionary, and webhook signature verification — generated from the live API, never hand-maintained.

Read September 14, 2026 at 00:11 UTC · Refreshed every 3 hours

API reference

Endpoints

GET /v1/whoami What your API key resolves to
What the presented key resolves to. The response can only ever reflect the presented key's own customer, never another's.
POST /v1/quickstart Get started in five minutes
5-min quickstart: curl/Python/TS/MCP/Clay snippets with the caller's own live key injected, plus an auto-created watch on a high-motion company on `body.webhook_url` so a webhook fires shortly after signup. Not billable -- getting started must never cost quota. Idempotent: re-opening the quickstart never mints a second watch or webhook endpoint.
GET /v1/onboarding Guided watch and webhook onboarding
Guided watch/webhook onboarding flow: first watch -> first signed webhook delivered -> the 80%-quota expansion nudge. Not billable, same as `/quickstart`. Idempotent: re-polling never mints a second watch or endpoint and never double-counts a delivery.
GET /v1/keys List your API keys
Account-scoped key listing -- every key (active and revoked) for the caller's own customer, with `last_used_at` from the usage ledger.
POST /v1/keys Issue a new API key
Self-serve key issuance: a developer who already holds one key can mint another for their OWN account -- no operator step. `scopes` is the only field the request body carries. The new key's customer AND its tier are both taken from the AUTHENTICATED caller's own key, never from the request, so a key can never be issued for another tenant nor carry more entitlement than the key that authorized it. Sending a `tier` is rejected with a 422 rather than ignored. To change your tier, change your plan -- the new tier then applies to every key on the account. The mint also inherits the parent key's acquisition surface, exactly as rotation carries it forward: an additional self-serve mint is the same acquisition, and the same entitlement, as the key that authorized it.
DELETE /v1/keys/{key_id} Revoke an API key
Self-serve key revocation, scoped to the caller's own customer -- revoking another tenant's key id 404s, identical to revoking one that doesn't exist at all (fail-fast, no cross-tenant leak).
POST /v1/keys/{key_id}/rotate Rotate an API key
Agent-driven key rotation: mints a replacement key bound to the SAME account and revokes `key_id` in one call -- an autonomous agent rotates its own credential without an operator or a human seat in the loop, so usage keeps billing to the account. Scoped to the caller's own customer; `key_id` need not be the presented key. 404 for an id that doesn't exist, belongs to another tenant, or is already revoked -- indistinguishable, same as `delete_key`.
GET /v1/usage Your metered usage over a rolling window
Metered usage for the caller's customer over a rolling window -- `window` is hours (default 24). Aggregates across every key the customer holds, not just the one presented on this call. `keys[]` splits those same totals per key, so a customer holding one key per environment (or per client) can attribute its spend. It lists the keys with at least one metered event in the window, ascending by `key_id`; each of the five totals sums across `keys[]` to the customer-level number printed beside it.
GET /v1/usage/dashboard Your usage and billing dashboard
Usage & billing dashboard: per-key usage vs quota (green/amber/red), the account's spend-cap status, an in-context 402 upgrade prompt when either is exhausted, and the 80%-quota expansion nudge -- every number read off the same meter enforced at request time. Not billable: reading the dashboard must never itself cost quota. `free_tier` reports the free allowance and how much of it this billing month has spent (null for an account holding no free key, which neither free wall can bind). `blocked_deliveries` counts the changes we DECIDED not to send you since that period began, per reason: `wall_blocked` is the free monthly change allowance, `cap_blocked` is your own spend cap. Both are terminal and are never retried, so a non-zero count is why a watch has gone quiet -- as distinct from nobody hiring.
POST /v1/account/pause Pause your account's metering
Pause-instead-of-cancel: suspends metering for the caller's OWN account -- every billable route 403s with `account_paused` until resumed. Watches are untouched.
POST /v1/account/resume Resume a paused account
Resume a paused account -- the next billable call the caller's keys make is enforced normally again.
GET /v1/companies/{company_id}/is-hiring Is this company hiring?
Cheap is-hiring gate -- ATS/board-only, freshness-floored: qualifies a company before an agent spends on a richer call. A free-tier account past its cost-to-serve cap degrades to a demand-signal-only path, returning `202 {status: "over_cap"}` instead of a served read -- the same convention as the cold-tail `202 {job_id, status: "crawling"}`.
GET /v1/companies/{company_id}/open-reqs A company's open reqs, deduped across boards
The company's current active reqs, deduped across boards -- ATS/board-only, freshness-floored. The same real req posted on two boards appears once, with both boards listed. An unresolvable `function` -- anything that is not a function taxonomy id -- is rejected with 422 instead of returning a silently-empty result. `limit` is bounded: an oversized page is rejected with 422 rather than truncated.
GET /v1/companies/{company_id}/enrichment Company firmographics from ATS and job boards
Basic ATS/board-derived company firmographics -- no LinkedIn-derived field ever appears here by construction. Freshness-floored: `boards` only reflects ATS/board sources observed at or before the floor cutoff.
GET /v1/companies/{company_id}/hiring-pulse A company's hiring velocity and momentum
A company's hiring velocity/direction/momentum in one call -- the first paid route, honoring `max_age` (seconds). Cold (no ATS/board data at all, and `max_age` demands freshness beyond nothing) returns `202 {job_id, status: "crawling"}` instead of a synchronous body.
GET /v1/reqs/search Find companies hiring for a role
Reverse who's-hiring-for {role, geo}: companies with active reqs matching `role` (posting title) / `geo` (country) / `since` (first-seen lower bound), deduped by company, each with its full hiring pulse and the specific reqs that matched. ATS-only, freshness-floored, keyset-paginated via `next_cursor` (the last page's highest `company_id`). `role` is a full-text query over the raw posting title, not an id: type the role the way a posting would spell it (`staff engineer`), every word must match, and a role that matches nothing comes back as an empty page rather than an error. The two filters are deliberately asymmetric -- an unresolvable `geo` IS rejected with 422, because a country either is or is not in the stored vocabulary, whereas an unmatched title is an honest zero result. `industry`, `industry_terms` and `headcount_band` cut the corpus down to your ICP's companies. Repeat a parameter to OR its values; different parameters AND. They are filters only -- no company facet is added to the response by passing one -- and a company whose fact we do not hold is never matched, so a cut is honestly narrow rather than quietly padded. `limit` is bounded: this endpoint meters the billed `source_row` atom off its own serialized rows, so an unbounded page is an unbounded invoice. An out-of-range `limit` is rejected with 422 rather than silently truncated; page through the full set with `next_cursor`. The shared public demo key gets a tighter page instead -- it bills nothing, so no ledger-derived gate can bound it, and a full-size page would make a world-readable credential a bulk-export window onto the fresh index.
POST /v1/reqs/qualified Find live reqs matching an ICP
The live reqs matching a full ICP definition -- "senior Go roles at 51-200-person fintechs in the EU, agencies excluded" -- as a page you pull, rather than a webhook you register and wait on. `criteria` is the same closed vocabulary a watch takes (`POST /v1/watches`), evaluated here in SQL instead of on the dispatcher's tick: three namespaced groups (`company`, `req`, `event`), AND across groups and fields, OR within a field. A field outside the vocabulary is a 422, and so is one this projection cannot answer -- `event.function` (a Python-derived taxonomy bucket, not a column; use `req.job_family`) and `event.event_type` (a ledger transition, where a row here is a live posting; use `/v1/events`). Never a silently-ignored filter: a dropped criterion answers a wider question than the caller asked. A NULL fact never matches (fail-closed), so a criterion on a sparse facet is honestly narrow rather than wrong. The one exception is `req.title_excludes`, which vetoes and therefore does NOT fire on an absent title -- an unknown fact cannot prove an exclusion either. POST rather than GET because criteria is a nested object; a URL-encoded AND-of-ORs would be an invented serialization, which is the open grammar the closed vocabulary exists to refuse. Rows carry the normalized req layer no other route serves -- skills, seniority, job family, declared **and** inferred compensation (with `salary_estimate_samples`, so an estimate can be judged rather than trusted), the eligibility block, apply actionability, lifecycle and dedup -- plus `req_key`, the ledger's own identity for the req, so a page joins to `/v1/events`. `salary_min_usd` and `salary_max_usd` are the pay band: either alone is half-open, both together bracket one comparable yearly-USD figure per row. That figure is the employer's declared salary when there is one (the midpoint of a declared range) and our inferred median otherwise, and the inferred one qualifies only above a sample floor -- so a band is never satisfied by a thinly-evidenced guess, and never half by the employer's number and half by ours. A req with no salary at all matches no band (fail-closed), and an inverted band is a 422 rather than a confidently empty page. `exclude_agencies` (default true) removes staffing agencies, aggregators and job boards, so a row names the employer. `live_only` (default true) keeps expired and superseded postings out. Keyset-paginated: pass the previous page's `next_cursor`, which is monotone, so it doubles as "everything new since my last call". `limit` is bounded -- this endpoint meters the billed `source_row` atom off its own serialized rows, so an out-of-range page is a 422 rather than a silent truncation.
GET /v1/jobs/search Search open roles across companies
Flat, role-granular job search: the individual open roles across companies matching `role` (function) / `geo` (country) / `since` (first-seen lower bound), one row per logical req. ATS-only, freshness-floored, PK-ordered and keyset-paginated via the opaque `next_cursor` (the page's last `company_id:req_key`). The company-granular reverse view is `/v1/reqs/search`; full multi-board dedup for one company is `/v1/companies/{company_id}/open-reqs`. `industry`, `industry_terms` and `headcount_band` cut the corpus down to your ICP's companies. Repeat a parameter to OR its values; different parameters AND. They are filters only -- no company facet is added to a served row by passing one -- and a company whose fact we do not hold is never matched, so a cut is honestly narrow rather than quietly padded. `q` is free-text title search, ranked: it expands semantically to nearby job titles and matches them against the posting's own title, and each row carries the `relevance` (0-1) it scored. `sort` picks the ordering -- 'relevance' (the default whenever `q` is given) or 'recency'. Omit both and the page keeps its shipped stable order exactly. `q` is INDEPENDENT of `role`: pass both to search titles within one function. An unresolvable `role` -- anything that is not a function taxonomy id -- is rejected with 422 instead of returning a silently-empty result. `limit` is bounded: an oversized page is rejected with 422 rather than truncated -- page through the full set with `next_cursor` instead of raising `limit`. The shared public demo key gets a tighter page instead -- it bills nothing, so no ledger-derived gate can bound it, and a full-size page would make a world-readable credential a bulk-export window onto the fresh index. `sort='relevance'` without a `q`, and a `next_cursor` replayed under a different `sort`, are 422s for the same reason: an order other than the one asked for reads as a measurement.
GET /v1/jobs/role One open role's detail
One open role, addressed by the `(company_id, req_key)` pair every `/v1/jobs/search` row carries -- the follow-up call for a role you already hold an identifier for, instead of re-pulling the whole company through `/v1/companies/{company_id}/open-reqs`. Scoped exactly as `/v1/jobs/search` is: ATS-only and freshness-floored, one row per logical req. The body adds `raw_title` (the posting's own title) and `boards` -- the full, deduped list of boards reporting this req, which a search page cannot afford per row. A req that does not exist, is closed, or has not reached the freshness floor is a 404, never an empty 200 -- and a 404 is not metered.
GET /v1/companies/{company_id}/first-hire A company's first hire in each function
The earliest first-in-function event per function -- detects a new budget line. Sourced from the role-slot recompute aggregate, not a specific board posting; `source_board` says so honestly rather than being excluded outright. Free-tier reads are history-capped. An unresolvable `function` -- anything that is not a function taxonomy id -- is rejected with 422 instead of returning a silently-empty result.
GET /v1/companies/{company_id}/repost-pain A company's hardest-to-fill reqs
Hard-to-fill reqs by repost count, hardest first -- ATS-only, freshness-floored. Free-tier reads are history-capped.
GET /v1/companies/{company_id}/ats-migrations A company's ATS vendor switches
A company's ATS-vendor switches -- ledger-backed, with a provenance-wrapped `occurred_at`. No ATS-only filter needed: ATS platform accounts have no LinkedIn dimension.
GET /v1/markets/role-demand Market-wide demand for a role
Market-wide (no single-company scope) active-req demand series for `role` / `geo` -- ATS-only by construction. An unresolvable `role` -- anything that is not a function taxonomy id -- is rejected with 422 instead of returning a silently-empty result.
GET /v1/companies/{company_id}/pre-action-brief A company's whole pre-action brief in one call
The motion atoms + history primitives pre-joined into one bounded, compact call -- an agent's whole pre-action context in one round-trip instead of five. Honors `max_age` (seconds); cold returns `202 {job_id, status: "crawling"}`, same as hiring-pulse.
GET /v1/events The change feed since your cursor
The `since=cursor` diff feed -- ledger events with `event_seq > since`, ordered ascending, plus a `next_cursor` an agent replays instead of polling or re-scraping. Meters one `change` unit per event RETURNED, independent of poll count: an empty page costs nothing. A free-tier caller is change-feed-gated to the same freshness floor every other free atom uses. `limit` is bounded: an oversized page is rejected with 422 rather than truncated -- replay with `next_cursor` instead of raising `limit`.
POST /v1/webhooks Register a webhook endpoint
Register a webhook delivery target -- `secret` is stored as the endpoint's HMAC signing key, scoped to the caller's own customer, and never logged. Idempotent on (customer, url): re-registering the same target returns the SAME id rather than a second row, so a retrying caller ends up with one endpoint it can hand straight to `POST /v1/watches`. Omitted `secret` is generated server-side.
POST /v1/watches Watch a company or a saved search
Subscribe to hiring events on a registered webhook -- scoped to the caller's own customer: `webhook_endpoint_id` must belong to the same customer. Meters one `watch` unit. The watch covers one company (`company_id`) or a saved search (`criteria`) -- exactly one, and a criteria outside the closed vocabulary is a 422 raised before any row is written. A free key holds a limited number of watches AT ONCE; the one past that is a 402 carrying the upgrade path, checked before the watch is created, so a walled call leaves no subscription behind. Cancelling a watch returns the slot.
GET /v1/watches List your live watches
The caller's own live watches -- what is currently subscribed on their customer, newest first, canceled ones excluded. A "Watch this search" signup lands with a watch already installed, and this is where its owner confirms what it watches and which endpoint it delivers to. Not billable, like the other self-serve introspection reads (`/webhook-deliveries`): reading your own subscriptions is account management, not metered corpus access. Each watch also reports `fires_last_hour` against its declared `max_fires_per_hour`, and `rate_limited` when the two have met: fires past that cap are DROPPED rather than queued, so this is the only place a saturated watch can be told from a quiet one. And each reports its heartbeat -- `last_fired_at`, `fires_last_7d`, `fires_last_30d` and a one-word `status` -- so a watch that is silent because nobody is hiring reads differently from one that is silent because its criteria can never match, its plan stopped it, or its endpoint is down. Absence is the customer's only evidence a watch is mis-specified, and for a metered product it is also the receipt for the months where the honest answer is "nothing matched".
DELETE /v1/watches/{watch_id} Cancel a watch
Cancel a watch, scoped to the caller's own customer -- canceling another tenant's watch id 404s, identical to canceling one that doesn't exist (fail-fast, mirrors `delete_key`). Canceling one's own already-canceled watch is a 204 no-op, so the verb stays idempotent.
GET /v1/webhook-deliveries Your webhook delivery log
The caller's own self-serve webhook delivery log -- every delivery for their own customer, most recent first. Each entry carries the `idempotency_key` the delivery was sent with (also its `X-Plane-Idempotency-Key` header, and the id of the billed unit on your invoice) plus the `response_status` your endpoint returned, so a charge can be matched to the delivery that earned it without a support thread. A NULL `response_status` means we observed no HTTP response: the delivery has not been attempted yet, the connection failed outright, or it was mailed to a hosted-email endpoint.
GET /v1/webhook-deliveries/{delivery_id} One webhook delivery's detail
One delivery's detail, scoped to the caller's own customer.
POST /v1/webhook-deliveries/{delivery_id}/replay Replay a webhook delivery
Reopens a webhook delivery for the next dispatcher tick to redeliver -- a self-serve replay debugger. Scoped to the caller's own customer, idempotent, and within the replay SLA window.
POST /v1/companies/{company_id}/outcomes Report a conversion outcome
Write back a conversion outcome for `company_id`. Appends to the caller's own outcome labels, which are retention-protected.
GET /v1/attribution See which delivered signals you won
"Signal #N -> you won." -- the caller's own outcome labels joined back to the signals this plane actually delivered them. A label for a company we never signalled is deliberately absent: this is an attribution view, not a label dump. `attributed_14d` counts the ones that landed in the last 14 days.
GET /v1/companies/{company_id}/intent A company's calibrated intent score
The calibrated-intent score + coverage meter for `company_id` -- computed live off every customer's submitted outcome labels. `score` is `None` below the coverage-gating minimum (never a fabricated number).
GET /v1/companies/resolve Find a company's id by domain or name
Turn a website or a company name into the `company_id` every company-scoped endpoint takes. Pass exactly one of `domain` or `name`; both or neither is a 422 naming that rule. A domain returns the companies registered at it; a name returns up to five candidates, each with a `match_confidence` -- 1.0 for an exact name, 0.8 for a match once legal suffixes are dropped. Each company carries its `country_code` and `coverage_status`, and no hiring signal -- ask `/v1/companies/{company_id}/is-hiring` for that. Nothing matching is an empty array, never a 404. Not billable.
POST /v1/clay/enrich Enrich a company for a Clay column
Clay HTTP-provider hiring-motion enrichment column -- entity-resolves `domain`/`name` to a company via the local plane cache, then composes company enrichment and hiring pulse into a flat, Clay-column-shaped response: no field beyond this declared model ever leaks, every field is ATS-only and provenance-backed by its own primitive. Metered to the presented (Clay account) key. A miss is a 200, not a 404: a 404 blanks the Clay cell, which the customer reads as "not hiring". It carries `coverage_status`, a `message`, a `checked_at` -- and NULL motion fields, never zeros. The call still meters.
POST /v1/migration-import Import a company list from your current vendor
Migration import from an incumbent: a switcher's uploaded Coresignal/Apollo company list, resolved to the panel and enriched with the hiring-motion column; every miss is routed to a real coverage request, so switching cost drops to near zero without a second "please crawl this" mechanism. Not billable -- same convention as `/quickstart` and `/onboarding`.

Webhook signature verification

Algorithm
HMAC-SHA256
Signature header
X-Plane-Signature
Idempotency header
X-Plane-Idempotency-Key

Recompute hmac_sha256(your_webhook_secret, raw_request_body) and compare it (constant-time) to the X-Plane-Signature header value. The X-Plane-Idempotency-Key header is stable across retries of the same delivery -- dedupe on it, don't process the same value twice.

Field dictionary

Field Type Description
account_hard_cap integer Account Hard Cap
account_id integer Account Id
account_soft_cap integer Account Soft Cap
account_status string Account Status
account_used integer Account Used
active_reqs integer Active Reqs
as_of string As Of
attempt_count integer Attempt Count
attributed_14d integer Attributed 14D
blocked_deliveries
board string Board
boards array Boards
bucket string Bucket
bucket_start string Start of the bucket (weekly by default), UTC.
by_function object By Function
by_meter_class object By Meter Class
calls integer Calls
cap_blocked integer Cap Blocked
changes integer Changes
changes_allowance integer Changes Allowance
changes_used integer Changes Used
changes_wall_reached boolean Changes Wall Reached
checked_at string Checked At
companies array Companies
company_domain Company Domain
company_id integer Company Id
company_name Company Name
contact_email string Contact Email
country Country
country_code The posting country's ISO-3166-1 alpha-2 code, uppercase (`DE`, `US`), derived from `country` and never stored. `null` when `country` is absent or is not a country this corpus names — never a guess. Note this is an output only: the `geo`/`country` filter still takes the name, because a bare two-letter code is ambiguous on input (`CA` is Canada to ISO and California to this column).
coverage number Coverage
coverage_status string Coverage Status
covered integer Covered
created_at string Created At
criteria Criteria
ctx object Context
cursor Cursor
customer_id string Customer Id
delivered_at Delivered At
detail array Detail
direction Direction
domain Domain
enrichment object Enrichment
enrichment_as_of Enrichment As Of
event_seq integer Event Seq
event_type string Event Type
event_types array Event Types
events array Events
exclude_agencies boolean Exclude Agencies
expansion_nudge_triggered boolean Expansion Nudge Triggered
fires_last_30d integer Fires Last 30D
fires_last_7d integer Fires Last 7D
fires_last_hour integer Fires Last Hour
first_seen string First Seen
first_webhook_delivered boolean First Webhook Delivered
forced_fresh integer Forced Fresh
free_tier
from_vendor string From Vendor
function Function
function_name The job function's human-readable name (`Software & IT`, `Healthcare`), derived from `function` and never stored. `function` itself stays the opaque taxonomy id because that is what the `?function=` filter accepts. `null` when the req carries no function attribution (`unspecified`), or when its id is outside the named title-family vocabulary — never a guess and never the raw id. Named on 60.3% of served reqs (5,126 of 8,497, measured 2026-08-24).
hq_country Hq Country
hq_country_code The company's HQ country as an ISO-3166-1 alpha-2 code, uppercase (`DE`, `US`), derived from `hq_country` and never stored. `null` when `hq_country` is absent or is not a country this corpus names — never a guess.
id integer Id
idempotency_key string Idempotency Key
input Input
is_hiring Is Hiring
is_surge Is Surge
items array Items
jobs array Jobs
key string Key
key_id integer Key Id
keys array Keys
label_count integer Label Count
last_fired_at Last Fired At
last_used_at Last Used At
limit integer Limit
live_only boolean Live Only
loc array Location
match_confidence number Match Confidence
matched_reqs array Matched Reqs
max_fires_per_hour integer Max Fires Per Hour
message Message
migrations array Migrations
misses array Misses
momentum array Posting history bucketed over time (weekly), oldest bucket first. A flow, over the company's whole publication history — not a series of `open_req_count` snapshots.
motion_as_of Motion As Of
msg string Message
name Name
negative_count integer Negative Count
new_roles_30d New Roles 30D
next_cursor integer Next Cursor
observed_at string Observed At
occurred_at
open_req_count Open Req Count
outcome string Outcome
paused boolean Paused
plan Plan
positive_count integer Positive Count
pulse
quota integer Quota
rate_limited boolean Rate Limited
raw_title The posting title exactly as the board published it, and the text `?role=` full-text matches. Populated on 100% of served reqs (8,497 of 8,497, measured 2026-08-24), so this is the field to render when `title` is null.
reason string Reason
referral_credit_units integer Referral Credit Units
relevance Relevance
repost_count
req_key Req Key
reqs array Reqs
resolved array Resolved
response_ms Response Ms
response_status Response Status
revoked_at Revoked At
role
rows array Rows
salary_max_usd Salary Max Usd
salary_min_usd Salary Min Usd
scopes Scopes
score Score
secret Secret
series array Series
since string Since
sla_credit_units integer Sla Credit Units
snippets object Snippets
source_board Source Board
status string Status
step string Step
tier string Tier
title Title
to_vendor string To Vendor
total integer Total
type string Error Type
upgrade_prompt
upgrade_url string Upgrade Url
url string Url
used integer Used
value Value
velocity Velocity
wall_blocked integer Wall Blocked
watch_id integer Watch Id
watches integer Watches
watches_allowance integer Watches Allowance
watches_used integer Watches Used
webhook_endpoint_id integer Webhook Endpoint Id
webhook_url string Webhook Url

Changelog

Every schema change is recorded automatically the moment the API's contract moves — this log is generated, never edited by hand.