api
Authentication
One header, three accepted spellings, and a deliberate refusal to tell an attacker which kind of wrong a wrong key is.
1The header
Authorization: Bearer <your key>, on every /v1 request. Nothing at all on /public — a key there is neither required nor useful.
In depthWhy it exists, what it is not, what people get wrong
There is one credential and it is the key's secret. There is no client id, no secret pair, no OAuth flow, no token exchange and no expiry to refresh. A key is created on the dashboard, it works until you revoke it, and it identifies a plan and a meter.
The secret is shown exactly once, when the key is created. It is stored as a SHA-256 hash, so nobody can recover it — if you lose it, rotate the key and it keeps its identity, its plan and its usage history while getting a new secret.
Exact contractTypes, defaults, ranges, errors, edge cases
GET /v1/jobs?country=IE&limit=25 HTTP/1.1
Host: api.jobopportunitiesapi.org
Authorization: Bearer sk_live_your_secret_here
Accept: application/jsonThe key travels in the header, never in the query string. A key in a URL ends up in browser history, in proxy logs, in Referer headers and in anything that records URLs, and there is no parameter here that accepts one.
$ export JOA_KEY='your-api-key-here' $ curl -s -H "Authorization: Bearer $JOA_KEY" \ 'https://api.jobopportunitiesapi.org/v1/jobs?country=IE&limit=2' | jq '.data[].title' "Senior Software Engineer" "Customer Success Manager"
2Three accepted spellings, and why
Bearer <key>, bearer <key> and the bare <key> with no scheme are all accepted. Bearer is canonical; the tolerance exists for integration platforms.
In depthWhy it exists, what it is not, what people get wrong
| Header value | Accepted | Comment |
|---|---|---|
Bearer sk_… | Yes | The canonical form. Write this. |
bearer sk_… | Yes | RFC 7235 requires the scheme token to be matched case-insensitively anyway. |
BEARER sk_… | Yes | Same rule. |
sk_… | Yes | The bare key, no scheme. Shipped 2026-08-22. |
?key=sk_… in the query string | No | There is no such parameter, deliberately. |
X-Api-Key: sk_… | No | Not read. |
The bare form is not sloppiness, it is a measured response to how the key is actually pasted. Integration platforms — Clay, Bubble, n8n, and every other product with a “paste your API key” box — give the user one field and send its contents verbatim. Refusing that produced a 401 whose only clue was the error message, and generated support mail for a case where the user had done nothing wrong.
Both spellings hash to the same lookup, so nothing about the security boundary moves. Document and write Bearer; expect the bare form to work if a tool sends it.
Exact contractTypes, defaults, ranges, errors, edge cases
The secret is hashed with SHA-256 and compared against the stored hash. There is no prefix-based lookup shortcut that could leak timing information about which keys exist, and a revoked or expired key takes exactly the same path as an unknown one.
3What a rejected key looks like
Missing, unknown, revoked and expired keys all return the same 401 with the same body. The API will not tell a prober which kind of wrong they have.
In depthWhy it exists, what it is not, what people get wrong
Distinguishing “this key was valid once” from “this key never existed” is useful to exactly one person, and it is not you. So all four failures are identical: same status, same error code, same message.
The practical consequence is that when a 401 surprises you, the debugging order is: check the header spelling, check you are on api.jobopportunitiesapi.org, check the key on the dashboard, and only then suspect the key itself.
Exact contractTypes, defaults, ranges, errors, edge cases
$ curl -s https://api.jobopportunitiesapi.org/v1/jobs { "error": "unauthorized", "message": "Send your key as: Authorization: Bearer YOUR_API_SECRET — the key on its own is accepted too.", "docs": "https://jobopportunitiesapi.org/api" }
| Situation | Status | `error` | What to do |
|---|---|---|---|
No Authorization header | 401 | unauthorized | Add it. |
| Unknown secret | 401 | unauthorized | Check the dashboard. |
| Revoked key | 401 | unauthorized | Create or rotate a key. |
| Expired key | 401 | unauthorized | Same. |
| Valid key, endpoint not on your plan | 403 | upgrade_required | X-JOA-Required-Feature names the missing entitlement — delta_feed or bulk_export. |
| Valid key, month's records spent | 402 | record_quota_exhausted | Do not retry. See the record meter. |
| Valid key, too many requests | 429 | — | Wait Retry-After seconds. |
4Creating, naming, rotating and revoking
Keys are managed on the dashboard. Rotating replaces the secret and keeps everything else; revoking is permanent and the row is kept for audit.
In depthWhy it exists, what it is not, what people get wrong
- Create
- Create a free key on /dashboard, or buy a plan at /pricing. The secret is shown once.
- Name
- Keys can be named, so a list of them is readable. The name is yours and is not sent anywhere.
- Rotate
- Issues a new secret for the same key. The plan, the meter and the usage history stay attached. Use this after a leak or a lost secret — it is not the same as creating a new key.
- Revoke
- Immediate and permanent. The key row is kept rather than deleted, because usage attached to it has to stay auditable.
- Cancel
- Cancels the subscription behind a paid key. See Cancelling.
You can hold more than one key. The usual reason is to separate environments — a key for production and one for a staging job — so that revoking one does not stop the other. The record allowance is per key, not per account, so splitting also splits the meter.
Exact contractTypes, defaults, ranges, errors, edge cases
Key management is done through the website's session, not through the API key itself: the endpoints behind it (POST /public/auth/keys, POST /public/auth/keys/{id}/rotate, .../revoke, .../name) require the signed session cookie the magic link creates, and a key cannot manage itself. That is deliberate — a leaked key cannot be used to rotate away from you.
GET /v1/me is the one place a key can inspect itself: it returns the plan, the status, the per-day and per-minute limits, the created date, the key prefix and today's usage. It costs no records and is safe to poll.
This page was rendered 11 September 2026, 03:39 UTC. Every figure on it comes from the endpoint named beside it, and every published request is re-sent against the live API before this site is allowed to build. If something here is wrong, the thumbs-down above reaches a person.