# Jobs feed specification for ATS

This is Joonow's public jobs feed specification for applicant tracking systems (ATS) and career sites. With it you build the feed, check it in the [validator](/en/integrate-your-ats/validator) and request the integration without talking to anyone. The program is free: see [what your ATS gets](/en/integrate-your-ats).

## Summary

- **One XML file** at a public URL, ideally with `https://`, with **all** of your clients' open jobs.
- **Each job** needs a stable id, title, description, apply URL and company.
- **Recommended:** the email of the person responsible for the job (`email`). It never shows on the site and moves the job up in the ranking ([why include it](#why-include-the-job-owners-email)).
- **Recommended:** the company's website and logo (`companyurl`, `companylogo`); the description (`companydescription`) is optional. The logo shows on the jobs, in search and in the directory, and all three help build the company page ([why include them](#why-include-the-company-website-logo-and-description)).
- **We read the feed every 6 hours** (the schedule is agreed on approval), with a conditional GET (`ETag` and `Last-Modified`).
- **A job that leaves the file is closed** on the second read in a row without it.
- **Applications happen on your site:** the button takes people to the job URL with `utm_source=joonow`. Or, with [applications by webhook](#applications-by-webhook) turned on, candidates apply on Joonow and the signed application reaches your system.
- **Before requesting, validate** in the [validator](/en/integrate-your-ats/validator): it shows each job's errors and stores nothing.

## Accepted formats

| Format | When to use it | Element for each job |
|---|---|---|
| Joonow XML (recommended) | You are building the feed now or already generate the Indeed XML. | `<job>` |
| Indeed XML | Your ATS already publishes the standard Indeed XML (`requisitionid`, `sourcename`, `remotetype`). | `<job>` |
| LinkedIn XML | Your ATS already generates the LinkedIn jobs XML (`partnerJobId`, `applyUrl`). | `<job>` |

All three use a `<source>` root with a list of `<job>` elements. Element names are case-insensitive and text can come inside `CDATA`.

## Joonow XML

It is the XML many ATS already generate for Indeed, with a few extra fields (expiration, hybrid model and level). Download the [example XML](/partners/joonow-feed-example.xml) and the [XSD schema](/partners/joonow-feed.xsd).

```xml
<?xml version="1.0" encoding="UTF-8"?>
<source>
  <publisher>Your ATS name</publisher>
  <publisherurl>https://www.your-ats.com</publisherurl>
  <lastBuildDate>2026-10-30T09:00:00Z</lastBuildDate>
  <job>
    <referencenumber>ACME-4821</referencenumber>
    <title><![CDATA[Junior Support Analyst]]></title>
    <company><![CDATA[Acme Logistics]]></company>
    <email>recruiting@acme.example</email>
    <companyurl>https://www.acme.example/</companyurl>
    <companylogo>https://www.acme.example/img/logo-acme-256.png</companylogo>
    <companydescription><![CDATA[Logistics operator with warehouses in Pernambuco and Bahia.]]></companydescription>
    <url><![CDATA[https://acme.your-ats.com/jobs/4821]]></url>
    <date>2026-10-28T13:00:00Z</date>
    <expirationdate>2026-12-15T23:59:59Z</expirationdate>
    <city><![CDATA[Austin]]></city>
    <state>TX</state>
    <country>US</country>
    <description><![CDATA[<p>Customer support...</p><h3>Requirements</h3><ul><li>...</li></ul>]]></description>
    <salary><![CDATA[US$ 3800 to US$ 4200 per month]]></salary>
    <jobtype>fulltime</jobtype>
    <category>Professional</category>
    <remotetype>Hybrid remote</remotetype>
  </job>
</source>
```

### Job fields

| Element | Required | Type | Example | How Joonow uses it |
|---|---|---|---|---|
| `referencenumber` | Yes | Text, up to 200 characters | `ACME-4821` | Identifies the job from one read to the next. It must be unique and **stable**: if it changes, it becomes a different job. Without it, we use the apply URL. |
| `title` | Yes | Text, up to 200 characters | `Junior Support Analyst` | Job title, in search and on the page. An ALL CAPS title becomes normal case (acronyms stay). |
| `description` | Yes | Simple HTML, up to 50 KB | `<p>…</p><ul><li>…</li></ul>` | Job description. Only `p`, `br`, `ul`, `ol`, `li`, `strong`, `b`, `em`, `i`, `u`, `h2`, `h3`, `h4`, `a`, `blockquote` and `hr` are kept; everything else (scripts, styles, images) is removed. |
| `url` | Yes | `https://` URL | `https://acme.your-ats.com/jobs/4821` | Destination of the "Apply" button, with `utm_source=joonow`. It must open the job itself. |
| `company` | Yes | Text | `Acme Logistics` | Company name: it creates the company page with all its jobs. Empty becomes "Confidential". |
| `city` | Recommended | Text | `Austin` | City: filters, location search and city pages. |
| `state` | Recommended | Code or name | `TX` | State or region. In Brazil, the state code (UF) or the full name. |
| `country` | Recommended | 2-letter ISO code or name | `US` | Country. Empty uses the feed's default country, set on approval. |
| `date` | Recommended | Date (ISO 8601 or RFC 1123) | `2026-10-28T13:00:00Z` | Posting date: sorting, "posted X days ago" and the job's age. Without it, we use the read date; a future date becomes today. |
| `email` | Recommended | Email, up to 254 characters | `recruiting@acme.example` | Work email of the person responsible for the job. **It never shows on the site.** The job gets ranking points and, if it gets reported, the Joonow team has someone to talk to ([why include it](#why-include-the-job-owners-email)). An invalid one is ignored, without rejecting the job. |
| `companyurl` | Recommended | Absolute URL | `https://www.acme.example/` | The company's own website (not the ATS, a social profile or a jobs page). It becomes the link on the company page and helps us find the logo and social profiles ([why include them](#why-include-the-company-website-logo-and-description)). |
| `companylogo` | Recommended | Absolute image URL | `https://www.acme.example/img/logo.png` | Company logo in PNG, JPEG or WebP, 128 px or larger (square works best). Joonow downloads a copy and serves it from its own site. |
| `companydescription` | Optional | Text or simple HTML, up to 4,000 characters | `Logistics operator with warehouses in the Northeast.` | What the company does, for its page. Repeat the same text on all of the company's jobs. |
| `expirationdate` | Optional | Date (ISO 8601) | `2026-12-15T23:59:59Z` | Expiration: after it, the job is closed. A job that arrives already expired is rejected. |
| `salary` | Optional | Text with an amount or range | `US$ 3800 to US$ 4200 per month` | Salary, only when the employer discloses it. We recognize currency (R$, US$, €), ranges and period (hour, day, week, month, year). |
| `jobtype` | Optional | Code | `fulltime` | Contract type (values below). |
| `category` | Optional | Code | `Professional` | Job level (values below). Other values are ignored. |
| `isRemote` | Optional | `Yes` or `No` | `Yes` | `Yes` = fully remote; `No` = on-site. |
| `remotetype` | Optional | Text | `Hybrid remote` | `Fully remote` = fully remote; `Hybrid remote` = hybrid. Used when `isRemote` is absent. |

The root's `publisher`, `publisherurl` and `lastBuildDate` elements are optional: they help identify the file but do not change the jobs.

### Why include the job owner's email

The `email` element (`posterEmail` in the LinkedIn XML) is the work email of whoever is responsible for the job: the recruiter or the employer's HR team mailbox. It is optional, but worth it:

- **The job ranks higher.** The email adds points to the listing's quality score, like the salary and a complete description, and that score breaks ties among the jobs that match a search: with the email, the job moves ahead of equivalent ones without it. Showing up earlier usually brings more views and more applications.
- **There is someone to talk to before the job comes down.** When the job is reported for something that can be fixed (wrong details, a swapped link), the Joonow team writes to that email before deciding whether to take it down.
- **The email stays with the Joonow team.** It never appears on the job, in search, in the API or to candidates, it is not sold or used for marketing, and it is deleted 30 days after the job comes down ([privacy policy](/en/privacy-policy)).

Send one complete address per job (`recruiting@company.com`). "No-reply" mailboxes don't count. An invalid email is ignored without rejecting the job (it goes live without the point), and the [validator](/en/integrate-your-ats/validator) shows how many jobs include the email.

### Why include the company website, logo and description

`companyurl`, `companylogo` and `companydescription` build the page of each client company on Joonow, with the same value on all of its jobs:

- **The logo shows everywhere:** on the job card, in search, on the open job and in the company directory, and it goes into the job's JSON-LD for Google.
- **They help build the company page:** the website becomes the page's link once checked, the social profiles come from it, and the description goes, together with the company's own website, into the short text we write about it (we never publish the description as it came).
- **None of this changes the job:** website, logo and description belong to the company. A new logo doesn't rewrite the jobs, and an invalid value is ignored without rejecting the job.

The logo is downloaded once and served by Joonow (we never load images from your server on the page). If the company claims its page, what it writes replaces what came in the feed. The [validator](/en/integrate-your-ats/validator) shows how many jobs include the logo and the website.

### Accepted values

**`jobtype` (contract type):** `fulltime` (permanent, full time), `parttime` (part time), `contract` (contractor, freelancer), `temporary` (temporary), `internship` (internship) and `volunteer` (volunteer). The Brazilian Portuguese labels are accepted too: Efetivo – CLT, Estatutário, Estágio, Prestador de Serviços - PJ, Freelancer, Autônomo, Cooperado, Temporário, Tempo Determinado and Intermitente.

**`category` (level):** the codes `Apprentice`, `Intern`, `Entry` (early career), `Operational`, `Professional`, `Management` (coordination and management) and `Executive` (leadership), or the Portuguese labels Aprendiz, Estágio, Trainee, Operacional, Profissional, Supervisão/Coordenação and Gerencial e Executivos.

**Work model:** `isRemote` with `Yes` or `No`, or `remotetype` with `Fully remote`, `Hybrid remote` or `On-site`. "COVID-19" (only temporarily remote) gets no work model.

### What not to use

Our reader understands the `priority` element for compatibility, but it is not part of the program: sponsored jobs are not arranged through the feed. `phone` and internal ATS fields are ignored. Apart from the email of the person responsible for the job, do not send personal data in the feed (never candidate data).

## Indeed XML

If your ATS already publishes the standard Indeed XML, choose "Indeed XML" in the validator. The differences from Joonow XML:

- the id can come in `referencenumber` or `requisitionid`;
- the company can come in `company` or `sourcename`;
- `jobtype` in English (`fulltime`, `parttime`, `contract`, `temporary`, `internship`, `volunteer`, `per diem`);
- `remotetype` with `Fully remote` or `Hybrid remote`;
- `email` is read as the email of the person responsible for the job, used as in Joonow XML ([why include it](#why-include-the-job-owners-email));
- `category` and `experience` are ignored.

## LinkedIn XML

If your ATS already generates the LinkedIn jobs XML, choose "LinkedIn XML". We read:

| Element | Use |
|---|---|
| `partnerJobId` | Stable job id |
| `title`, `description`, `applyUrl` | Title, description and apply URL |
| `company`, `companyId` | Company; the group's `companyId` together with the name separates each brand |
| `city`, `state`, `country` | Location |
| `listDate`, `expirationDate` | Posting and expiration |
| `salaries/salary/lowEnd/amount`, `highEnd/amount`, `currencyCode`, `period` | Structured salary (`HOURLY`, `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY`) |
| `jobType` | `FULL_TIME`, `PART_TIME`, `CONTRACT`, `TEMPORARY`, `INTERNSHIP`, `VOLUNTEER` |
| `experienceLevel` | `INTERNSHIP`, `ENTRY_LEVEL`, `ASSOCIATE`, `MID_SENIOR_LEVEL`, `DIRECTOR`, `EXECUTIVE` |
| `workplaceTypes` | `On-site`, `Hybrid` or `Remote` |
| `posterEmail` | Email of the person responsible for the job, used like `email` in Joonow XML ([why include it](#why-include-the-job-owners-email)) |

## How we read the feed

- **Frequency:** every 6 hours by default. The schedule is agreed on approval, and the partner dashboard has a "Request a read now" button.
- **Conditional GET:** we send `If-None-Match` and `If-Modified-Since` with the previous read's `ETag` and `Last-Modified`. A `304` response ends the read without downloading anything; a file identical to the previous one also stores nothing.
- **Compression:** use HTTP compression (`Content-Encoding: gzip`, `deflate` or `br`). A downloadable `.gz` file is not accepted.
- **Size:** up to 2 GB uncompressed and 15 minutes of download.
- **Address:** a public URL, `http` or `https` (prefer `https`), up to 5 redirects. Internal network addresses are blocked.
- **Robot:** `JoonowFeedBot/1.0 (+https://www.joonow.com/robots.txt)`. If you have a firewall, allow this User-Agent.
- **XML:** UTF-8, **without** `<!DOCTYPE>` or external entities. A file with a DTD is rejected for security reasons.

## Job lifecycle

1. **New:** it goes in on the next read and shows up on the site within minutes.
2. **Changed:** we update it when the job's content changes (title, description, location, salary, dates…).
3. **Closed:** when it is missing from the file in **two reads in a row**, when its expiration (`expirationdate`) passes, or when the apply link is down in two checks 24 hours apart.
4. **Maximum age:** a job that is never renewed (its `date` never moves forward) is closed after 60 days; no job stays live for more than 180 days. To keep a job, republish it with a new date.
5. **After closing:** the page shows "job closed" with similar jobs and, after 30 days, answers 410 to search engines.

Always send the **complete inventory** on every read: a partial file closes the jobs that were left out.

## Safety valve

So that a broken feed does not take down your clients' jobs, the read is **quarantined** when:

- more than **5%** of the records are rejected; or
- the number of valid jobs drops more than **30%** compared to the previous read (with 50 jobs or more).

In quarantine nothing is published or closed, and the partner dashboard shows why. Fix the file and request a new read.

## Confidential company

A job without `company` shows as **Confidential**: it has no company page and is not in the directory. If your ATS puts its own name in place of the company on confidential jobs, tell us in the integration request: we treat that name as "Confidential", and your brand never shows up as the hiring company.

## Salary

- Send it only when the employer discloses it: **we never estimate salaries**.
- A single amount or a range, with currency and period: `US$ 4000`, `US$ 3800 to US$ 4200 per month`, `US$ 25/hour`.
- Write thousands without a separator or with a dot (`3800` or `3.800`): a comma is always read as the decimal mark, so `3,800` becomes 3.80.
- Without a period, we assume monthly. Jobs with a salary are included in the salary filter and in Google's structured data.

## Location and language

- **City and state** where the person will work. A fully remote job can come without a city, with `isRemote` = `Yes`.
- **Country** as a 2-letter ISO code (`BR`, `US`, `MX`…). Without a country, the feed's default country applies.
- **Language:** one feed per language (Portuguese, Spanish or English). The language is set on approval; for jobs in more than one language, send separate feeds.

## Applying

- Use the **direct job URL** on your ATS, with `https://`.
- No home page, job list, URL shortener, WhatsApp, Telegram or email: the job gets a warning or is rejected.
- We add `utm_source=joonow&utm_medium=referral` (if the URL already has `utm_source`, we keep yours). That way you measure applications coming from Joonow in your analytics.
- Want the application in your system without the candidate leaving Joonow? Turn on [applications by webhook](#applications-by-webhook).

## Quality and review

Each job goes through risk rules before going live: asking the candidate for payment, applying through a messaging app, personal email in the description, shortened links, very short descriptions, salaries far from the role's reality, among others. Above the limit, the job is **held for review** by the team and does not show up until approved. The validator shows these signals before the integration.

## Best practices for jobs to look good

- **Clear title** with just the role: "HR Analyst", not "JOB 123 – HR ANALYST URGENT!!!".
- **Complete description** (duties, requirements, benefits) in simple HTML with headings and lists. More than 200 characters.
- **Salary** whenever the employer allows it.
- **The job owner's email** on every job: it ranks higher and, if it gets reported, the team has someone to talk to.
- **Company website and logo** on every job (and the description, if you have one): the logo shows in search and they help build the company page.
- **Correct city and state**; remote jobs marked as remote.
- **Real dates** for posting and, when there is one, expiration.
- **Stable ids:** editing the job does not change the `referencenumber`.
- **One job, one record:** the same job in several cities becomes one record per city, each with its own id.

### On Google

Every Joonow job has its own page with `JobPosting` structured data built from your feed: `baseSalary` only with a disclosed salary, `validThrough` only when there is an expiration, remote `jobLocationType` only for fully remote jobs and the company name as `hiringOrganization`. Complete and correct data in the feed helps jobs show up in Google's job search.

## Applications by webhook

Besides sending jobs, your ATS can **receive applications** from Joonow. With the webhook on, people who apply to a job in your feeds do it on Joonow itself: they see the company, your system as the destination and the list of what will be sent, they confirm, and Joonow delivers the application to your system, which creates the record on behalf of the client company. Jobs from partners that didn't turn it on work as always: the candidate goes to the apply URL. The [ATS integration terms](/en/ats-integration-terms) apply.

### How to turn it on

In the partner panel, under **Applications**:

1. Accept the integration terms (the version, the date and who accepted are recorded).
2. Enter the HTTPS URL that receives deliveries: port 443, a domain name (not an IP) and no username or password in the URL. We don't follow redirects.
3. Generate the secret key. It's shown once: keep it in a secrets vault.
4. Send the test application (fake data, `"test": true`) and answer 2xx.
5. Turn receiving on. Changing the URL turns it off until a new test.

### The delivery

Each delivery is a `POST` to your URL, with `Content-Type: application/json; charset=utf-8`, a UTF-8 body and these headers:

| Header | Example | What it's for |
|---|---|---|
| `Joonow-Event-Id` | `0193f5c2-7b1e-7c4a-9d2e-5a1f0c3b8e21` | Unique delivery id (the same `id` as the body): your idempotency key. |
| `Joonow-Event-Type` | `application.submitted` | Event type (the same `type` as the body). |
| `Joonow-Timestamp` | `1793628191` | Signing time, in Unix seconds. |
| `Joonow-Signature` | `v1=5d4a…` | HMAC-SHA256 of the key over `{timestamp}.{body}`, in hex. |
| `User-Agent` | `JoonowWebhooks/1.0` | Identifies Joonow (not proof: the signature is the proof). |

Your server has 10 seconds to answer.

### Application body (`application.submitted`, version 1)

```json
{
  "id": "0193f5c2-7b1e-7c4a-9d2e-5a1f0c3b8e21",
  "type": "application.submitted",
  "version": "1",
  "created_at": "2026-11-02T14:03:11Z",
  "test": false,
  "data": {
    "application": { "id": "0193f5c2-7b1e-7c4a-9d2e-5a1f0c3b8e21", "submitted_at": "2026-11-02T14:03:11Z", "source": "joonow" },
    "job": {
      "external_id": "ACME-4821",
      "joonow_id": "k3j9x2m4pq",
      "title": "Junior Support Analyst",
      "company": "Acme Logistics",
      "url": "https://www.joonow.com/en/job/junior-support-analyst-k3j9x2m4pq"
    },
    "candidate": {
      "name": "Maria Souza",
      "email": "maria.souza@example.com",
      "phone": "+1 305 555 0100",
      "location": { "city": "Miami", "region": "FL", "country": "US" },
      "experiences": [
        { "title": "Customer service representative", "company": "Example Store", "start": "2023-04", "current": true, "summary": "Customer service and after-sales." }
      ],
      "education": [
        { "course": "Business Administration", "institution": "Example College", "level": "undergraduate", "start": "2019-02", "end": "2022-12", "current": false }
      ],
      "skills": ["Customer service", "Excel"],
      "languages": [{ "name": "Spanish", "level": "intermediate" }],
      "salary_expectation": { "amount": 3000, "currency": "USD", "period": "month" }
    },
    "answers": [{ "question": "Are you available to work on Saturdays?", "answer": "Yes" }],
    "resume": {
      "file_name": "maria-souza.pdf",
      "content_type": "application/pdf",
      "size_bytes": 182344,
      "url": "https://www.joonow.com/api/partners/v1/resumes/4bZ9qT…",
      "expires_at": "2026-11-05T14:03:12Z"
    }
  }
}
```

| Field | Always sent? | What it is |
|---|---|---|
| `id` | yes | Delivery id (UUID), equal to the `Joonow-Event-Id` header. |
| `type` | yes | `application.submitted`. |
| `version` | yes | `1`. New fields may appear without a version change; a breaking change becomes `2`, with 90 days' notice. Ignore what you don't know. |
| `created_at` | yes | When the candidate confirmed (ISO 8601, UTC). |
| `test` | yes | `true` for the panel's test application: answer 2xx, but don't create anyone. |
| `data.application.id` | yes | Application id (the same as the delivery). Use it to send the status back. |
| `data.application.source` | yes | `joonow`. |
| `data.job.external_id` | yes | The job id in your feed (`referencenumber`, `partnerJobId`). |
| `data.job.joonow_id`, `title`, `company`, `url` | yes | The job as it is on Joonow. |
| `data.candidate.name`, `email` | yes | Name and email of the candidate's account. |
| `data.candidate.phone` | if filled in | As the candidate typed it. |
| `data.candidate.location` | if filled in | `city`, `region` (state or province) and `country` (ISO 3166-1, 2 letters). |
| `data.candidate.links` | reserved | List of `{ "type", "url" }`. Not sent yet: it comes when the profile has links. |
| `data.candidate.experiences` | if filled in | `title`, `company`, `start` and `end` (`YYYY-MM`), `current`, `summary`. |
| `data.candidate.education` | if filled in | `course`, `institution`, `level` (`high_school`, `technical`, `undergraduate`, `postgraduate`, `masters`, `doctorate`, `course`), `start`, `end`, `current`. |
| `data.candidate.skills` | if filled in | List of strings. |
| `data.candidate.languages` | if filled in | `name` and `level` (`basic`, `intermediate`, `advanced`, `fluent`, `native`). |
| `data.candidate.salary_expectation` | if filled in | `amount`, `currency` (ISO 4217) and `period` (`hour`, `day`, `week`, `month`, `year`). |
| `data.answers` | yes (may be empty) | `question` and `answer`: the answers to the job's questions. |
| `data.resume` | if there is a resume | `file_name`, `content_type`, `size_bytes` and the file: `url` with `expires_at` (link) or `content_base64` (inside the delivery). |

Only what the candidate filled in and saw on the confirmation screen is sent: empty fields are left out (no `null` and no empty lists, except `answers`). Sensitive data (such as disability) is not sent in this version, and neither are the job's sensitive questions: the confirmation screen doesn't show them.

### Resume

- **Temporary link (default):** `data.resume.url` is a signed `GET` valid for 3 days from each delivery attempt. Download it right away and store it in your system. The link needs no login: treat it as a secret and don't log it. Once expired, or after the candidate deletes the account, it answers 404.
- **Inside the delivery:** choose it in the panel; the file goes in `data.resume.content_base64` (PDF or DOCX, up to 5 MB).

### Verifying the signature

1. Read the **raw** body, the bytes as they arrived, before parsing JSON.
2. Check that `Joonow-Timestamp` is at most 5 minutes from your clock, either way.
3. Compute `HMAC-SHA256(key, "{Joonow-Timestamp}.{body}")` in lowercase hex and prefix it with `v1=`.
4. Compare, in constant time, with each item of `Joonow-Signature` (comma separated). One match is enough: while rotating the key there are two, the new key's and the previous one's, for 24 hours.
5. No valid signature or outside the window: answer 401 and store nothing.

```js
import crypto from 'node:crypto';

function validSignature(rawBody, headers, key) {
  const t = headers['joonow-timestamp'] ?? '';
  if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = Buffer.from('v1=' + crypto.createHmac('sha256', key).update(t + '.').update(rawBody).digest('hex'));
  return (headers['joonow-signature'] ?? '').split(',').map((s) => Buffer.from(s.trim()))
    .some((s) => s.length === expected.length && crypto.timingSafeEqual(s, expected));
}
```

### Response, idempotency and retries

- Answer **2xx only after storing** the application. The response body is ignored.
- The same `id` may arrive more than once (a retry after a network failure, or the candidate resending an application that didn't arrive). If you already have that `id`, answer 2xx without creating it again.
- Any other answer, or none within 10 seconds, is a failure. A redirect (3xx) is a failure too.
- We retry with growing intervals (1 min, 5 min, 30 min, 2 h, 6 h and then every 12 h) up to 72 hours after the confirmation. Then we stop and email the candidate and your ATS contact.
- **410 Gone** means "this job no longer accepts applications": we stop right away and let people know.
- The panel shows each delivery, the attempts, the last response and the failures.

### Status back

So the candidate can follow along on Joonow, send each application's status to `https://www.joonow.com/api/partners/v1/events`, with `POST`, `Content-Type: application/json` and the same `Joonow-Timestamp` and `Joonow-Signature`, computed with your key the same way.

```json
{ "type": "application.in_review", "application_id": "0193f5c2-7b1e-7c4a-9d2e-5a1f0c3b8e21", "occurred_at": "2026-11-03T10:15:00Z" }
```

- `type`: `application.received` (stored), `application.in_review` (being reviewed) or `application.closed` (the process ended for the candidate). The status only moves forward: a late event doesn't move it back.
- Responses: `200` with `{"ok":true,"applied":true}` (or `false`, when nothing changed), `400` for an unknown event, `401` for a wrong signature, time or id, `413` above 16 KB and `429` when there are too many.
- The test application accepts the status too: use it to test the way back.
- The candidate sees the status only in their own area and gets an email when the application goes into review or is closed.

### Correction and erasure requests

When a candidate asks Joonow to correct or erase their data, we notify every system that received an application from them (LGPD, art. 18, § 6), through the same webhook, with the same signature and the same retries:

```json
{
  "id": "0193f611-2c40-7e2b-8a51-0d9e7f3a1c55",
  "type": "candidate.erasure_requested",
  "version": "1",
  "created_at": "2026-11-20T09:00:00Z",
  "test": false,
  "data": {
    "reason": "account_deleted",
    "candidate": { "email": "maria.souza@example.com" },
    "applications": [{ "id": "0193f5c2-7b1e-7c4a-9d2e-5a1f0c3b8e21", "job_external_id": "ACME-4821", "submitted_at": "2026-11-02T14:03:11Z" }],
    "respond_by": "2026-12-05T09:00:00Z"
  }
}
```

- `candidate.erasure_requested`: erase (or ask the client company to erase) what came from Joonow. `reason` is `account_deleted` (the candidate deleted the account) or `candidate_request` (asked through the privacy center).
- `candidate.correction_requested`: `data.candidate` carries the current data, in the same format as the application, to replace what you received.
- Pass it on to the client company and confirm within 15 days (`respond_by`) with an event back: `{ "type": "candidate.erasure_completed", "request_id": "<request id>" }` (or `candidate.correction_completed`).

### Security

- HTTPS only, with TLS 1.2 or newer. Reject deliveries without a valid signature or outside the 5-minute window.
- Keep the key in a secrets vault and rotate it in the panel if you suspect a leak.
- Don't log application bodies or resume links in open logs.
- Deliveries come from IP addresses that may change: trust the signature, not the IP.

## How to request the integration

1. Build the feed following this specification.
2. Paste the URL into the [validator](/en/integrate-your-ats/validator) and fix what it points out.
3. With the feed validated, fill in the request right there. The team reviews it within 2 business days.
4. Once approved, you get access to the partner dashboard by email, with reads, live jobs, rejections and clicks per month.

Technical questions? Talk to us through [Contact us](/en/contact#publishing).

