# Employer opt-out and takedowns

How a site owner removes themselves, why the removal is verified rather than taken on trust, and how it propagates to every endpoint.

**What this covers:** Removing your site; How it propagates; Corrections and takedowns for a single listing.

**Assumed knowledge:** Nothing.

**Canonical HTML:** https://jobopportunitiesapi.org/docs/ledger/optout  
**Machine-readable index:** https://jobopportunitiesapi.org/docs/ai/index.md  
**Last verified:** 2026-08-22  
**Superseded by:** the live API at https://api.jobopportunitiesapi.org and its spec at https://jobopportunitiesapi.org/openapi.json — where this file and the API disagree, the API is right.

---

<a id="optout-how"></a>

## 1. Removing your site

Go to /optout, enter your domain and an email address at that domain, and return the six-digit code. The removal takes effect immediately.

### 1.1 In depth

Vacancies here are read from employers' own public careers pages and from the systems they publish into, and we send candidates to the employer's own application page. The default is therefore to include. An owner who objects has to be able to act immediately, though — not email somebody and wait — so the removal is self-service and takes effect the moment it is verified.

It is verified because an unverified opt-out form is a denial-of-service button: anyone could remove a competitor's listings with a form submission. So a request is only honoured once a code sent to an address **at that domain** is returned, which is something only someone controlling the domain can do.

> **Scope of the removal** — The domain is excluded from future fetching, and its rows are removed from this ledger and from the public API. They are retained inside the parent product (Erioun), because the objection is to redistribution rather than to a candidate finding your vacancy. If you want something different from that, say so at [/contact](https://jobopportunitiesapi.org/contact).

### 1.2 Exact contract

1. `POST /public/optout/request` with the domain and an email address on it. The domain is normalised first — scheme, `www.`, path, port and query are all stripped, and it is lowercased — so `https://www.Acme.com/careers` and `acme.com` are the same request.
2. A six-digit code is generated with a cryptographic random source (a broken RNG fails the request rather than falling back to something guessable) and mailed to the address. Only its salted hash is stored, salted with the domain so a leaked hash cannot be replayed against a different one.
3. `POST /public/optout/verify` with the domain and the code. Five attempts, then the code is dead. The code expires after 30 minutes, and a fresh one cannot be requested more often than every 2 minutes.
4. On success the domain is marked opted out. Its rows move to `optout_hidden` in the withheld population and stop being served everywhere.

The email address must be **at the domain being removed** — that is the whole verification. An address at a different domain is refused, not silently accepted.

**See also**

- [How it propagates](./ledger-optout.md#optout-propagation)
- [Withheld rows — held, not served](./ledger-data-model.md#withheld)
- [Every form, and where it posts](./website-pages.md#site-forms)

<a id="optout-propagation"></a>

## 2. How it propagates

Opted-out rows leave every endpoint at once — live, closed and the change feeds — and no parameter re-admits them.

### 2.1 In depth

A removal that leaked through one endpoint would not be a removal. So it is applied at the serving predicate rather than per handler: the rows are excluded from the served set itself, which every listing endpoint, every detail endpoint and every feed reads from.

If you are mirroring this data, an opt-out reaches you as a `withdrawn` change on `/v1/changes` — the same kind used when we withdraw a row for any other reason. Treat it as you treat `delisted`: stop serving the row. That is the mechanism by which somebody else's opt-out becomes your obligation too, and it is why an integration that ignores unknown `change` values is a problem rather than an inconvenience.

### 2.2 Exact contract

| Surface | Effect of an opt-out |
| --- | --- |
| `/v1/jobs`, `/public/jobs` | Rows disappear. They are counted in `withheld_reasons.optout_hidden`. |
| `/v1/jobs/{id}`, `/public/jobs/{slug}` | 404. |
| `/v1/jobs/closed`, `/v1/jobs/expired` | Rows disappear from the closure feeds as well. |
| `/v1/changes` | The row is reported once with `change: "withdrawn"`, then never again. |
| `/v1/companies`, `/public/companies` | The company's `open_roles` falls; the company disappears once it has none. |
| `/v1/export` | Rows are not written into the stream. |
| The website | `/job/<slug>` and `/company/<slug>` 404, and the sitemap stops listing them. |
| Future fetching | The domain is not re-fetched, so the rows do not come back on the next refresh. |

> **There is no parameter that re-admits an opted-out row** — `quality=all` re-admits gated rows and nothing else. `include_poster_type` re-admits agency and board postings and nothing else. Neither touches `optout_hidden`, and that is not an oversight.

**See also**

- [The four change kinds — and the one that breaks integrations](./endpoints-changes.md#change-kinds)
- [Withheld rows — held, not served](./ledger-data-model.md#withheld)
- [GET /v1/changes](./endpoints-changes.md#endpoint-changes)

<a id="takedowns"></a>

## 3. Corrections and takedowns for a single listing

One wrong row does not need a domain-wide opt-out. Write to us with the listing URL and what is wrong with it.

### 3.1 In depth

Opting out removes everything from a domain, which is the right response to “do not carry us” and the wrong response to “this one row is stale” or “this salary is wrong”. For the second kind, the correction route is [/contact?topic=data](https://jobopportunitiesapi.org/contact?topic=data), which reaches a person rather than a queue.

The most useful thing to send is the listing URL on this site (or its `id`) and the specific field that is wrong. If the row is stale — the vacancy is filled — saying so gets it closed with a reason rather than deleted, which is the correct outcome for a ledger.

### 3.2 Exact contract

- **A whole domain** — [/optout](https://jobopportunitiesapi.org/optout) — self-service, verified, immediate.
- **One listing, wrong data** — [/contact?topic=data](https://jobopportunitiesapi.org/contact?topic=data).
- **One listing, already filled** — Same route. It will be closed with a reason, not deleted — see [Closed roles](./ledger-data-model.md#closed-roles).
- **A legal request** — [/contact](https://jobopportunitiesapi.org/contact), or `support@jobopportunitiesapi.org`. Both are monitored; the address is published on every page of the site.
- **Privacy** — There is no personal data in the ledger — a row names a company, not a person. [The privacy policy](https://jobopportunitiesapi.org/privacy) covers what the website itself collects.

**See also**

- [Removing your site](./ledger-optout.md#optout-how)
- [The legal pages](./website-legal.md#site-legal-pages)

---

## Where to go next

This file is part of **The ledger**. Others in the same group:

- [What the ledger is](./ledger-what-joa-is.md) — Employer-direct openings, kept as a record rather than a feed. What that phrase actually commits us to, and who it suits.
- [The data model](./ledger-data-model.md) — live + withheld + closed = ledger_rows, and it reconciles exactly. Almost every misunderstanding about this product traces back to this one equation.
- [Provenance](./ledger-provenance.md) — Every field on every row says whether the source published it, whether we inferred it, or whether it is absent. This is the most distinctive thing in the product.
- [Sourcing and refusals](./ledger-sourcing.md) — The three source classes, the providers inside each, the redistribution flag that gates them, and the sources we fetch for ourselves and never republish.
- [Coverage and honesty](./ledger-coverage.md) — How to read /public/coverage, why the weak numbers are published as prominently as the strong ones, and what measured_at and stale actually mean.

Always useful:

- [index.md](./index.md) — the map of every file here
- [BUILD-A-SITE.md](./BUILD-A-SITE.md) — the paste-whole brief for building against this API
- [quickstart.md](./quickstart.md) — zero to a first authenticated response
- [api-errors.md](./api-errors.md) — every status code and whether to retry it
