Developers

Getting started

Create a personal API key, send it with a request and read your first answer from the Attia API.

Overview

The Attia API lets your own programs read data from a workspace, and create jobs in it, over HTTPS. Every address starts with https://attia.app/api/v1, every answer is JSON, and a personal API key identifies you.

Version 1 has seven operations so far: List jobs, Get one job, Create a job, List applications, Get one application, List candidates and Get one candidate. This page takes you from no key to your first answer.

The API is available on Business and Enterprise. On any other plan Settings → API explains what the API does instead of showing its settings. An admin gets Upgrade to Business there, and anyone else a line asking a workspace admin to upgrade.

Create a key

Open Settings → Security & access → Personal API keys and choose New API key. Give the key a name and at least the Read permission. Attia shows the key once, so copy it then. A key starts with attia_api_.

A key acts as you. It can do what you can do in the workspace, limited further by the permissions and teams you gave it. Authentication covers permissions, revoking and IP restrictions.

Send the key

Send the key in the Authorization header as a bearer token.

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

Call the API from a server or a script, never from a web page. The API sends no CORS headers, so a browser refuses the answer, and a key in page source is a key anyone can copy.

Your first request

Put the key in an environment variable and ask for two jobs.

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

A list answers with data and next_cursor.

{  "data": [    {      "id": "0192f3a1-6b02-7d4c-8e11-5a9c0b7d3e01",      "title": "Backend developer",      "slug": "backend-developer",      "status": "live",      "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"    },    {      "id": "0192f3a1-7c44-7b1e-9a50-3f1d2c6e8a02",      "title": "Store manager",      "slug": "store-manager",      "status": "draft",      "team_id": "0192f39f-11aa-7c3e-b2d4-9e8f7a6b5c4d",      "employment_type": "part_time",      "contract_type": "temporary",      "workplace_type": "onsite",      "locations": [],      "positions_count": 1,      "application_deadline": null,      "published_at": null,      "closed_at": null,      "archived_at": null,      "created_at": "2026-10-02T08:15:30.482913Z",      "updated_at": "2026-10-02T08:15:30.482913Z"    }  ],  "next_cursor": "eyJ1IjoiMjAyNi0xMC0wMlQwODoxNTozMC40ODI5MTNaIiwiaSI6IjAxOTJmM2ExLTdjNDQtN2IxZS05YTUwLTNmMWQyYzZlOGEwMiJ9"}

next_cursor is not null, so more jobs exist. Pagination shows how to fetch them.

Webhooks

A webhook sends a POST to your URL when something happens in the workspace. Webhooks come with the API, on Business and Enterprise. A workspace admin sets up each one with a label, a URL, the events it follows and, if they want, one team. See Set up a webhook. A tool such as Zapier can also subscribe and unsubscribe through the API, with an admin's key: see Webhooks.

Version 1 sends eight events:

  • job.created: a job was created.
  • job.updated: a field the API returns for a job changed, or a deleted job was restored.
  • job.deleted: a job was deleted.
  • application.created: an application was created.
  • application.status_changed: an application moved to another status.
  • application.updated: a field the API returns for an application changed, or a deleted application was restored.
  • application.deleted: an application was deleted.
  • candidate.erased: a candidate was erased from the workspace for good.

Each change sends one event, with the record's public_revision as its revision. A move to another status sends application.status_changed, and it sends application.updated too only when the same change touches another field, such as the assignee. A change to a field the API does not return sends nothing, but it can still move public_revision, so revision numbers can skip. A job or application moved to another team sends its event to webhooks on the new team, not the old one.

Application events go out only while an admin has turned on Candidate data in the API in Settings → API. While it is off Attia sends none, and turning it back on does not send the ones it skipped. candidate.erased goes out either way, because it carries only the candidate's id, so you can erase your copy. A candidate belongs to no team, so candidate.erased goes to every webhook that follows it, including a webhook set to one team. If you never saw that candidate, you can ignore the id.

A webhook acts as the admin who set it up. Attia checks that admin's access when it sends, so a job or application the admin can no longer see is not sent. A webhook turns off when its admin leaves the workspace or stops being an admin, or after 24 hours of failed deliveries. When its admin has left or is no longer an admin, it then acts as the admin who turns it on again.

Set up a webhook

An admin sets up a webhook in Settings → API. Under Webhooks, choose New webhook, or the + when the list already has one, and fill in the form:

  • Label: the webhook's name in the list.
  • URL: the address Attia sends each event to. It must start with https://.
  • Signing secret: Attia makes it for you. Use it to verify the signature.
  • Data change events: Applications, Candidates and Jobs. Applications starts checked. Each box follows every event of its kind, and Candidates is candidate.erased. To follow single events, subscribe through the API.
  • Team selection: All public teams or one team. You cannot change it after creation. All public teams leaves out private teams, so a private team needs a webhook of its own.

