Wipperoz
Browse the docs

Post a job from your own system

Updated

If your roles already live somewhere else — an ATS, an internal tool, a spreadsheet somebody guards — this is how they reach Orbit without anyone retyping them.

What you need

A key with the read and manage jobs scope, created in Orbit → Settings → API keys. A read-only key answers 403 on every request on this page, deliberately: publishing puts an advertisement in front of the public, and the key that renders a careers site should not also be able to post advertisements.

export ORBIT_API_KEY="…a key with read and manage jobs…"

The shape: two calls

Creating and publishing are separate.

  1. POST /v1/jobs creates a draft. Nothing is public.
  2. POST /v1/jobs/{jobId}/publish takes it live. Publishing is free.

That is deliberate. Publishing puts the role on your hosted job page, lets candidates apply and enters it into the Wipperoz job seeker feed. A single call that did all of that would mean a key in a misconfigured loop posting advertisements before anyone read a log.

It also leaves a person in the loop when you want one: create the drafts from your system, and let a recruiter publish them from Orbit after reading them. If you do not want one, call publish yourself. Both are supported; the choice is yours rather than ours.

Managed job ads

A published ad collects applications and lists them in the order they arrived. Managing an ad is Orbit’s paid service on top: its applicants are ranked, your candidate pool is matched against it, and you can screen them, from Orbit or over the API (see Screen a candidate). A recruiter switches it on per ad, in Orbit — the read and manage jobs scope creates, edits, publishes and closes roles, and does not manage ads.

One thing reaches this API: a managed ad’s title and category are locked, as Edit a role below explains.

Create the draft

curl -X POST https://api.wipperoz.com/v1/jobs \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ats-job-4471" \
  -d '{
    "title": "Senior Backend Engineer",
    "category": "engineering",
    "description": "<p>Own the services behind our hiring pipeline.</p>",
    "responsibilities": ["Design and ship services on AWS Lambda and DynamoDB"],
    "skills": [
      {"name": "TypeScript", "level": "required"},
      {"name": "DynamoDB", "level": "nice_to_have"}
    ],
    "employmentType": "full_time",
    "contractType": "permanent",
    "salary": {"min": 150000, "max": 180000, "currency": "AUD", "period": "year"},
    "location": {"country": "AU", "state": "NSW", "city": "Sydney", "remotePolicy": "hybrid"}
  }'
{
  "job": {
    "id": "job_01J9A0B1C2EXAMPLE",
    "status": "draft",
    "title": "Senior Backend Engineer",
    "category": "engineering",
    "applyLink": "https://www.wipperoz.com/en/apply/job_01J9A0B1C2EXAMPLE?src=careers&account=acc_01J8Z3EXAMPLE"
  }
}

Keep the id. It is how you publish, edit and close the role later, and it is the id you should store against the role in your own system.

Only title is required here. Publishing also needs a description and, the first time a role goes live, a category; nothing else ever blocks it. description takes HTML, which is what the Orbit editor produces; plain text is fine too.

Categories

category says what kind of job this is, from a fixed list. Send the value; the label is what Orbit shows.

Value Shown in Orbit as
engineering Engineering
data Data
product Product
design Design
marketing Marketing
sales Sales
customer_success Customer success
operations Operations
finance Finance
hr_people HR and people
legal Legal
healthcare Healthcare
education Education
hospitality Hospitality
retail Retail
logistics Logistics
construction_trades Construction and trades
manufacturing Manufacturing
admin_support Admin and support
other Other

Anything else answers 400 naming the values it accepts. other is for the role that fits none of them.

Publish it

