# Authentication Every request carries an API key as a bearer token. Keys are created in the account area under **Settings → API keys**, with the **New key** button. The key is shown once, on creation, and only a hash is stored, so copy it there and then. A key looks like `12|kX9fQ2...`: a number, a pipe, then a long random string. Send all of it, number and pipe included, after `Bearer`. ```bash curl https://ydomain.com/api/v1/domains \ -H "Authorization: Bearer 12|kX9fQ2..." \ -H "Accept: application/json" ``` A missing or unknown key gives `401`: ```json { "message": "Unauthenticated." } ``` ## Abilities A key holds an explicit list of abilities. Every endpoint declares the one it needs, so a read-only key can never write, whatever it is pointed at. | Ability | Grants | | --- | --- | | `domains:read` | Read listings | | `domains:write` | Create, update and delete listings | | `leads:read` | Read leads and their message threads | | `leads:write` | Change lead status and reply to a buyer | | `webhooks:manage` | List, create and delete webhook endpoints | `GET /me` works with any valid key and reports the abilities the current key holds. Using an endpoint outside them gives `403`: ```json { "message": "Invalid ability provided." } ``` Give a key only what it needs. A script that imports listings does not need `leads:read`, and a reporting job needs nothing beyond the two read abilities. ## Expiry and revoking A key can be given an expiry when it is created; after that moment it is refused like an unknown key. Keys can be revoked one by one under **Settings → API keys**, or all at once under **Settings → Security**. Changing your password revokes every key as well. ## Rate limits - **120 requests per minute** per key. - Unauthenticated requests are limited to 20 per minute per IP address. Every response carries the usual headers: ```http X-RateLimit-Limit: 120 X-RateLimit-Remaining: 117 ``` Over the limit you get `429` with a `Retry-After` header in seconds: ```json { "message": "Too Many Attempts." } ``` Back off for that long rather than retrying immediately; retries inside the window count against the limit too. ## Pagination List endpoints are paginated. `per_page` defaults to 25 and is capped at 100. ```bash curl "https://ydomain.com/api/v1/domains?per_page=2&page=2" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json" ``` ```json { "data": [ { "domain": "example.net" }, { "domain": "example.org" } ], "links": { "first": "https://ydomain.com/api/v1/domains?page=1", "last": "https://ydomain.com/api/v1/domains?page=74", "prev": "https://ydomain.com/api/v1/domains?page=1", "next": "https://ydomain.com/api/v1/domains?page=3" }, "meta": { "current_page": 2, "from": 3, "last_page": 74, "per_page": 2, "to": 4, "total": 148 } } ``` Walk a list by following `links.next` until it is `null`, rather than counting pages yourself: a listing added while you are paging shifts the offsets. ## Errors | Status | Means | | --- | --- | | `401` | No key, an unknown key, or an expired one | | `403` | The key lacks the ability, or the record belongs to someone else | | `404` | No such record on your account | | `422` | The body failed validation | | `429` | Rate limited; wait for `Retry-After` | | `5xx` | Our side. Retry with backoff; a `POST` is not automatically idempotent | A `422` names every field that failed: ```json { "message": "The extension field is required.", "errors": { "extension": ["The extension field is required."], "price": ["The price field must be a number."] } } ``` Note that a record belonging to another account gives `404`, not `403`: the API does not confirm that an id exists elsewhere.