Writing jobs
Creating, editing, publishing and closing the account's advertisements. Needs the `jobs:write` scope.
Create a draft job
POST /v1/jobs
Creates a **draft**. It is not public, and it does not appear in GET /v1/jobs until it is published. The role lands in Orbit where a wizard-authored draft lands, so a recruiter can open, edit and publish it by hand.
title is required. Everything else is optional here and nudged at publish time — only a title, a description, a category and a location with a country gate publishing.
Send an Idempotency-Key so a retried request returns the first answer instead of creating a second advertisement.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | A value of your own that identifies this intent — a job id from your
own system, or a UUID. Retrying with the same key returns the first
answer instead of repeating the work, for 24 hours. The reply to a
replay carries Idempotent-Replay: true.
Example greenhouse-job-4471-publish. |
Request body
Required. Shape: JobWrite.
| 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. |
{
"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"
],
"skills": [
{
"name": "TypeScript",
"level": "required"
}
],
"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"
}
}Responses
201 — The draft, with the id you publish it by.
Headers: X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
{
"job": {
"id": "job_01J9A0B1C2EXAMPLE",
"status": "draft",
"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"
],
"skills": [
{
"name": "TypeScript",
"level": "required"
}
],
"benefits": [],
"employmentType": "full_time",
"contractType": "permanent",
"salary": {
"min": 150000,
"max": 180000,
"currency": "AUD",
"period": "year"
},
"location": {
"country": "AU",
"state": "NSW",
"city": "Sydney",
"remotePolicy": "hybrid"
},
"company": {
"name": "Acme Robotics"
},
"updatedAt": "2026-09-08T01:10:00.000Z",
"applyLink": "https://www.wipperoz.com/en/apply/job_01J9A0B1C2EXAMPLE?src=careers&account=acc_01J8Z3EXAMPLE"
}
}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"
}
}409 — The Idempotency-Key is in use — by a different request, or by one still running. Body: Error.
{
"error": {
"code": "conflict",
"message": "This `Idempotency-Key` was already used for a different request. Use a new key, or send the original request again.",
"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"
}
}Edit a job
PATCH /v1/jobs/{jobId}
Sets the fields you send and leaves the rest alone.
On a **draft** the change applies at once. On a **published** ad the words are staged rather than replacing what candidates are currently applying against, and the response carries pendingRevision: true until someone promotes them — a recruiter in Orbit, or this call with publishEdits: true.
expiresAt and maxMatches are exempt from staging and always apply immediately: both exist to stop something, and holding them behind a promotion would keep the thing running.
A **managed** ad's title and category are locked: an edit that changes either — directly, or through publishEdits promoting a staged one — answers 409 with managed_ad_locked and changes nothing. Re-sending the current value is not a change.
A live job keeps the country it was published with: publishEdits on a staged location with no country answers 422 not_publishable with missing: [location] and changes nothing.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
jobIdrequired | path | string | Example job_01J8Z3K9Q0EXAMPLE. |
Request body
Required. Shape: JobWrite.
| 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. |
publishEdits | boolean | Promote the staged words in this same call, so a live ad shows them immediately. Default false. |
{
"title": "Staff Backend Engineer",
"salary": {
"min": 170000,
"max": 200000,
"currency": "AUD",
"period": "year"
},
"publishEdits": true
}Responses
200 — The job as it now stands, and whether an edit is waiting to be promoted.
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"
},
"pendingRevision": false
}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"
}
}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"
}
}409 — The job is a managed ad, and the edit would change its title or category. fields names which. Nothing was written. Body: Error.
{
"error": {
"code": "managed_ad_locked",
"message": "This job is a managed ad, so its title cannot change. Post the new role as a new job.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"fields": [
"title"
]
}
}422 — The job is missing something publishing requires. missing names it. Body: Error.
{
"error": {
"code": "not_publishable",
"message": "This job cannot be published until it has a title, a description, a category and a location with a country.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"missing": [
"title",
"description",
"category",
"location"
]
}
}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"
}
}Publish a job
POST /v1/jobs/{jobId}/publish
Takes a draft live: it becomes visible on the hosted job page, its applyLink starts accepting candidates, and it enters the Wipperoz job seeker feed.
**Publishing is free.** Ranking applicants and matching the role against the candidate pool are what a *managed* ad does; managing an ad is done in Orbit. Re-publishing a role that has been live before keeps its address, and firstPublish says which happened. A closed or expired role that is published again comes back **unmanaged**; managing it again (in Orbit) takes a slot from that moment.
A job with no title, no description or — on its first publish — no category or no location.country answers 422 naming the fields that block it (missing). For a remote role, the country is the one candidates can be in. Everything else is optional; a role that has been live before re-publishes without a country.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
jobIdrequired | path | string | Example job_01J9A0B1C2EXAMPLE. |
Idempotency-Key | header | string | A value of your own that identifies this intent — a job id from your
own system, or a UUID. Retrying with the same key returns the first
answer instead of repeating the work, for 24 hours. The reply to a
replay carries Idempotent-Replay: true.
Example greenhouse-job-4471-publish. |
Request body
Optional.
| Field | Type | Description |
|---|---|---|
expiryDays | integer | null | When the role stops accepting applications. null — the default — leaves it live until it is closed by hand. One of: 30, 60, 90, null. Default null. |
{
"expiryDays": 30
}Responses
200 — The role is live.
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"
},
"firstPublish": true
}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"
}
}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"
}
}409 — The Idempotency-Key is in use — by a different request, or by one still running. Body: Error.
{
"error": {
"code": "conflict",
"message": "This `Idempotency-Key` was already used for a different request. Use a new key, or send the original request again.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b"
}
}422 — The job is missing something publishing requires. missing names it. Body: Error.
{
"error": {
"code": "not_publishable",
"message": "This job cannot be published until it has a title, a description, a category and a location with a country.",
"requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
"missing": [
"title",
"description",
"category",
"location"
]
}
}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"
}
}Close a job
POST /v1/jobs/{jobId}/close
Stops a live role. The hosted page stops accepting applications and says the role has closed, the role leaves the job seeker feed, and the CV access the role granted your recruiters ends with it — the reason for that access was an open application.
Applications already received stay, and stay readable in Orbit. Closing is not deleting; this API has no delete.
Only a published role can be closed. A draft or an already-closed role answers 409.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
jobIdrequired | path | string | Example job_01J8Z3K9Q0EXAMPLE. |
Responses
200 — The role is closed.
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"
},
"status": "closed",
"closedAt": "2026-09-08T01:10:00.000Z"
}
}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"
}
}409 — The job is not in a state this action can be taken from. Body: Error.
{
"error": {
"code": "conflict",
"message": "Only a published job can be closed. Read the job to see the status it is in.",
"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. |