Jobs Data API
Search normalized job listings, explore company hiring, and read observed changes with a scoped customer key. Start with a small query, then inspect the response and credit usage.
Updated September 8, 2026
Quickstart
- Create an account and verify your email. New verified organizations receive 2,500 onboarding credits for 14 days.
- In API keys, create a key with
jobs:read. Copy it while it is shown and store it in a server environment variable namedEXENDE_API_KEY. - Run this request. Five returned records cost five credits; an empty result costs zero.
bash
Search for up to five jobs
curl --get "https://data.exende.dev/v1/jobs/search" \
--data-urlencode "q=software engineer" \
--data-urlencode "limit=5" \
-H "Authorization: Bearer $EXENDE_API_KEY"The response is { data: Job[], nextCursor: string | null }. Each job has a canonical ID, company, title, available location and normalized fields, source URL, and observation timestamps. See the complete response below. Your request appears in Usage after it is recorded.
Authentication & scopes
Base URL: https://data.exende.dev. Send Authorization: Bearer $EXENDE_API_KEY. Keys belong to your organization, are shown once, and can be revoked in the dashboard. Keep them out of browser code, URLs, and logs.
| Scope | Access |
|---|---|
jobs:read | Jobs, search, job history, history feed, and skills. |
companies:read | Company detail, company jobs, and company hiring. |
signals:read | Overview and hiring metrics. |
Only the documented customer routes are supported. Jobs Data keys and credits are separate from the x402 infrastructure payment model.
Search jobs
GET /v1/jobs/search
q is required. Search uses bounded, safely quoted FTS prefix terms. Keyset pagination: default 50, max 100. Ordered by last_seen_at DESC, id DESC. Pass nextCursor as cursor with unchanged filters. Concurrent catalogue updates mean this is not a frozen snapshot. Credit cost: 1 per returned record. Required scope: jobs:read.
bash
Example request
curl "https://data.exende.dev/v1/jobs/search?q=software%20engineer&limit=5" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
limit | integer / query | Page size. Missing or invalid numeric values fall back to 50; the response never exceeds 100 records. Maximum: 100. Minimum: 1. |
q (required) | string / query | Trimmed FTS prefix query across title, department and description. Terms are combined with AND; at least one term must have two letters or numbers. Not a company or city search. Maximum length: 160. |
companyId | string / query | Exact canonical company ID. Format: uuid. |
domain | string / query | Company domain normalized by the API. Maximum length: 253. |
location | string / query | Case-insensitive source location substring; SQL LIKE % and _ wildcards retain their current meaning. Maximum length: 100. |
country | string / query | ISO alpha-2 country, case-insensitive; normalized to uppercase. |
title | string / query | Case-insensitive job-title substring; SQL LIKE wildcards retain their current meaning. Maximum length: 100. |
skill | string / query | Normalized skill slug or recognized alias. Unknown skill returns an empty page. Maximum length: 100. |
employmentType | string / query | Normalized employment type. Values: full_time, part_time, contract, temporary, internship, unknown. |
workplaceType | string / query | Normalized workplace type. Values: remote, hybrid, on_site, unknown. |
status | string / query | Current canonical job status. Values: active, closed. |
firstSeenAfter | string / query | Inclusive lower bound on first_seen_at. Format: date-time. |
publishedAfter | string / query | Inclusive lower bound on the source posted_at timestamp; missing publication dates do not match. Format: date-time. |
cursor | string / query | Opaque nextCursor from the previous response. Preserve all other filters. Maximum length: 500. |
200 response: Successful JSON response. Returns data: [] when there are no matching records.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | Job[] | See response schema. |
data[].id | string | Canonical source-scoped job identifier. |
data[].company_id | string | Canonical company identifier. |
data[].company | string | Canonical company name. |
data[].canonical_domain | string | null | Canonical company domain; not an ATS hostname. |
data[].title | string | Source job title. |
data[].location | string | null | Source location text; not a normalized city identifier. |
data[].country_code | string | null | ISO alpha-2 country only when explicit evidence supports normalization; otherwise null. |
data[].department | string | null | Department if provided. |
data[].employment_type | string | See response schema. |
data[].workplace_type | string | See response schema. |
data[].status | string | See response schema. |
data[].source_url | string | Public listing URL retained for attribution. |
data[].first_seen_at | string | First Exende observation, not necessarily publication. |
data[].last_seen_at | string | Most recent canonical observation; unchanged visibility is capped at once per UTC day. |
nextCursor | string | null | Opaque keyset cursor, or null when there is no further page. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"company_id": "00000000-0000-4000-8000-000000000000",
"company": "Company name",
"canonical_domain": null,
"title": "Job title from the source",
"location": null,
"country_code": null,
"department": null,
"employment_type": "full_time",
"workplace_type": "remote",
"status": "active",
"source_url": "https://company.example/careers/job",
"first_seen_at": "2000-01-01T00:00:00.000Z",
"last_seen_at": "2000-01-01T00:00:00.000Z"
}
],
"nextCursor": null
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
List jobs
GET /v1/jobs
Filter current normalized job records. Keyset pagination: default 50, max 100. Ordered by last_seen_at DESC, id DESC. Pass nextCursor as cursor with unchanged filters. Concurrent catalogue updates mean this is not a frozen snapshot. Credit cost: 1 per returned record. Required scope: jobs:read.
bash
Example request
curl "https://data.exende.dev/v1/jobs?limit=5" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
limit | integer / query | Page size. Missing or invalid numeric values fall back to 50; the response never exceeds 100 records. Maximum: 100. Minimum: 1. |
q | string / query | Trimmed FTS prefix query across title, department and description. Terms are combined with AND; at least one term must have two letters or numbers. Not a company or city search. Maximum length: 160. |
companyId | string / query | Exact canonical company ID. Format: uuid. |
domain | string / query | Company domain normalized by the API. Maximum length: 253. |
location | string / query | Case-insensitive source location substring; SQL LIKE % and _ wildcards retain their current meaning. Maximum length: 100. |
country | string / query | ISO alpha-2 country, case-insensitive; normalized to uppercase. |
title | string / query | Case-insensitive job-title substring; SQL LIKE wildcards retain their current meaning. Maximum length: 100. |
skill | string / query | Normalized skill slug or recognized alias. Unknown skill returns an empty page. Maximum length: 100. |
employmentType | string / query | Normalized employment type. Values: full_time, part_time, contract, temporary, internship, unknown. |
workplaceType | string / query | Normalized workplace type. Values: remote, hybrid, on_site, unknown. |
status | string / query | Current canonical job status. Values: active, closed. |
firstSeenAfter | string / query | Inclusive lower bound on first_seen_at. Format: date-time. |
publishedAfter | string / query | Inclusive lower bound on the source posted_at timestamp; missing publication dates do not match. Format: date-time. |
cursor | string / query | Opaque nextCursor from the previous response. Preserve all other filters. Maximum length: 500. |
200 response: Successful JSON response. Returns data: [] when there are no matching records.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | Job[] | See response schema. |
data[].id | string | Canonical source-scoped job identifier. |
data[].company_id | string | Canonical company identifier. |
data[].company | string | Canonical company name. |
data[].canonical_domain | string | null | Canonical company domain; not an ATS hostname. |
data[].title | string | Source job title. |
data[].location | string | null | Source location text; not a normalized city identifier. |
data[].country_code | string | null | ISO alpha-2 country only when explicit evidence supports normalization; otherwise null. |
data[].department | string | null | Department if provided. |
data[].employment_type | string | See response schema. |
data[].workplace_type | string | See response schema. |
data[].status | string | See response schema. |
data[].source_url | string | Public listing URL retained for attribution. |
data[].first_seen_at | string | First Exende observation, not necessarily publication. |
data[].last_seen_at | string | Most recent canonical observation; unchanged visibility is capped at once per UTC day. |
nextCursor | string | null | Opaque keyset cursor, or null when there is no further page. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"company_id": "00000000-0000-4000-8000-000000000000",
"company": "Company name",
"canonical_domain": null,
"title": "Job title from the source",
"location": null,
"country_code": null,
"department": null,
"employment_type": "full_time",
"workplace_type": "remote",
"status": "active",
"source_url": "https://company.example/careers/job",
"first_seen_at": "2000-01-01T00:00:00.000Z",
"last_seen_at": "2000-01-01T00:00:00.000Z"
}
],
"nextCursor": null
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Job detail
GET /v1/jobs/{id}
Retrieve one canonical job and its normalized skills. Not paginated. Credit cost: 1. Required scope: jobs:read.
bash
Set JOB_ID or COMPANY_DOMAIN from a returned record
curl "https://data.exende.dev/v1/jobs/$JOB_ID" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
id (required) | string / path | Canonical UUID from a job record. |
200 response: Successful JSON response. Unknown job returns 404.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | JobDetail | See response schema. |
data.id | string | Canonical source-scoped job identifier. |
data.company_id | string | Canonical company identifier. |
data.company | string | Canonical company name. |
data.canonical_domain | string | null | Canonical company domain; not an ATS hostname. |
data.title | string | Source job title. |
data.location | string | null | Source location text; not a normalized city identifier. |
data.country_code | string | null | ISO alpha-2 country only when explicit evidence supports normalization; otherwise null. |
data.department | string | null | Department if provided. |
data.employment_type | string | See response schema. |
data.workplace_type | string | See response schema. |
data.status | string | See response schema. |
data.source_url | string | Public listing URL retained for attribution. |
data.first_seen_at | string | First Exende observation, not necessarily publication. |
data.last_seen_at | string | Most recent canonical observation; unchanged visibility is capped at once per UTC day. |
data.website_url | string | null | See response schema. |
data.description_text | string | null | Available normalized description text; null when unavailable. |
data.posted_at | string | null | Source publication date when available. |
data.closed_at | string | null | Recorded closure time, if closed. |
data.skills | Skill[] | See response schema. |
data.skills[].slug | string | See response schema. |
data.skills[].canonical_name | string | See response schema. |
data.skills[].category | string | See response schema. |
data.description_enrichment_status | string | null | Legacy supplementary description-processing status. Do not use it as source freshness or a stable customer workflow signal. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"company_id": "00000000-0000-4000-8000-000000000000",
"company": "Company name",
"canonical_domain": null,
"title": "Job title from the source",
"location": null,
"country_code": null,
"department": null,
"employment_type": "full_time",
"workplace_type": "remote",
"status": "active",
"source_url": "https://company.example/careers/job",
"first_seen_at": "2000-01-01T00:00:00.000Z",
"last_seen_at": "2000-01-01T00:00:00.000Z",
"website_url": null,
"description_text": null,
"posted_at": null,
"closed_at": null,
"skills": [
{
"slug": "skill-slug",
"canonical_name": "Canonical name",
"category": "category"
}
],
"description_enrichment_status": null
}
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Job history
GET /v1/jobs/{id}/history
Newest observed versions first, ordered by observed_at DESC, id DESC. Historical snapshot fields use camelCase. Not paginated. Credit cost: 1 per returned record. Required scope: jobs:read.
bash
Set JOB_ID or COMPANY_DOMAIN from a returned record
curl "https://data.exende.dev/v1/jobs/$JOB_ID/history?limit=5" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
id (required) | string / path | Canonical job UUID. |
limit | integer / query | Page size. Missing or invalid numeric values fall back to 50; the response never exceeds 100 records. Maximum: 100. Minimum: 1. |
200 response: Successful JSON response. An unknown job or a job without versions returns data: [].
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | JobVersion[] | See response schema. |
data[].id | string | Version ID; not guaranteed to be a UUID. |
data[].observedAt | string | See response schema. |
data[].changeType | string | See response schema. |
data[].contentHash | string | See response schema. |
data[].snapshot | JobSnapshot | null | See response schema. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": [
{
"id": "00000000000000000000000000000000",
"observedAt": "2000-01-01T00:00:00.000Z",
"changeType": "created",
"contentHash": "0000000000000000000000000000000000000000000000000000000000000000",
"snapshot": null
}
]
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
History feed
GET /v1/history
Latest 100 immutable version events, ordered by observed_at DESC, id DESC. Query limit and cursor are not supported. Not paginated. Credit cost: 1 per returned record. Required scope: jobs:read.
bash
Example request
curl "https://data.exende.dev/v1/history" -H "Authorization: Bearer $EXENDE_API_KEY"No query parameters.
200 response: Successful JSON response. Returns data: [] when there are no matching records.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | HistoryEvent[] | See response schema. |
data[].id | string | See response schema. |
data[].observed_at | string | See response schema. |
data[].change_type | string | See response schema. |
data[].job_id | string | See response schema. |
data[].title | string | See response schema. |
data[].company | string | See response schema. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": [
{
"id": "00000000000000000000000000000000",
"observed_at": "2000-01-01T00:00:00.000Z",
"change_type": "created",
"job_id": "00000000-0000-4000-8000-000000000000",
"title": "Job title from the source",
"company": "Company name"
}
]
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Company detail
GET /v1/companies/{domain}
Canonical identity with current active count and observation bounds. Not paginated. Credit cost: 2. Required scope: companies:read.
bash
Set JOB_ID or COMPANY_DOMAIN from a returned record
curl "https://data.exende.dev/v1/companies/$COMPANY_DOMAIN" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
domain (required) | string / path | Canonical company domain, for example a domain returned on a job record. |
200 response: Successful JSON response. A known company with no active jobs returns active_jobs: 0. Unknown company returns 404.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | Company | See response schema. |
data.id | string | See response schema. |
data.canonical_name | string | See response schema. |
data.canonical_domain | string | null | See response schema. |
data.website_url | string | null | See response schema. |
data.active_jobs | integer | See response schema. |
data.first_seen_at | string | null | Earliest job observation for the company. |
data.last_seen_at | string | null | Latest job observation for the company. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"id": "00000000-0000-4000-8000-000000000000",
"canonical_name": "Canonical name",
"canonical_domain": null,
"website_url": null,
"active_jobs": 0,
"first_seen_at": null,
"last_seen_at": null
}
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Company jobs
GET /v1/companies/{domain}/jobs
Filters are combined with the company in the path. Keyset pagination: default 50, max 100. Ordered by last_seen_at DESC, id DESC. Pass nextCursor as cursor with unchanged filters. Concurrent catalogue updates mean this is not a frozen snapshot. Credit cost: 1 per returned record. Required scope: companies:read.
bash
Set JOB_ID or COMPANY_DOMAIN from a returned record
curl "https://data.exende.dev/v1/companies/$COMPANY_DOMAIN/jobs?limit=5" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
domain (required) | string / path | Canonical company domain or company UUID. |
limit | integer / query | Page size. Missing or invalid numeric values fall back to 50; the response never exceeds 100 records. Maximum: 100. Minimum: 1. |
q | string / query | Trimmed FTS prefix query across title, department and description. Terms are combined with AND; at least one term must have two letters or numbers. Not a company or city search. Maximum length: 160. |
companyId | string / query | Exact canonical company ID. Format: uuid. |
domain | string / query | Company domain normalized by the API. Maximum length: 253. |
location | string / query | Case-insensitive source location substring; SQL LIKE % and _ wildcards retain their current meaning. Maximum length: 100. |
country | string / query | ISO alpha-2 country, case-insensitive; normalized to uppercase. |
title | string / query | Case-insensitive job-title substring; SQL LIKE wildcards retain their current meaning. Maximum length: 100. |
skill | string / query | Normalized skill slug or recognized alias. Unknown skill returns an empty page. Maximum length: 100. |
employmentType | string / query | Normalized employment type. Values: full_time, part_time, contract, temporary, internship, unknown. |
workplaceType | string / query | Normalized workplace type. Values: remote, hybrid, on_site, unknown. |
status | string / query | Current canonical job status. Values: active, closed. |
firstSeenAfter | string / query | Inclusive lower bound on first_seen_at. Format: date-time. |
publishedAfter | string / query | Inclusive lower bound on the source posted_at timestamp; missing publication dates do not match. Format: date-time. |
cursor | string / query | Opaque nextCursor from the previous response. Preserve all other filters. Maximum length: 500. |
200 response: Successful JSON response. A valid but unmatched company domain or filter returns data: [], nextCursor: null.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | Job[] | See response schema. |
data[].id | string | Canonical source-scoped job identifier. |
data[].company_id | string | Canonical company identifier. |
data[].company | string | Canonical company name. |
data[].canonical_domain | string | null | Canonical company domain; not an ATS hostname. |
data[].title | string | Source job title. |
data[].location | string | null | Source location text; not a normalized city identifier. |
data[].country_code | string | null | ISO alpha-2 country only when explicit evidence supports normalization; otherwise null. |
data[].department | string | null | Department if provided. |
data[].employment_type | string | See response schema. |
data[].workplace_type | string | See response schema. |
data[].status | string | See response schema. |
data[].source_url | string | Public listing URL retained for attribution. |
data[].first_seen_at | string | First Exende observation, not necessarily publication. |
data[].last_seen_at | string | Most recent canonical observation; unchanged visibility is capped at once per UTC day. |
nextCursor | string | null | Opaque keyset cursor, or null when there is no further page. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000000",
"company_id": "00000000-0000-4000-8000-000000000000",
"company": "Company name",
"canonical_domain": null,
"title": "Job title from the source",
"location": null,
"country_code": null,
"department": null,
"employment_type": "full_time",
"workplace_type": "remote",
"status": "active",
"source_url": "https://company.example/careers/job",
"first_seen_at": "2000-01-01T00:00:00.000Z",
"last_seen_at": "2000-01-01T00:00:00.000Z"
}
],
"nextCursor": null
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Company hiring
GET /v1/companies/{domain}/hiring
Deterministic role mix, history readiness and a daily timeline. Current summary and historical events have distinct meanings. Not paginated. Credit cost: 5. Required scope: companies:read.
bash
Set JOB_ID or COMPANY_DOMAIN from a returned record
curl "https://data.exende.dev/v1/companies/$COMPANY_DOMAIN/hiring" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
domain (required) | string / path | Canonical company domain. |
days | integer / query | Requested rolling observation window. Missing or invalid numeric values fall back to 30. Maximum: 90. Minimum: 7. |
200 response: Successful JSON response. Unknown company returns 404. A known company can return zero counts and null acceleration.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | CompanyHiring | See response schema. |
data.company | CompanyIdentity | See response schema. |
data.company.id | string | See response schema. |
data.company.canonical_name | string | See response schema. |
data.company.canonical_domain | string | null | See response schema. |
data.company.website_url | string | null | See response schema. |
data.period_days | integer | See response schema. |
data.summary | CompanyHiringSummary | See response schema. |
data.summary.active_jobs | integer | See response schema. |
data.summary.created_jobs | integer | See response schema. |
data.summary.updated_jobs | integer | See response schema. |
data.summary.closed_jobs | integer | See response schema. |
data.summary.remote_jobs | integer | See response schema. |
data.summary.engineering_jobs | integer | See response schema. |
data.summary.sales_jobs | integer | See response schema. |
data.summary.ai_jobs | integer | See response schema. |
data.summary.countries_hiring | integer | See response schema. |
data.summary.country_classified_jobs | integer | See response schema. |
data.summary.country_unclassified_jobs | integer | See response schema. |
data.summary.hiring_velocity | number | Recorded created versions divided by period days. Initial ingestion can contribute. |
data.summary.hiring_acceleration | number | null | Recent creation rate minus preceding interval rate. Null until the history spans the requested window. |
data.summary.history_complete | boolean | The earliest immutable version reaches the requested window start. This is a readiness gate, not proof of uninterrupted or whole-market coverage. |
data.timeline | CompanyHiringDay[] | See response schema. |
data.timeline[].day | string | See response schema. |
data.timeline[].created_jobs | integer | See response schema. |
data.timeline[].updated_jobs | integer | See response schema. |
data.timeline[].closed_jobs | integer | See response schema. |
data.timeline[].active_jobs | integer | Active jobs reconstructed at the end of the UTC calendar day. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"company": {
"id": "00000000-0000-4000-8000-000000000000",
"canonical_name": "Canonical name",
"canonical_domain": null,
"website_url": null
},
"period_days": 7,
"summary": {
"active_jobs": 0,
"created_jobs": 0,
"updated_jobs": 0,
"closed_jobs": 0,
"remote_jobs": 0,
"engineering_jobs": 0,
"sales_jobs": 0,
"ai_jobs": 0,
"countries_hiring": 0,
"country_classified_jobs": 0,
"country_unclassified_jobs": 0,
"hiring_velocity": 0,
"hiring_acceleration": null,
"history_complete": false
},
"timeline": [
{
"day": "2000-01-01",
"created_jobs": 0,
"updated_jobs": 0,
"closed_jobs": 0,
"active_jobs": 0
}
]
}
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Skills
GET /v1/skills
Up to 100 skills with active jobs, ordered by active_jobs DESC, canonical_name ASC. No query parameters supported. Not paginated. Credit cost: 1 per returned record. Required scope: jobs:read.
bash
Example request
curl "https://data.exende.dev/v1/skills" -H "Authorization: Bearer $EXENDE_API_KEY"No query parameters.
200 response: Successful JSON response. Returns data: [] when there are no matching records.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | SkillCount[] | See response schema. |
data[].slug | string | See response schema. |
data[].canonical_name | string | See response schema. |
data[].category | string | See response schema. |
data[].active_jobs | integer | See response schema. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": [
{
"slug": "skill-slug",
"canonical_name": "Canonical name",
"category": "category",
"active_jobs": 0
}
]
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Overview metrics
GET /v1/metrics/overview
Active counts and daily visibility confirmations in the last 14 calendar days. The legacy failed_runs field is an aggregate, not a freshness guarantee. Not paginated. Credit cost: 2. Required scope: signals:read.
bash
Example request
curl "https://data.exende.dev/v1/metrics/overview" -H "Authorization: Bearer $EXENDE_API_KEY"No query parameters.
200 response: Successful JSON response. A successful aggregate object costs 2 credits even if all counts are zero.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | OverviewMetrics | See response schema. |
data.active_jobs | integer | See response schema. |
data.active_sources | integer | See response schema. |
data.failed_runs | integer | Legacy aggregate currently returned by this authenticated route; not a freshness measure. |
data.daily | object[] | See response schema. |
data.daily[].day | string | See response schema. |
data.daily[].observations | integer | See response schema. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"active_jobs": 0,
"active_sources": 0,
"failed_runs": 0,
"daily": [
{
"day": "2000-01-01",
"observations": 0
}
]
}
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Hiring metrics
GET /v1/metrics/hiring
Current role mix, version events, readiness-gated acceleration and top-12 groups. Timeline contains days with events; company-hiring timeline instead fills calendar days. Not paginated. Credit cost: 5. Required scope: signals:read.
bash
Example request
curl "https://data.exende.dev/v1/metrics/hiring" -H "Authorization: Bearer $EXENDE_API_KEY"| Parameter | Type / location | Meaning |
|---|---|---|
days | integer / query | Requested rolling observation window. Missing or invalid numeric values fall back to 30. Maximum: 90. Minimum: 7. |
200 response: Successful JSON response. A successful aggregate object costs 5 credits even if counts are zero or grouped arrays are empty.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | HiringMetrics | See response schema. |
data.period_days | integer | See response schema. |
data.summary | HiringSummary | See response schema. |
data.summary.active_jobs | integer | See response schema. |
data.summary.new_jobs | integer | See response schema. |
data.summary.updated_jobs | integer | See response schema. |
data.summary.closed_jobs | integer | See response schema. |
data.summary.remote_jobs | integer | See response schema. |
data.summary.engineering_jobs | integer | See response schema. |
data.summary.sales_jobs | integer | See response schema. |
data.summary.ai_jobs | integer | See response schema. |
data.summary.countries_hiring | integer | See response schema. |
data.summary.country_classified_jobs | integer | See response schema. |
data.summary.country_unclassified_jobs | integer | See response schema. |
data.summary.hiring_velocity | number | Recorded created versions divided by period days. Initial ingestion can contribute. |
data.summary.hiring_acceleration | number | null | Recent creation rate minus preceding interval rate. Null until the history spans the requested window. |
data.summary.history_complete | boolean | The earliest immutable version reaches the requested window start. This is a readiness gate, not proof of uninterrupted or whole-market coverage. |
data.timeline | HiringDay[] | See response schema. |
data.timeline[].day | string | See response schema. |
data.timeline[].created_jobs | integer | See response schema. |
data.timeline[].updated_jobs | integer | See response schema. |
data.timeline[].closed_jobs | integer | See response schema. |
data.companies | HiringCompany[] | See response schema. |
data.companies[].company_id | string | See response schema. |
data.companies[].company | string | See response schema. |
data.companies[].canonical_domain | string | null | See response schema. |
data.companies[].active_jobs | integer | See response schema. |
data.companies[].new_jobs | integer | See response schema. |
data.companies[].updated_jobs | integer | See response schema. |
data.companies[].closed_jobs | integer | See response schema. |
data.companies[].remote_jobs | integer | See response schema. |
data.companies[].engineering_jobs | integer | See response schema. |
data.companies[].sales_jobs | integer | See response schema. |
data.companies[].ai_jobs | integer | See response schema. |
data.companies[].countries_hiring | integer | See response schema. |
data.companies[].hiring_velocity | number | Recorded created versions divided by period days. Initial ingestion can contribute. |
data.companies[].hiring_acceleration | number | null | Recent creation rate minus preceding interval rate. Null until the history spans the requested window. |
data.companies[].history_complete | boolean | The earliest immutable version reaches the requested window start. This is a readiness gate, not proof of uninterrupted or whole-market coverage. |
data.companies[].new_jobs_per_day | number | Alias of hiring_velocity. |
data.locations | object[] | See response schema. |
data.locations[].location | string | See response schema. |
data.locations[].active_jobs | integer | See response schema. |
data.countries | object[] | See response schema. |
data.countries[].country_code | string | See response schema. |
data.countries[].active_jobs | integer | See response schema. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"period_days": 7,
"summary": {
"active_jobs": 0,
"new_jobs": 0,
"updated_jobs": 0,
"closed_jobs": 0,
"remote_jobs": 0,
"engineering_jobs": 0,
"sales_jobs": 0,
"ai_jobs": 0,
"countries_hiring": 0,
"country_classified_jobs": 0,
"country_unclassified_jobs": 0,
"hiring_velocity": 0,
"hiring_acceleration": null,
"history_complete": false
},
"timeline": [
{
"day": "2000-01-01",
"created_jobs": 0,
"updated_jobs": 0,
"closed_jobs": 0
}
],
"companies": [
{
"company_id": "00000000-0000-4000-8000-000000000000",
"company": "Company name",
"canonical_domain": null,
"active_jobs": 0,
"new_jobs": 0,
"updated_jobs": 0,
"closed_jobs": 0,
"remote_jobs": 0,
"engineering_jobs": 0,
"sales_jobs": 0,
"ai_jobs": 0,
"countries_hiring": 0,
"hiring_velocity": 0,
"hiring_acceleration": null,
"history_complete": false,
"new_jobs_per_day": 0
}
],
"locations": [
{
"location": "Source location text",
"active_jobs": 0
}
],
"countries": [
{
"country_code": "GB",
"active_jobs": 0
}
]
}
}Common errors: 400, 401, 402, 403, 404, 429, 500, 503. See Errors & rate limits below for handling.
Public overview
GET /v1/public/overview
Aggregate-only public proof. No key and no credits. No listing, customer, or source-health records. Last updated is the latest canonical job observation.
bash
Example request
curl "https://data.exende.dev/v1/public/overview"No query parameters.
200 response: Current aggregate overview.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | PublicOverview | See response schema. |
data.active_jobs | integer | See response schema. |
data.active_companies | integer | See response schema. |
data.active_sources | integer | See response schema. |
data.last_updated_at | string | null | Most recent canonical job observation; not the timestamp when every source was checked. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"active_jobs": 0,
"active_companies": 0,
"active_sources": 0,
"last_updated_at": null
}
}Common errors: 429, 503. See Errors & rate limits below for handling.
Public catalogue preview
GET /v1/public/search
Deterministic AND terms over public title, department, source location, company name/domain and normalized skills; exact country names and supported country codes filter country_code directly; skill aliases are recognized. Only q is accepted. No pagination or export. Sample membership is fixed by migration, with at most 12 enrolled jobs across all queries; responses contain at most 5. Full descriptions, source payloads, customer and operator data are excluded. Aggregates describe matching active canonical jobs, not just samples. Limits: public gateway 20 requests/minute per client IP and Data Plane 60/minute; Cloudflare limits are approximate.
bash
Example request
curl "https://data.exende.dev/v1/public/search"| Parameter | Type / location | Meaning |
|---|---|---|
q | string / query | Up to 80 characters and 6 terms; at least 2 characters when nonempty. Omit for catalogue-wide aggregates and the fixed sample. Control characters, wildcard-only queries and duplicate q are rejected. NFKC-normalized, case-insensitive. Maximum length: 80. |
200 response: Real matching aggregates and up to 5 enrolled samples. No matches returns zero counts and empty arrays. Matches can exist without any enrolled sample.
Response fields and full structural example
| Field | Type | Meaning |
|---|---|---|
data | PublicCataloguePreview | See response schema. |
data.query | string | See response schema. |
data.summary | object | See response schema. |
data.summary.active_jobs | integer | See response schema. |
data.summary.companies | integer | See response schema. |
data.summary.countries | integer | See response schema. |
data.summary.locations | integer | See response schema. |
data.summary.country_unclassified_jobs | integer | See response schema. |
data.summary.first_observed_at | string | null | See response schema. |
data.summary.last_observed_at | string | null | See response schema. |
data.top_locations | PublicCatalogueGroup[] | See response schema. |
data.top_locations[].value | string | See response schema. |
data.top_locations[].active_jobs | integer | See response schema. |
data.top_countries | PublicCatalogueGroup[] | See response schema. |
data.top_countries[].value | string | See response schema. |
data.top_countries[].active_jobs | integer | See response schema. |
data.top_roles | PublicCatalogueGroup[] | See response schema. |
data.top_roles[].value | string | See response schema. |
data.top_roles[].active_jobs | integer | See response schema. |
data.top_companies | PublicCatalogueGroup[] | See response schema. |
data.top_companies[].value | string | See response schema. |
data.top_companies[].active_jobs | integer | See response schema. |
data.sample_jobs | PublicJobSample[] | See response schema. |
data.sample_jobs[].id | string | Canonical job identifier. |
data.sample_jobs[].title | string | Public job title, capped at 180 characters. |
data.sample_jobs[].company | string | Canonical public company name. |
data.sample_jobs[].location | string | null | Source location label, not a normalized city. |
data.sample_jobs[].country_code | string | null | Explicit supported country evidence, or null. |
data.sample_jobs[].employment_type | string | Normalized public classification; unknown values remain unknown. |
data.sample_jobs[].workplace_type | string | Normalized public classification; unknown values remain unknown. |
data.sample_jobs[].status | string | Only currently active enrolled samples are shown. |
data.sample_jobs[].first_seen_at | string | null | First canonical observation, or null. |
data.sample_jobs[].last_seen_at | string | null | Latest canonical observation, or null. |
data.preview | object | See response schema. |
data.preview.sample_limit | integer | See response schema. |
data.preview.pool_limit | integer | See response schema. |
data.preview.enrolled_samples | integer | See response schema. |
data.preview.pagination | boolean | See response schema. |
data.preview.projection | string | See response schema. |
Structural example only. Placeholder names, IDs, dates, and counts illustrate the shape; these are not live catalogue records.
json
Structural example — not live data
{
"data": {
"query": "",
"summary": {
"active_jobs": 0,
"companies": 0,
"countries": 0,
"locations": 0,
"country_unclassified_jobs": 0,
"first_observed_at": null,
"last_observed_at": null
},
"top_locations": [],
"top_countries": [],
"top_roles": [],
"top_companies": [],
"sample_jobs": [],
"preview": {
"sample_limit": 5,
"pool_limit": 12,
"enrolled_samples": 0,
"pagination": false,
"projection": "limited_public_fields"
}
}
}Common errors: 400, 429, 503. See Errors & rate limits below for handling.
Pagination & request limits
Jobs list, search, and company jobs use keyset pagination. The default is 50 records and the maximum is 100. Keep filters unchanged and send the returned nextCursor as cursor. Stop when it is null. Records are ordered by last_seen_at DESC, id DESC; ongoing catalogue changes mean pages are not a frozen snapshot.
Job history accepts limit (default 50, max 100) with no cursor. Skills and the history feed return at most 100 records and do not accept pagination parameters. The hiring window is 7–90 days, default 30; grouped companies, locations, and countries are capped at 12.
The current backend falls back to 50 for an invalid numeric limit and 30 for invalid numeric days. Other invalid filters and malformed cursors return 400. Unknown query parameters on customer routes are ignored, so use the parameters documented for each operation. The strict public preview instead rejects every parameter other than q.
Credit metering
Credits are not requests. Per-record operations cost one credit per returned record: 5 results = 5 credits, 50 results = 50 credits. Empty lists and failed requests cost zero. Successful fixed-cost aggregate objects still incur their fixed cost when their numeric counts are zero.
| GET route | Scope | Credits |
|---|---|---|
/v1/jobs/search | jobs:read | 1 per returned record |
/v1/jobs | jobs:read | 1 per returned record |
/v1/jobs/{id} | jobs:read | 1 |
/v1/jobs/{id}/history | jobs:read | 1 per returned record |
/v1/history | jobs:read | 1 per returned record |
/v1/companies/{domain} | companies:read | 2 |
/v1/companies/{domain}/jobs | companies:read | 1 per returned record |
/v1/companies/{domain}/hiring | companies:read | 5 |
/v1/skills | jobs:read | 1 per returned record |
/v1/metrics/overview | signals:read | 2 |
/v1/metrics/hiring | signals:read | 5 |
The authoritative credit debit must succeed before customer data is returned. Insufficient balance returns 402 with insufficient_credits. This is a Jobs Data credit error, not an x402 payment challenge.
Errors & rate limits
json
Example insufficient-credit error
{
"error": {
"code": "insufficient_credits",
"message": "This account does not have enough credits for this operation."
}
}| HTTP | Code | Action |
|---|---|---|
| 400 | invalid_request | Check parameter values and cursor. Validation responses can include error.details. |
| 401 | unauthorized | Check the key, expiry, revocation, and organization status. |
| 402 | insufficient_credits | Use a smaller result limit or add a suitable plan. |
| 403 | insufficient_scope | Create a key with the route’s required scope. |
| 404 | not_found | Check the route or record identifier. Empty lists use 200 instead. |
| 429 | rate_limited | Respect Retry-After: 60. Apply backoff and avoid retry bursts. |
| 503 | service_unavailable | Retry with bounded backoff; a required service is unavailable. |
Production has a pre-authentication abuse limiter (240/minute per connection IP) and an authenticated limiter (120/minute per key). Cloudflare limits are location-local and eventually consistent. Shared-IP traffic can share a limit. Store the X-Request-ID response header for support. Unexpected upstream failures may return a non-JSON body; handle HTTP status before parsing.
Freshness, metrics & caching
Public overview cache: max-age=60, s-maxage=300, stale-while-revalidate=600. The latest observation timestamp does not mean every source was checked at that time. Authenticated metrics can reuse a server-side five-minute calculation cache; authentication and metering still occur on each request, and client responses use private, no-store.
Hiring velocity is recorded created versions divided by days. Acceleration compares recent and preceding creation rates and is null until the earliest immutable history spans the requested window. history_complete is that readiness gate; it does not prove uninterrupted or whole-market observation. Company timelines include zero-event days; the overall hiring timeline includes days with events. Country nulls remain unclassified. See Coverage & methodology.