Choose Create webhook, and Attia opens the webhook's page. It shows the delivery status and the delivery failures, and you can copy the secret at any time or rotate it. The three-dot menu there, and on the webhook's row in the list, has Edit webhook for the label, the URL and the events, Disable webhook or Enable webhook, and Delete webhook.

After a downgrade below Business a webhook stays. The page of one that is on reads Paused: plan, and Attia sends nothing until the workspace is back on Business or Enterprise. Attia does not send the events it skipped.

The request

The body is JSON.

{  "id": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",  "type": "job.created",  "version": "v1",  "timestamp": "2026-10-02T08:15:31.004127Z",  "workspace_id": "0192f39e-02b1-7a5c-9d3e-4f6a8b0c2d1e",  "revision": 1,  "data": {    "id": "0192f3a1-7c44-7b1e-9a50-3f1d2c6e8a02",    "title": "Store manager",    "slug": "store-manager",    "status": "draft",    "team_id": "0192f39f-11aa-7c3e-b2d4-9e8f7a6b5c4d",    "employment_type": "part_time",    "contract_type": "temporary",    "workplace_type": "onsite",    "locations": [],    "positions_count": 1,    "application_deadline": null,    "published_at": null,    "closed_at": null,    "archived_at": null,    "created_at": "2026-10-02T08:15:30.482913Z",    "updated_at": "2026-10-02T08:15:30.482913Z"  }}
  • id is the delivery's id, the same value as the webhook-id header.
  • type is the event.
  • version is the API version data follows: v1.
  • timestamp is when the change happened. It stays the same on every retry; the webhook-timestamp header carries the attempt's time.
  • revision is the record's revision when the event happened, so you can order two events for the same record. It is null on candidate.erased.
  • data is the job or application as GET /api/v1/jobs/{id} or GET /api/v1/applications/{id} returns it to the admin when Attia sends. It can be newer than the event. On job.deleted, application.deleted and candidate.erased, data holds only id.

Three headers carry the signature: webhook-id, webhook-timestamp and webhook-signature. The request also has content-type: application/json and user-agent: Attia-Webhooks/1.0.

Verify the signature

Attia signs with Standard Webhooks. webhook-signature is v1, and then the base64 HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<body>, keyed with the bytes after whsec_ in your secret. For 24 hours after a secret is rotated the header holds two signatures separated by a space, and the request is genuine if either one matches. Any Standard Webhooks library checks this for you, and refuses a timestamp more than five minutes from your clock.

Answer and retries

Answer with a 2xx status within 5 seconds. After a 408, a 429, a 5xx, a timeout or no connection, Attia tries again under the same webhook-id: 1 minute after the first try, 1 hour after the second, then every 6 hours, for up to 24 hours. If an Attia worker stops in the middle of a delivery, the next try can come sooner. If a delivery still fails after 24 hours, Attia turns the webhook off, cancels its queued deliveries, and tells every admin by email and in the inbox. An admin turns it on again in Settings → API. The webhook says there why it is off, and its page lists the deliveries that failed in the last 90 days. Attia does not resend the events it skipped while the webhook was off. Any other status, a redirect included, ends the delivery. A body over 256 KB is never sent.

Delivery is at least once, so the same webhook-id can reach you twice. Skip an id you have already handled.

An admin can redeliver one failed delivery at a time from the webhook's page in Settings → API, while the webhook is on and for 30 days after the delivery failed. A redelivery keeps the same webhook-id and id, so skip it if you already handled that id. Attia reads data again under the webhook admin's access at that moment. The delivery leaves the list once it arrives, and stays with its new result if it fails again.

Next steps

  • Authentication: the key format, permissions, revoking and IP restrictions.
  • Pagination: page size, cursors, polling for changes, and how timestamps, nulls and enums look.
  • Errors: the problem document and every error code.
  • Rate limits: how many requests you can send and the headers that count them.
  • Versioning: what can change in version 1 and how a retirement is announced.
  • Jobs: every parameter and field of GET /api/v1/jobs and GET /api/v1/jobs/{id}.
  • Applications: every parameter and field of GET /api/v1/applications and GET /api/v1/applications/{id}.
  • Candidates: every parameter and field of GET /api/v1/candidates and GET /api/v1/candidates/{id}, and the switch they need.
  • Webhooks: subscribing with POST /api/v1/webhooks, unsubscribing with DELETE /api/v1/webhooks/{id} and reading sample events with GET /api/v1/webhooks/samples/{event}, as Zapier does, and listing webhooks with GET /api/v1/webhooks.