curl -X POST https://api.wipperoz.com/v1/jobs/job_01J9A0B1C2EXAMPLE/publish \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ats-job-4471-publish" \
  -d '{"expiryDays": 30}'
{
  "job": {"id": "job_01J9A0B1C2EXAMPLE", "status": "published", "url": "…"},
  "firstPublish": true
}
  • expiryDays is 30, 60, 90, or null for no expiry. null is the default: a role that disappears on a date nobody chose is worse than one that stays up.
  • firstPublish is false when the role had been live before — a closed role you are reopening keeps its URL.
  • A role missing what publishing needs answers 422 with a missing list rather than a vague 400: title or description on any publish, and category on a role’s first.

Edit a role

curl -X PATCH https://api.wipperoz.com/v1/jobs/job_01J9A0B1C2EXAMPLE \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"salary": {"min": 170000, "max": 200000, "currency": "AUD", "period": "year"}}'

Send only the fields you are changing; the rest are left alone.

On a draft the change applies immediately. On a published role the new words are staged rather than replacing what candidates are currently reading, and the response says so:

{"job": {"…": "…as it still reads today"}, "pendingRevision": true}

People are applying against the words on the page, and changing them underneath an open application changes what someone agreed to after they agreed to it. So a recruiter promotes the edit in Orbit — or you promote it yourself in the same call:

-d '{"salary": {...}, "publishEdits": true}'

Two fields skip the queue and always apply at once: expiresAt and maxMatches. Both exist to stop something, and holding “stop at fifty matches” behind a promotion would keep the matching running past the cap you set.

A managed ad keeps its title and category

Once an ad is managed, an edit that changes its title or category — directly, or by promoting a staged one with publishEdits — answers 409 and changes nothing: not the live ad, and not what is staged either. The answer:

{
  "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"]
  }
}

fields names what the edit would have changed. Sending the current value again is not a change, so a sync that sends every field on every run keeps working.

The lock is what stops a managed ad being renamed into a different job. If the role upstream really is a different job now, post it as a new one. If the new title is simply right, a recruiter stops managing the ad in Orbit, on the role’s Settings tab, and the edit goes through. This API does not say whether an ad is managed; this 409 is where an integration finds out.

Close a role

curl -X POST https://api.wipperoz.com/v1/jobs/job_01J9A0B1C2EXAMPLE/close \
  -H "Authorization: Bearer $ORBIT_API_KEY"

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. Closing a managed ad also gives its slot back to your plan.

Applications you already received stay, and stay readable in Orbit. Closing is not deleting, and this API has no delete. Only a published role can be closed; a draft or an already-closed role answers 409.

Retries that do not post twice

Send an Idempotency-Key on every POST. Any value that identifies the intent — the role’s id in your own system is ideal.

  • A retry with the same key returns the first answer, with Idempotent-Replay: true. No second advertisement, no second publish.
  • The same key with a different body answers 409. Two intents wearing one name is a bug worth being told about.
  • Keys are remembered for 24 hours, per account.
  • A request that was refused releases its key, so a 422 you fix by adding what was missing can be retried with the same key.

Without the header, two identical creates make two advertisements. Networks time out; send the header.

Keeping a sync honest

  • Store our id against your role. It is the only stable handle.
  • Close what closed upstream. A stale advertisement costs a candidate their afternoon, which is worse than an absent one.
  • Do not re-create on every sync. PATCH the role you already created; a fresh POST makes a second advertisement with a second URL.
  • Map your job families to a category once, and send it on every create. A draft without one cannot go live.
  • Treat managed_ad_locked as a decision, not a retry. Retrying the same rename gets the same 409.
  • Rate limit: 120 requests a minute per key. Forty roles is forty calls, comfortably inside it.

Reference

Create a draft jobPOST /v1/jobs Publish a jobPOST /v1/jobs/{jobId}/publish Edit a jobPATCH /v1/jobs/{jobId} Close a jobPOST /v1/jobs/{jobId}/close
Wipperoz Logo

Wipperoz is a video-first interactive virtual CV platform designed to replace traditional PDF resumes with dynamic, shareable profiles.

© 2026 Wipperoz. All rights reserved

Developed by epoqx.ai