Jobs
Reading the calling account's published, closed and expired jobs. Drafts never appear here.
List the account's jobs
GET /v1/jobs
Published jobs, plus closed and expired ones with their status set, so a careers page can render its own closed state instead of breaking links it already published. Drafts are never returned.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | Comma-separated statuses to include. Defaults to all three. Example published,closed. |
updatedSince | query | string | ISO 8601 timestamp or epoch milliseconds. Only jobs changed at or after this moment. Example 2026-09-01T00:00:00Z. |
limit | query | integer | Default 50. |
cursor | query | string | The nextCursor of the previous page. |
Responses
200 — One page of jobs. Body: JobList.
Headers: X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
{
"jobs": [
{
"id": "job_01J8Z3K9Q0EXAMPLE",
"status": "published",
"title": "Senior Backend Engineer",
"category": "engineering",
"description": "Own the services behind our hiring pipeline, from the API surface to the queues that move applications through it.",
"responsibilities": [
"Design and ship services on AWS Lambda and DynamoDB",
"Review and mentor across the backend team"
],
"skills": [
{
"name": "TypeScript",
"level": "required"
},
{
"name": "DynamoDB",
"level": "nice_to_have"
}
],
"benefits": [
"Remote-first",
"Learning budget"
],
"seniority": "senior",
"yearsExperience": "5+",
"employmentType": "full_time",
"contractType": "permanent",
"salary": {
"min": 150000,
"max": 180000,
"currency": "AUD",
"period": "year"
},
"location": {
"address": "Level 3, 1 Market St, Sydney NSW 2000, Australia",
"country": "AU",
"state": "NSW",
"city": "Sydney",
"remotePolicy": "hybrid",
"onsiteDays": "2"
},
"workStyle": "hybrid",
"company": {
"name": "Acme Robotics",
"logoUrl": "https://content.wipperoz.com/accounts/acme/logo.png"
},
"postedAt": "2026-09-01T02:15:00.000Z",
"updatedAt": "2026-09-03T10:40:00.000Z",
"expiresAt": "2026-10-01T02:15:00.000Z",
"url": "https://www.wipperoz.com/en/jobs/acme-robotics/senior-backend-engineer",
"applyLink": "https://www.wipperoz.com/en/apply/job_01J8Z3K9Q0EXAMPLE?src=careers&account=acc_01J8Z3EXAMPLE"
}
],
"nextCursor": null
}400 — A query parameter is malformed. The message says which. Body: Error.
{
"error": {
"code": "bad_request",
"message": "`limit` must be an integer between 1 and 100",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}401 — No API key, or one that is not valid. Body: Error.
{
"error": {
"code": "unauthorized",
"message": "This endpoint requires an API key. Send it as `Authorization: Bearer <key>` or in `x-api-key`.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}403 — The key is valid but does not carry the scope this route needs. Body: Error.
{
"error": {
"code": "forbidden",
"message": "This key does not carry the `jobs:read` scope. Mint a key with it in Orbit → Settings → API keys.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}429 — Over the per-key limit. Wait Retry-After seconds. Body: Error.
Headers: Retry-After
{
"error": {
"code": "rate_limited",
"message": "Rate limit of 120 requests per minute exceeded. Retry after 37 seconds.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}Read one job
GET /v1/jobs/{jobId}
One of the calling account's jobs. Another account's job, a draft and an unknown id all answer 404 — a draft has no public existence yet.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
jobIdrequired | path | string | Example job_01J8Z3K9Q0EXAMPLE. |
Responses
200 — The job.
Headers: X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
{
"job": {
"id": "job_01J8Z3K9Q0EXAMPLE",
"status": "published",
"title": "Senior Backend Engineer",
"category": "engineering",
"description": "Own the services behind our hiring pipeline, from the API surface to the queues that move applications through it.",
"responsibilities": [
"Design and ship services on AWS Lambda and DynamoDB",
"Review and mentor across the backend team"
],
"skills": [
{
"name": "TypeScript",
"level": "required"
},
{
"name": "DynamoDB",
"level": "nice_to_have"
}
],
"benefits": [
"Remote-first",
"Learning budget"
],
"seniority": "senior",
"yearsExperience": "5+",
"employmentType": "full_time",
"contractType": "permanent",
"salary": {
"min": 150000,
"max": 180000,
"currency": "AUD",
"period": "year"
},
"location": {
"address": "Level 3, 1 Market St, Sydney NSW 2000, Australia",
"country": "AU",
"state": "NSW",
"city": "Sydney",
"remotePolicy": "hybrid",
"onsiteDays": "2"
},
"workStyle": "hybrid",
"company": {
"name": "Acme Robotics",
"logoUrl": "https://content.wipperoz.com/accounts/acme/logo.png"
},
"postedAt": "2026-09-01T02:15:00.000Z",
"updatedAt": "2026-09-03T10:40:00.000Z",
"expiresAt": "2026-10-01T02:15:00.000Z",
"url": "https://www.wipperoz.com/en/jobs/acme-robotics/senior-backend-engineer",
"applyLink": "https://www.wipperoz.com/en/apply/job_01J8Z3K9Q0EXAMPLE?src=careers&account=acc_01J8Z3EXAMPLE"
}
}401 — No API key, or one that is not valid. Body: Error.
{
"error": {
"code": "unauthorized",
"message": "This endpoint requires an API key. Send it as `Authorization: Bearer <key>` or in `x-api-key`.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}403 — The key is valid but does not carry the scope this route needs. Body: Error.
{
"error": {
"code": "forbidden",
"message": "This key does not carry the `jobs:read` scope. Mint a key with it in Orbit → Settings → API keys.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}404 — No such job on this account. Body: Error.
{
"error": {
"code": "not_found",
"message": "No such job on this account",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}429 — Over the per-key limit. Wait Retry-After seconds. Body: Error.
Headers: Retry-After
{
"error": {
"code": "rate_limited",
"message": "Rate limit of 120 requests per minute exceeded. Retry after 37 seconds.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}Schemas
Error
| Field | Type | Description |
|---|---|---|
errorrequired | object |
ScreeningRequest
| Field | Type | Description |
|---|---|---|
templateIdrequired | string | One of the account's active screening templates. |
completeBy | string (date-time) | The deadline. Defaults to seven days from now. |
message | string | A note the candidate reads in the invitation. |
language | string | The language of the invitation email, when the candidate is reached by email. One of: en, es, fr, pt. Default en. |
ScreeningTriggered
| Field | Type | Description |
|---|---|---|
screeningrequired | object | |
billingrequired | string | included — one of the managed ad's included screenings; overage — billed at priceCents if it completes. One of: included, overage. |
priceCentsrequired | integer | The template's cost per screening, locked now, in minor units, excluding GST. |
currencyrequired | string | One of: AUD, USD. |
JobList
| Field | Type | Description |
|---|---|---|
jobsrequired | Job[] | |
nextCursorrequired | string | null | Pass back as cursor for the next page. null when there is no more. |
Skill
| Field | Type | Description |
|---|---|---|
namerequired | string | |
levelrequired | string | One of: required, nice_to_have. |
Salary
| Field | Type | Description |
|---|---|---|
min | number | |
max | number | |
currency | string | ISO 4217, as entered by the recruiter. |
period | string | Free text as entered, typically year, month, day or hour. |
Location
Where the role is. address is the place in the recruiter's words — a picked suggestion's full address, or text typed by hand — and it is what the Wipperoz job page shows first. city, state and country are set only when the place was picked from a suggestion (or sent by you), so a location typed by hand is address alone: show address when it is there, and build structured data (jobLocation) from the parts. A job needs a country to be published for the first time.
| Field | Type | Description |
|---|---|---|
address | string | The place as written. At most 300 characters when you send it. |
country | string | Required to publish a job for the first time. For a remote role, the country candidates can be in. |
state | string | |
city | string | |
remotePolicy | string | One of: onsite, hybrid, remote. |
onsiteDays | string |
Company
| Field | Type | Description |
|---|---|---|
name | string | |
logoUrl | string (uri) |
Job
Enough to build a JobPosting JSON-LD block: title, description, postedAt → datePosted, expiresAt → validThrough, company → hiringOrganization, location → jobLocation, salary → baseSalary, employmentType.
| Field | Type | Description |
|---|---|---|
idrequired | string | |
statusrequired | string | published accepts applications. The other two are returned, not omitted, so a careers page can render its own closed state. One of: published, closed, expired. |
titlerequired | string | |
category | string | What kind of job this is. Absent on a role published before categories existed. One of: engineering, data, product, design, marketing, sales, customer_success, operations, finance, hr_people, legal, healthcare, education, hospitality, retail, logistics, construction_trades, manufacturing, admin_support, other. |
descriptionrequired | string | |
responsibilitiesrequired | string[] | |
skillsrequired | Skill[] | |
benefitsrequired | string[] | |
seniority | string | |
yearsExperience | string | |
employmentType | string | One of: full_time, part_time, contract, temporary, other, volunteer, internship. |
contractType | string | One of: permanent, part_time, fixed_term, contract, casual. |
salary | Salary | |
location | Location | Where the role is. address is the place in the recruiter's words —
a picked suggestion's full address, or text typed by hand — and it is
what the Wipperoz job page shows first. city, state and country
are set only when the place was picked from a suggestion (or sent by
you), so a location typed by hand is address alone: show address
when it is there, and build structured data (jobLocation) from the
parts.
A job needs a country to be published for the first time.
|
workStyle | string | |
companyrequired | Company | |
postedAt | string (date-time) | |
updatedAt | string (date-time) | The latest of an edit, the publish and the close. What updatedSince compares against. |
expiresAt | string (date-time) | The role stops accepting applications at this moment. Use as validThrough. |
closedAt | string (date-time) | |
url | string (uri) | The Wipperoz-hosted job page, when the role has one. |
applyLinkrequired | string (uri) | The hosted apply page. Link candidates here; it records that they came from your site. |
JobWrite
What you may set on a job. Every field is optional on PATCH; title is required on POST. The vocabulary is the one the reads return, so an enum you already map for rendering is the enum you send. What you may **not** set: status, id, slug, url, applyLink, company, postedAt, updatedAt and closedAt are ours — derived, permanent, or a lifecycle transition with its own endpoint. Sending one is a 400, deliberately, rather than being ignored: a field you thought you set and we silently dropped is worse than an error.
| Field | Type | Description |
|---|---|---|
title | string | |
category | string | What kind of job this is, from a fixed list. Required before a role's first publish. Locked, with the title, once the ad is managed in Orbit. One of: engineering, data, product, design, marketing, sales, customer_success, operations, finance, hr_people, legal, healthcare, education, hospitality, retail, logistics, construction_trades, manufacturing, admin_support, other. |
description | string | Rich text HTML, as the Orbit editor produces. Plain text is fine. |
responsibilities | string[] | |
skills | Skill[] | |
benefits | string[] | |
seniority | string | |
yearsExperience | string | |
employmentType | string | One of: full_time, part_time, contract, temporary, other, volunteer, internship. |
contractType | string | One of: permanent, part_time, fixed_term, contract, casual. |
salary | Salary | |
location | Location | Where the role is. address is the place in the recruiter's words —
a picked suggestion's full address, or text typed by hand — and it is
what the Wipperoz job page shows first. city, state and country
are set only when the place was picked from a suggestion (or sent by
you), so a location typed by hand is address alone: show address
when it is there, and build structured data (jobLocation) from the
parts.
A job needs a country to be published for the first time.
|
workStyle | string | |
expiresAt | string | null (date-time) | When the role stops accepting applications. null clears it — live until closed by hand. Applies immediately, even on a published ad. |
maxMatches | integer | null | Stop generating AI matches once this many exist. null removes the cap. Applies immediately, even on a published ad. |
AuthoredJob
The job as its own account sees it. Identical to Job, except that status may also be draft — the state a job is created in, which the read endpoints never return. A draft already carries an applyLink. It does not resolve until the role is published; it is there so a careers page can be built against the record before it goes live.
| Field | Type | Description |
|---|---|---|
idrequired | string | |
statusrequired | string | One of: draft, published, closed, expired. |
titlerequired | string | |
category | string | What kind of job this is. Absent on a role published before categories existed. One of: engineering, data, product, design, marketing, sales, customer_success, operations, finance, hr_people, legal, healthcare, education, hospitality, retail, logistics, construction_trades, manufacturing, admin_support, other. |
descriptionrequired | string | |
responsibilitiesrequired | string[] | |
skillsrequired | Skill[] | |
benefitsrequired | string[] | |
seniority | string | |
yearsExperience | string | |
employmentType | string | One of: full_time, part_time, contract, temporary, other, volunteer, internship. |
contractType | string | One of: permanent, part_time, fixed_term, contract, casual. |
salary | Salary | |
location | Location | Where the role is. address is the place in the recruiter's words —
a picked suggestion's full address, or text typed by hand — and it is
what the Wipperoz job page shows first. city, state and country
are set only when the place was picked from a suggestion (or sent by
you), so a location typed by hand is address alone: show address
when it is there, and build structured data (jobLocation) from the
parts.
A job needs a country to be published for the first time.
|
workStyle | string | |
companyrequired | Company | |
postedAt | string (date-time) | |
updatedAt | string (date-time) | The latest of an edit, the publish and the close. What updatedSince compares against. |
expiresAt | string (date-time) | The role stops accepting applications at this moment. Use as validThrough. |
closedAt | string (date-time) | |
url | string (uri) | The Wipperoz-hosted job page, when the role has one. |
applyLinkrequired | string (uri) | The hosted apply page. Link candidates here; it records that they came from your site. |
ConsentText
| Field | Type | Description |
|---|---|---|
consentTextIdrequired | string | |
versionrequired | string | Send this back in consent.version. Changes whenever the wording does. |
localerequired | string | One of: en, es, fr, pt. |
textrequired | string | Render this unmodified beside your apply button. |
UploadSlot
| Field | Type | Description |
|---|---|---|
uploadIdrequired | string | Name this in cv.uploadId within the hour. |
uploadUrlrequired | string | PUT the PDF here with Content-Type: application/pdf and Content-Length equal to bytes, before expiresAt. |
expiresAtrequired | string (date-time) |
ConsentReceipt
Your record of showing the candidate the consent text and recording their acceptance. Stored verbatim with the application, beside our own copy of the text in locale — what the candidate read.
| Field | Type | Description |
|---|---|---|
consentTextIdrequired | string | One of: orbit_api_apply. |
versionrequired | string | The version you rendered, from GET /v1/consent-text. Currently api_apply_v1. |
locale | string | The language you showed it in. The claim email goes out in it too. One of: en, es, fr, pt. Default en. |
acceptedAtrequired | string | integer | When the candidate accepted it — ISO 8601, or epoch milliseconds. Not more than five minutes ahead of our clock. |
iprequired | string | The candidate's address at the point of acceptance. |
userAgentrequired | string |
ApplicationWrite
| Field | Type | Description |
|---|---|---|
namerequired | string | |
emailrequired | string (email) | The candidate's own address. The claim link goes here, and one application per address per job is kept. |
phone | string | |
location | string | |
cvrequired | object | |
coverNote | string | |
answers | object[] | Your own screening questions and what the candidate answered, shown to the recruiter as typed. |
consentrequired | ConsentReceipt | Your record of showing the candidate the consent text and recording
their acceptance. Stored verbatim with the application, beside our
own copy of the text in locale — what the candidate read.
|
lawfulBasisrequired | object |
ApplicationCreated
| Field | Type | Description |
|---|---|---|
applicationIdrequired | string | |
candidateIdrequired | string | The id POST /v1/jobs/{jobId}/candidates/{candidateId}/screening takes for this person. The same value as applicationId. |
statusrequired | string | One of: received. |
cvStatusrequired | string | One of: processing. |
jobrequired | object |
ApplicationStatus
| Field | Type | Description |
|---|---|---|
applicationIdrequired | string | |
jobIdrequired | string | |
candidateIdrequired | string | |
statusrequired | string | withdrawn only when the PDF could not be read; reason says so. One of: received, withdrawn. |
cvStatusrequired | string | One of: processing, staged, claimed, deleted, failed. |
createdAtrequired | string (date-time) | |
claimedAt | string (date-time) | Once the candidate has claimed the Virtual CV. |
reason | string | On withdrawn. One of: cv_unreadable. |
CandidateWrite
| Field | Type | Description |
|---|---|---|
namerequired | string | |
emailrequired | string (email) | The person's own address. One import per address per account; the claim email goes here. |
cvrequired | object | |
sendClaimEmail | boolean | Whether to email the person that a Virtual CV was built for them, with a way to claim or delete it. A person who already has an account is told either way. Default true. |
locale | string | The claim email's language. One of: en, es, fr, pt. Default en. |
lawfulBasisrequired | object |
CandidateCreated
| Field | Type | Description |
|---|---|---|
candidateIdrequired | string | The id My candidates knows this person by. |
statusrequired | string | One of: queued, existing_account, suppressed, already_imported, rejected. |
cvStatusrequired | string | processing only when a Virtual CV is being built (queued). One of: processing, none. |
rejection | object | On rejected. |