Wipperoz
Parcourir la documentation

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, it is not charged for, 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 and a description gate publishing.

Send an Idempotency-Key so a retried request returns the first answer instead of creating a second advertisement.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringA 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.

FieldTypeDescription
titlestring
descriptionstringRich text HTML, as the Orbit editor produces. Plain text is fine.
responsibilitiesstring[]
skillsSkill[]
benefitsstring[]
senioritystring
yearsExperiencestring
employmentTypestringOne of: full_time, part_time.
contractTypestringOne of: permanent, part_time, fixed_term, contract, casual.
salarySalary
locationLocation
workStylestring
expiresAtstring | null (date-time)When the role stops accepting applications. null clears it — live until closed by hand. Applies immediately, even on a published ad.
maxMatchesinteger | nullStop generating AI matches once this many exist. null removes the cap. Applies immediately, even on a published ad.
{
  "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"
  ],
  "skills": [
    {
      "name": "TypeScript",
      "level": "required"
    }
  ],
  "employmentType": "full_time",
  "contractType": "permanent",
  "salary": {
    "min": 150000,
    "max": 180000,
    "currency": "AUD",
    "period": "year"
  },
  "location": {
    "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",
    "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.

Parameters

NameInTypeDescription
jobIdrequiredpathstring Example job_01J8Z3K9Q0EXAMPLE.

Request body

Required. Shape: JobWrite.

FieldTypeDescription
titlestring
descriptionstringRich text HTML, as the Orbit editor produces. Plain text is fine.
responsibilitiesstring[]
skillsSkill[]
benefitsstring[]
senioritystring
yearsExperiencestring
employmentTypestringOne of: full_time, part_time.
contractTypestringOne of: permanent, part_time, fixed_term, contract, casual.
salarySalary
locationLocation
workStylestring
expiresAtstring | null (date-time)When the role stops accepting applications. null clears it — live until closed by hand. Applies immediately, even on a published ad.
maxMatchesinteger | nullStop generating AI matches once this many exist. null removes the cap. Applies immediately, even on a published ad.
publishEditsbooleanPromote 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",
    "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"
  },
  "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"
  }
}

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, it enters the Wipperoz job seeker feed and the first matching run begins.

**This spends credits.** The publish fee is taken once, on first publish — re-publishing a role that has been live before keeps its address and is not charged again, and the response says which happened. Not enough credits answers 402 and nothing is written.

A job with no title or no description answers 422 naming the fields that block it. Everything else is optional.

Parameters

NameInTypeDescription
jobIdrequiredpathstring Example job_01J9A0B1C2EXAMPLE.
Idempotency-KeyheaderstringA 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.

FieldTypeDescription
expiryDaysinteger | nullWhen 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",
    "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"
  },
  "creditsCharged": 500,
  "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"
  }
}

402 — The account cannot afford the publish fee. Nothing was written. Body: Error.

{
  "error": {
    "code": "insufficient_credits",
    "message": "This account does not have enough credits to publish. Top up in Orbit → Settings → Billing and call this endpoint again.",
    "requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
    "balanceCredits": 120
  }
}

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 and a description.",
    "requestId": "5f7a9c1e-2b3d-4e5f-8a9b-0c1d2e3f4a5b",
    "missing": [
      "title",
      "description"
    ]
  }
}

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

NameInTypeDescription
jobIdrequiredpathstring 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",
      "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"
    },
    "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

FieldTypeDescription
errorrequiredobject

JobList

FieldTypeDescription
jobsrequiredJob[]
nextCursorrequiredstring | nullPass back as cursor for the next page. null when there is no more.

Skill

FieldTypeDescription
namerequiredstring
levelrequiredstringOne of: required, nice_to_have.

Salary

FieldTypeDescription
minnumber
maxnumber
currencystringISO 4217, as entered by the recruiter.
periodstringFree text as entered, typically year, month, day or hour.

Location

FieldTypeDescription
countrystring
statestring
citystring
remotePolicystringOne of: onsite, hybrid, remote.
onsiteDaysstring

Company

FieldTypeDescription
namestring
logoUrlstring (uri)

Job

Enough to build a JobPosting JSON-LD block: title, description, postedAtdatePosted, expiresAtvalidThrough, companyhiringOrganization, locationjobLocation, salarybaseSalary, employmentType.

FieldTypeDescription
idrequiredstring
statusrequiredstringpublished accepts applications. The other two are returned, not omitted, so a careers page can render its own closed state. One of: published, closed, expired.
titlerequiredstring
descriptionrequiredstring
responsibilitiesrequiredstring[]
skillsrequiredSkill[]
benefitsrequiredstring[]
senioritystring
yearsExperiencestring
employmentTypestringOne of: full_time, part_time.
contractTypestringOne of: permanent, part_time, fixed_term, contract, casual.
salarySalary
locationLocation
workStylestring
companyrequiredCompany
postedAtstring (date-time)
updatedAtstring (date-time)The latest of an edit, the publish and the close. What updatedSince compares against.
expiresAtstring (date-time)The role stops accepting applications at this moment. Use as validThrough.
closedAtstring (date-time)
urlstring (uri)The Wipperoz-hosted job page, when the role has one.
applyLinkrequiredstring (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.

FieldTypeDescription
titlestring
descriptionstringRich text HTML, as the Orbit editor produces. Plain text is fine.
responsibilitiesstring[]
skillsSkill[]
benefitsstring[]
senioritystring
yearsExperiencestring
employmentTypestringOne of: full_time, part_time.
contractTypestringOne of: permanent, part_time, fixed_term, contract, casual.
salarySalary
locationLocation
workStylestring
expiresAtstring | null (date-time)When the role stops accepting applications. null clears it — live until closed by hand. Applies immediately, even on a published ad.
maxMatchesinteger | nullStop 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.

FieldTypeDescription
idrequiredstring
statusrequiredstringOne of: draft, published, closed, expired.
titlerequiredstring
descriptionrequiredstring
responsibilitiesrequiredstring[]
skillsrequiredSkill[]
benefitsrequiredstring[]
senioritystring
yearsExperiencestring
employmentTypestringOne of: full_time, part_time.
contractTypestringOne of: permanent, part_time, fixed_term, contract, casual.
salarySalary
locationLocation
workStylestring
companyrequiredCompany
postedAtstring (date-time)
updatedAtstring (date-time)The latest of an edit, the publish and the close. What updatedSince compares against.
expiresAtstring (date-time)The role stops accepting applications at this moment. Use as validThrough.
closedAtstring (date-time)
urlstring (uri)The Wipperoz-hosted job page, when the role has one.
applyLinkrequiredstring (uri)The hosted apply page. Link candidates here; it records that they came from your site.
Wipperoz Logo

Wipperoz est une plateforme de CV virtuel interactive axée sur la vidéo, conçue pour remplacer les CV PDF traditionnels par des profils dynamiques et partageables.

© 2026 Wipperoz. Tous droits réservés

Développé par epoqx.ai