Developers

Jobs

GET /api/v1/jobs returns the jobs in your workspace a page at a time, and GET /api/v1/jobs/{id} returns one, with every parameter and field.

Overview

GET /api/v1/jobs lists the jobs in your workspace, and GET /api/v1/jobs/{id} returns one of them. You get the jobs you can see in the app, minus any team your key's team access leaves out. Both need the Read permission. POST /api/v1/jobs creates a job and needs Write.

Authentication, Errors and Rate limits cover what every operation shares. This page covers the parameters and the fields.

Request

curl "https://attia.app/api/v1/jobs?limit=50" \  -H "Authorization: Bearer $ATTIA_API_KEY"

All four query parameters are optional.

ParameterTypeDefaultMeaning
limitinteger, 1 to 10025How many jobs to return
cursorstringnoneThe next_cursor of the previous page
updated_sinceRFC 3339 timestampnoneOnly jobs updated at or after this time
include_archivedtrue or falsefalseAlso return archived jobs

A parameter outside this table gets a 400 with the code invalid-request. So does a parameter sent twice.

Response

The answer holds the jobs in data, sorted by updated_at and then id, oldest change first. next_cursor is a string when more jobs exist and null on the last page.

{  "data": [    {      "id": "0192f3a1-6b02-7d4c-8e11-5a9c0b7d3e01",      "title": "Backend developer",      "slug": "backend-developer",      "status": "live",      "publish_status": "published",      "team_id": "0192f39f-11aa-7c3e-b2d4-9e8f7a6b5c4d",      "employment_type": "full_time",      "contract_type": "permanent",      "workplace_type": "hybrid",      "locations": ["Eksempelvik"],      "positions_count": 2,      "application_deadline": "2026-11-15",      "published_at": "2026-09-28T07:30:00.000000Z",      "closed_at": null,      "archived_at": null,      "created_at": "2026-09-25T12:04:51.220417Z",      "updated_at": "2026-10-01T09:42:10.118305Z"    }  ],  "next_cursor": null}
FieldTypeMeaning
idUUIDThe job's id. It never changes
titlestringThe job title
slugstringThe job's name in its URL, unique in the workspace
statusenumdraft, live, paused or closed. Whether candidates can apply, which the app calls Publish. See Job status
publish_statusenumunpublished, scheduled or published. The job's status in the app. A scheduled job goes out at its publish time, and an unpublished job with a closed_at was published before
team_idUUIDThe team that owns the job
employment_typeenumfull_time or part_time
contract_typeenumpermanent, temporary, fixed_contract, seasonal, project, trainee, apprenticeship, self_employed or agency
workplace_typeenumremote, hybrid or onsite
locationslist of stringsThe places the job names. Empty when it names none
positions_countintegerHow many people the job hires, at least 1
application_deadlinedate or nullThe last day to apply, YYYY-MM-DD
published_attimestamp or nullWhen the job went live
closed_attimestamp or nullWhen the job was last unpublished. null while it is scheduled or published
archived_attimestamp or nullWhen the job was archived
created_attimestampWhen the job was created
updated_attimestampWhen the job last changed

An enum can gain a value in version 1, so handle one you do not know. Timestamps, nulls and enums describes each format.

Page through every job

Ask for a page, then send next_cursor back as cursor until it is null.

curl "https://attia.app/api/v1/jobs?limit=100&cursor=eyJ1IjoiMjAyNi0xMC0wMlQwODoxNTozMC40ODI5MTNaIiwiaSI6IjAxOTJmM2ExLTdjNDQtN2IxZS05YTUwLTNmMWQyYzZlOGEwMiJ9" \  -H "Authorization: Bearer $ATTIA_API_KEY"

The cursor stays stable while people edit jobs. A job edited during your read moves behind the cursor, and you may see it again on a later page. Keep the latest copy per id. Pagination explains the sort order.

Poll for changes

updated_since is inclusive. Send a time five minutes before the largest updated_at you have stored. A change gets its updated_at when the save starts, so it can turn up with a time older than one you already read. The answer repeats jobs you already have, so drop the repeats by id. With 2026-10-01T09:42:10.118305Z as the largest, the call is this.

curl "https://attia.app/api/v1/jobs?updated_since=2026-10-01T09:37:10.118305Z" \  -H "Authorization: Bearer $ATTIA_API_KEY"

You can combine updated_since with cursor and limit when one poll returns more than a page.

Archived jobs

The list leaves archived jobs out unless you ask for them. That matters when you keep a copy. A job that someone archives stops appearing in the default list, and your copy never learns why. Send include_archived=true when you poll, and an archived job comes back with a time in archived_at.

