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",
"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": {
"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",
"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": {
"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 |
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
| Field | Type | Description |
|---|---|---|
country | string | |
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 | |
descriptionrequired | string | |
responsibilitiesrequired | string[] | |
skillsrequired | Skill[] | |
benefitsrequired | string[] | |
seniority | string | |
yearsExperience | string | |
employmentType | string | One of: full_time, part_time. |
contractType | string | One of: permanent, part_time, fixed_term, contract, casual. |
salary | Salary | |
location | Location | |
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 | |
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. |
contractType | string | One of: permanent, part_time, fixed_term, contract, casual. |
salary | Salary | |
location | Location | |
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 | |
descriptionrequired | string | |
responsibilitiesrequired | string[] | |
skillsrequired | Skill[] | |
benefitsrequired | string[] | |
seniority | string | |
yearsExperience | string | |
employmentType | string | One of: full_time, part_time. |
contractType | string | One of: permanent, part_time, fixed_term, contract, casual. |
salary | Salary | |
location | Location | |
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. |