curl "https://attia.app/api/v1/jobs?include_archived=true&updated_since=2026-10-01T00:00:00Z" \  -H "Authorization: Bearer $ATTIA_API_KEY"

Get one job

GET /api/v1/jobs/{id} returns the job with that id. It takes no query parameters, so one gets a 400 with the code invalid-request.

curl "https://attia.app/api/v1/jobs/0192f3a1-6b02-7d4c-8e11-5a9c0b7d3e01" \  -H "Authorization: Bearer $ATTIA_API_KEY"

The answer holds the job in data, with the same fields as a job in the list. An archived job comes back too, with a time in archived_at.

{  "data": {    "id": "0192f3a1-6b02-7d4c-8e11-5a9c0b7d3e01",    "title": "Backend developer",    "slug": "backend-developer",    "status": "live",    "publish_status": "published",    "team_id": "0192f39f-11aa-7c3e-b2d4-9e8f7a6b5c4d",    "employment_type": "full_time",    "contract_type": "permanent",    "workplace_type": "hybrid",    "locations": ["Eksempelvik"],    "positions_count": 2,    "application_deadline": "2026-11-15",    "published_at": "2026-09-28T07:30:00.000000Z",    "closed_at": null,    "archived_at": null,    "created_at": "2026-09-25T12:04:51.220417Z",    "updated_at": "2026-10-01T09:42:10.118305Z"  }}

A 404 with the code not-found means no job your key can see has that id. The job may not exist, may be deleted, may belong to another workspace, or may sit in a team the list leaves out for you. Every one of these gets the same answer, so a 404 never tells you whether the job exists. An id that is not a UUID gets the same 404.

Create a job

POST /api/v1/jobs creates a job in a team you pick. It needs the Write permission. The job starts as a draft: Unpublished, on no career site and on no job board. Every field other than the title and the team takes its default, and you finish the job in the app.

curl -X POST "https://attia.app/api/v1/jobs" \  -H "Authorization: Bearer $ATTIA_API_KEY" \  -H "Idempotency-Key: 0192f3c4-2d18-7a6e-9f30-6b1e8c2d4a77" \  -H "Content-Type: application/json" \  -d '{"title": "Backend developer", "team_id": "0192f39f-11aa-7c3e-b2d4-9e8f7a6b5c4d"}'

The body is a JSON object with these two fields, and both are required.

FieldTypeMeaning
titlestringThe job title, at most 200 characters. It cannot be blank, and spaces at either end are removed
team_idUUIDThe team the job belongs to. You must be able to create jobs in it, and a key limited to some teams must include it

A body that is not a JSON object gets a 400 with the code invalid-request. So does a missing field, a blank title, a title over 200 characters, a field outside this table and any query parameter. errors names each one.

The answer is a 201 with the job in data, with the same fields as Get one job. The Location header holds the job's path.

HTTP/2 201location: /api/v1/jobs/0192f3a1-6b02-7d4c-8e11-5a9c0b7d3e01
{  "data": {    "id": "0192f3a1-6b02-7d4c-8e11-5a9c0b7d3e01",    "title": "Backend developer",    "slug": "backend-developer-0192f3a16b02",    "status": "draft",    "publish_status": "unpublished",    "team_id": "0192f39f-11aa-7c3e-b2d4-9e8f7a6b5c4d",    "employment_type": "full_time",    "contract_type": "permanent",    "workplace_type": "onsite",    "locations": [],    "positions_count": 1,    "application_deadline": null,    "published_at": null,    "closed_at": null,    "archived_at": null,    "created_at": "2026-10-29T08:15:30.482913Z",    "updated_at": "2026-10-29T08:15:30.482913Z"  }}

In the app, the job's Activity and the workspace audit log name you as the creator, followed by "via API".

A team you cannot create jobs in gets a 400 invalid-request that names team_id. The answer is the same whether the team does not exist, is archived, belongs to another workspace, or is one your key's team access leaves out. A view-only workspace gets workspace-view-only.

Retry a create safely

Every create needs an Idempotency-Key header. Pick a new value for each job: 1 to 255 letters, digits, - or _. A UUID works. A create without one gets a 400 idempotency-key-required, before the API reads the body.

When a create times out or the connection drops, send it again with the same key. Within 24 hours, the same key with the same body and the same API key creates no second job. It gets the first answer back: the 201, the Location and the job as it was created, plus the header Idempotency-Replayed: true. Read the job with GET /api/v1/jobs/{id} to see it as it is now. The order of the fields in the body does not matter, but a changed value does.

The same key with another body or another API key gets a 422 idempotency-key-reused, and so does the same key after 24 hours, up to 90 days; after 90 days the same key creates a new job. A retry that arrives while the first request is still running waits for it. If the wait runs too long, the retry gets a 429 rate-limited with Retry-After: 1.