Overview
The Content API turns a written brief and whatever source material you hand it into a finished, published piece of communication: a blog post, a one-page site, or a presentation deck. It is the public front door to the generation pipeline Jetski Bay runs internally — the same code path, reached with an API key instead of a browser session.
The base URL is https://content.jetskibay.com. Every request and every
response is application/json, UTF-8. The version lives in the path:
everything below is under /v1.
Nothing here is synchronous. Generating a piece takes minutes, so
the three create endpoints return 202 with a job to poll rather than
holding a connection open. There is no blocking variant to reach for, and that is
deliberate — an open socket for four minutes is a request you were going to lose.
Responses are additive: a future release may add fields, and a well-behaved client
ignores the ones it does not recognise. GET /v1 is the authoritative
description of what this deployment currently supports — if this page and that
response ever disagree, believe the API.
Authentication
Every metered endpoint takes a key, in either of two headers. Pick one; they are equivalent.
x-api-key: jbk_7c41f0a9d2e8b5163a0f4c7e
# …or, if your client prefers a standard scheme
Authorization: Bearer jbk_7c41f0a9d2e8b5163a0f4c7e
A request with no usable credential is rejected with 401 and carries the
challenge header
www-authenticate: Bearer realm="Jetski Bay Content API".
Three endpoints need no key at all —
GET /v1,
GET /v1/variants and
GET /v1/skills?variant=… —
so you can build a picker in your own UI before anyone has signed up.
/v1/skills is the one of the three that will not answer bare:
its variant query parameter is required.
Every path here serves a fixed set of methods — the three create endpoints
are POST only, everything else is GET, plus
DELETE on
/v1/keys/current. Anything else on a
path that does exist is 405
method_not_allowed, not a 404. And a deployment
with no key-signing secret configured answers 503
key_secret_unavailable on every keyed route before it looks at
your credential at all — that one is ours to fix, not yours.
Keys
Keys are minted per email address and mailed to it. A trial key is good for 10 requests and expires 24 hours after it is issued. Only a hash of the key is stored, so the email is the one place the key itself exists: if you lose it, mint another.
No key yet?
POST/v1/keys
Mints a key for an email address and mails it. No authentication — this is how you get authenticated in the first place. The response never contains the key.
curl -X POST https://content.jetskibay.com/v1/keys \ -H "content-type: application/json" \ -d '{"email":"you@company.com"}'
{
"status": "sent",
"message": "Your API key is on its way to you@company.com.",
"key": {
"keyPrefix": "jbk_7c41f0a…",
"tier": "trial",
"requestLimit": 10,
"requestsRemaining": 10,
"expiresAt": "2026-09-06T09:12:44.000Z"
}
}
Re-minting carries your allowance over and kills the old key. Ask for a key at an address that already has a live one and you get a fresh key with whatever requests were left on the old one — and the old one stops working immediately. Losing a key costs you nothing, and asking for a second one while the first is still alive buys you nothing — the arithmetic is the same either way. What bounds this in general is the mint cap: a limited number of keys per address, and per network, per day.
Your work belongs to the address, not to the key. A job — and any entity that job produced — is readable by any key minted to the same email address, not only by the exact key that started it. That is deliberate rather than sloppy: re-minting is the documented way to recover a lost key, and if ownership were bound to the key id, recovering would silently orphan every job in flight and every page you had already made. Same address, same work.
A key minted to a different address gets 404 on your
job ids, never 403 — a 403 would confirm the id
exists, which is exactly what a stranger should not learn. Nothing here
widens anything: a key is only ever delivered to the address it was minted
for, so holding the mailbox is already what holding the key proves.
The job contract
All three create endpoints behave identically. You POST a brief, you get
202 immediately, and you poll the URL it hands you until the job reaches a
terminal state.
{
"requestId": "9c2f7a41-6d0b-4f18-9a72-1b3e5c8d40aa",
"status": "queued",
"variant": "blog",
"statusUrl": "https://content.jetskibay.com/v1/jobs/9c2f7a41-…",
"pollAfterMs": 4000,
"contextRef": "contexts/4f0b…c71a/manifest.json"
}
Wait pollAfterMs milliseconds, then GET the
statusUrl. Keep waiting and polling while status is
queued or running. Do not invent your own interval that is
shorter than the one you were given — the number is there so we can slow you down
without breaking you.
| Status | Terminal | Means |
|---|---|---|
queued | no | Accepted and waiting for a worker. |
running | no | In progress. The phase field says where it is. |
succeeded | yes | Finished. The result is on the job. |
failed | yes | Did not finish. A plain-language reason is on the job. |
cancelled | yes | Stopped before it finished. |
unknown | no | The pipeline reported a state this version of the API does not recognise. It is surfaced as unknown rather than coerced into one of the five above, because a wrong guess would be worse than an honest one. Keep polling. |
The first five are the whole set. Treat anything else — including
unknown — as "still working" and keep polling until the
response says otherwise.
A terminal succeeded carries a result object:
entityId always, liveUrl for a blog post or a
page, downloadUrl for a deck. A terminal failed
or cancelled carries error instead. Only a
non-terminal response carries pollAfterMs, so its absence is
itself a signal that the job has stopped moving.
A create also honours an idempotency-key request header — one
to 180 visible characters. Send the same key twice with the same body and
the prepared context is reused rather than rebuilt, and the key travels on
to the pipeline so a retry does not fan out into parallel work. Send the
same key with a different body and the request is refused with
409 invalid_request. Omit it and one is generated
for you, which means a blind retry is a fresh run. Note that the header
does not make a retry free: quota is charged per request, before any of
this is looked at.
Ownership of what you made is recorded when you poll the finished job. A create does not know its own id until the pipeline picks one — a blog slug, a page id, a deck's durable name — so the ledger binds that id to you the first time you read the job in a terminal state.
The consequence is worth planning for rather than discovering: fire a
create, never poll it to succeeded, and you will not be able
to update or rewrite that entity later, or to
download that deck. Both answer 404, because as far as the
ledger is concerned nobody has finished making that thing. Poll every job
you start through to a terminal state, even when you do not need the
result at that moment.
A running job also reports the pipeline phase it is in, in this order:
prepare, source, generate,
validate, commit, publish. Phases are for
showing a human what is happening; branch on status, not on
phase.
Service index
GET/v1
Unauthenticated. Reports the API version, the variant list, the current trial terms, the list of endpoints this deployment serves, and the branding state of each generator. If you are writing a client, read this rather than hardcoding what this page says.
{
"service": "jetskibay-content",
"version": "v1",
"schemaVersion": 1,
"documentation": "https://content.jetskibay.com/docs",
"variants": ["blog", "page", "deck"],
"trial": {
"requestLimit": 10,
"windowHours": 24,
"deliver": "email"
},
"endpoints": {
"index": "/v1",
"variants": "/v1/variants",
"skills": "/v1/skills",
"keys": "/v1/keys",
"keyStatus": "/v1/keys/current",
"posts": "/v1/posts",
"pages": "/v1/pages",
"decks": "/v1/decks",
"jobs": "/v1/jobs/{requestId}",
"deckDownload": "/v1/decks/{id}/download"
},
"brandSource": {
"blog": "live",
"page": "live",
"deck": "unreported"
}
}
brandSource
Each generator resolves the Jetski Bay brand live at run time and falls back to a compiled-in snapshot when that fetch fails. Somebody working in the Show UI can see a degraded brand on the page in front of them. You cannot: you get a finished artifact with no way to tell which branding went into it. That asymmetry is the only reason this field is here.
| Value | Means |
|---|---|
live | That service resolved branding from the live source on its last check. |
snapshot | The live fetch failed and the compiled-in snapshot is in use. Output is still produced; it may be visually behind. |
unreported | The service answered but does not publish a branding state. deck currently reads this always — it is not a degradation, it is an absence of a signal. |
unavailable | The health probe itself failed or timed out. Says nothing about branding, only that we could not ask. |
The whole index response is cached for 60 seconds, because it takes no key
and three health probes per anonymous hit would be an amplifier rather than
a feature. So brandSource is up to a minute stale, and it is not
a monitoring endpoint — status.jetskibay.com
is.
Variants
GET/v1/variants
Unauthenticated. Returns the three variants — blog, page,
deck — with the modes each one accepts, the options each one honours,
where its output lands, and the endpoint each one is posted to. This is the honest
source for a variant picker: if we add options or change a mode, they show up here
first.
{
"variants": [
{
"id": "deck",
"label": "Presentation deck",
"endpoint": "/v1/decks",
"modes": ["create", "rewrite"],
"output": {
"kind": "download",
"downloadEndpoint": "/v1/decks/{id}/download",
"liveUrl": false
},
"options": {
"targetSlides": { "type": "integer", "minimum": 3, "maximum": 30 },
"aspectRatio": { "enum": ["16:9", "4:3"] },
"speakerNotes": { "type": "boolean" },
"audience": { "enum": ["general", "leadership", "project-team", "external"] },
"tone": { "enum": ["insightful", "direct", "optimistic"] },
"goal": { "enum": ["inform", "align", "persuade", "launch"] },
"density": { "enum": ["airy", "balanced", "detailed"] }
}
}
]
}
output.kind is "published" with
liveUrl: true for blog and page, and
"download" with liveUrl: false for
deck. Branch on that rather than hardcoding the one exception.
Modes are per variant
There is no universal set of modes. page takes
create and update; blog and
deck take create and rewrite. Sending
a mode a variant does not accept is a 400
invalid_request naming the variant — not a silent downgrade to
create.
| Variant | Modes | Needs an entityId |
|---|---|---|
blog |
create · rewrite |
For rewrite. |
page |
create · update |
For update. |
deck |
create · rewrite |
For rewrite. |
The modes array on this endpoint is the authoritative list — the
table above is a convenience, and if the two ever disagree, believe the API.
mode defaults to create when you omit it.
update and rewrite both require an
entityId, and that id must be one
a finished job of yours produced.
Skills
GET/v1/skills?variant=blog
Unauthenticated. A skill is a named behaviour the pipeline can switch on for a run —
a house style, a research habit, a structural pattern. Pass variant to get
the catalogue that applies to that variant, then send the ids you want as
skillIds on the request body.
Every variant already runs three skills you did not ask for — the ones marked
selected_by_default, which are also the ones marked
required. Those count against the ceiling of five skills per
run, so in practice you are adding at most two. Ask for a sixth, or for an id
that is not in this variant's catalogue, and the request comes back
400 bad_request saying which.
?variant= is required. Call
GET /v1/skills with no variant, or with anything
other than blog, page or deck, and
you get 400 invalid_variant. There is
deliberately no default: a skill catalogue for the wrong variant is a list
of ids the pipeline will refuse, which would surface as a far more
confusing 400 one request later.
{
"variant": "blog",
"skills": [
{
"id": "…",
"label": "…",
"description": "What this skill changes about the run.",
"variants": ["blog"],
"required": false,
"selected_by_default": false,
"version": "…"
}
]
}
Skill ids are not stable literals in this document on purpose — the catalogue moves. Read it at request time and send back ids you got from it.
Create
Three endpoints, one body. The variant is the path you post to, so you do not send a
variant field.
| Endpoint | Makes | Modes | Notes |
|---|---|---|---|
POST /v1/posts |
A blog post | create · rewrite |
Published to the blog with its own slug. rewrite starts the text over on a post you already made, and needs its entityId. |
POST /v1/pages |
A one-pager | create · update |
Send an existing entityId with mode: "update" to revise that page in place instead of creating a second one. A page does not take rewrite. |
POST /v1/decks |
A presentation | create · rewrite |
Delivered as a downloadable artifact, not a public URL. See where output lands. A deck does not take update. |
A contradicting variant is refused, not overridden.
Leave the field out, or set it to the variant the path already names. Post
{"variant":"blog"} to /v1/decks and you get
400 variant_conflict, because a client sending
that is a client that thinks it is posting something else — and silently
honouring the path is how you end up billed for a deck you asked to be a
blog post.
All three answer 202 with the job envelope described in the job contract. All three charge one request against your key at the moment work starts.
curl -X POST https://content.jetskibay.com/v1/posts \ -H "x-api-key: $JETSKI_KEY" \ -H "content-type: application/json" \ -d '{ "mode": "create", "brief": "How we cut cold-start latency on the fleet. Lead with the measurement, not the fix. Engineers, not executives.", "research": { "webSearch": true }, "options": { "targetWords": 900, "audience": "project-team", "tone": "direct", "goal": "inform" } }'
# Updating an existing one-pager in place: pass its entityId. curl -X POST https://content.jetskibay.com/v1/pages \ -H "x-api-key: $JETSKI_KEY" \ -H "content-type: application/json" \ -d '{ "mode": "update", "entityId": "pg_4f81c07a", "brief": "Refresh the numbers for Q3 and drop the pilot section.", "options": { "audience": "external", "density": "airy" } }'
curl -X POST https://content.jetskibay.com/v1/decks \ -H "x-api-key: $JETSKI_KEY" \ -H "content-type: application/json" \ -d '{ "brief": "Fifteen minutes on the migration, for the leadership review.", "research": { "webSearch": false, "urlCaptures": [ { "url": "https://example.com/postmortem", "includeText": true, "includeScreenshot": false } ] }, "options": { "targetSlides": 12, "aspectRatio": "16:9", "speakerNotes": true, "audience": "leadership", "goal": "align" } }'
The page example says "mode": "update" because that is the
follow-up mode a page has. The same shape against
/v1/posts or /v1/decks would say
"mode": "rewrite" instead — swapping the two is a
400, and one you have already paid for. See
modes.
Poll a job
GET/v1/jobs/{requestId}
Free — polling never consumes quota. A job is readable by any key minted to the
address that started it, which is not the same thing as the one key that started
it: see keys for why re-minting has to keep working. A key
belonging to a different address gets 404, not 403,
because whether that job exists is none of its business.
{
"requestId": "9c2f7a41-…",
"variant": "blog",
"status": "running",
"phase": "generate",
"createdAt": "2026-09-05T09:12:44.000Z",
"updatedAt": "2026-09-05T09:14:02.000Z",
"pollAfterMs": 4000
}
{
"requestId": "9c2f7a41-…",
"variant": "blog",
"status": "succeeded",
"phase": "publish",
"result": {
"entityId": "how-we-cut-cold-start-latency",
"liveUrl": "https://blog.jetskibay.com/posts/how-we-cut-cold-start-latency"
}
}
{
"requestId": "3b70e2c9-…",
"variant": "deck",
"status": "succeeded",
"phase": "publish",
"result": {
"entityId": "dk_1a9f0c",
"downloadUrl": "https://content.jetskibay.com/v1/decks/dk_1a9f0c/download"
}
}
A deck carries downloadUrl and no liveUrl; a blog
post or a page carries liveUrl and no downloadUrl.
Neither key is invented for the other variant, so result.liveUrl
being absent on a deck is the contract, not a missing field.
{
"requestId": "9c2f7a41-…",
"status": "failed",
"error": "The brief did not contain enough to write about."
}
A failed job is still an HTTP 200. The request to read the job
succeeded; the job is what failed. Branch on the status field, not on
the status code, or you will loop forever on a dead job.
Download a deck
GET/v1/decks/{id}/download
Returns the deck artifact for a finished deck job, authenticated with any key minted to the address that created it. This is the only way to get a deck out of the API — see where output lands for why. Free: the request that made the deck was already charged, and charging again for fetching the thing you paid for would make a retried download cost real money.
The {id} must be an entity one of your finished jobs
produced — which, as
the job contract says, means a job you polled through to
succeeded. Anything else is 404: an id you never
made, an id you made but never polled, and someone else's id are deliberately
indistinguishable from the outside.
curl -L https://content.jetskibay.com/v1/decks/dk_1a9f0c/download \
-H "x-api-key: $JETSKI_KEY" \
-o deck.html
Key status
GET/v1/keys/current
Reports the allowance and expiry of the key on the request. Free — checking how much you have left does not spend any of it. Useful as a cheap credential check before you commit to a run.
{
"key": {
"keyPrefix": "jbk_7c41f0a…",
"tier": "trial",
"requestLimit": 10,
"requestCount": 3,
"requestsRemaining": 7,
"createdAt": "2026-09-05T09:12:44.000Z",
"expiresAt": "2026-09-06T09:12:44.000Z",
"lastUsedAt": "2026-09-05T10:40:11.000Z"
},
"valid": true,
"reason": ""
}
An expired or exhausted key still answers here — that is the point of the
endpoint. valid goes false and reason
carries the code you would have got from a real call
(expired_api_key, quota_exhausted). Only a
credential we cannot resolve at all is an error instead.
DELETE/v1/keys/current
Revokes the key on the request, immediately and permanently. Also free. Use it the moment a key ends up somewhere it should not be — in a screenshot, a commit, a support ticket. Revoking does not stop you minting another at the same address.
Revoking costs you nothing and buys you nothing. The allowance belongs to the
address for a rolling 24 hours, not to whichever key happens to be
alive: a new key is issued for the trial limit minus everything that address
has already spent in the window, whether the key that spent it is live,
revoked, or expired. So re-minting over a working key and revoking first come
out at exactly the same number, and an address that has spent all ten gets
429 allowance_exhausted until the window rolls
forward. Mint often enough and you will meet the per-address and per-network
mint caps first, which are 429 mint_limit_reached
for the rest of the day.
Request body
The three create endpoints share one body: the platform's generation contract,
schema version 2. Everything is optional except brief.
| Field | Type | What it does |
|---|---|---|
brief |
string | Required. What you want made, in prose. This is the whole instruction — angle, audience, what to leave out. A vague brief produces vague work; the options tune it, they do not replace it. At most 40,000 characters. |
mode |
See modes | Defaults to create. Which modes exist depends on the variant — page takes create and update, blog and deck take create and rewrite. update revises an existing piece against the brief; rewrite starts the text over on the same entity. Both need an entityId. A mode the variant does not accept is a 400 invalid_request. |
entityId |
string · null | The thing to act on, required by update and rewrite. 1–180 characters of [A-Za-z0-9_.:-]; anything else is 400 invalid_entity_id. It must be an entity one of your own finished jobs produced, or you get 404 — a blog slug is on the open web and a page id is short, so without that check a guessed id would be a licence to rewrite somebody else's work. |
skillIds |
string[] | Extra named behaviours to switch on for this run. Three are always on for every variant and the ceiling is five in total, so at most two of these are yours. Read the ids from GET /v1/skills?variant=…. |
research.webSearch |
boolean | Let the pipeline search the open web while it works. Off by default. Turning it on makes a run slower and better-sourced. |
research.urlCaptures |
object[] | Specific pages to read, at most three. Each entry is { url, includeText, includeScreenshot } — an http(s) URL, text for the words, screenshot when the layout is the point. Both flags default to true. |
files |
object[] | Source documents inlined as { name, type, b64 }: a filename, a MIME type, and base64 content. At most ten files and about 20 MB of content, inside a request body capped at 32 MB. This is how you feed it a transcript, a spec, or a spreadsheet it could not otherwise reach. |
options |
object | The shaping controls, below. Every one of them is nullable and every one has a sane default. |
The options block
| Option | Values | Applies to |
|---|---|---|
targetWords | integer, 250–5000 | Blog, page. A target, not a contract. |
targetSlides | integer, 3–30 | Deck. |
audience | general · leadership · project-team · external | All three. |
tone | insightful · direct · optimistic | All three. |
goal | inform · align · persuade · launch | All three. |
density | airy · balanced · detailed | All three. How much goes on a page or a slide. |
aspectRatio | 16:9 · 4:3 | Deck. |
speakerNotes | boolean | Deck. |
Fields you will not need. schemaVersion is fixed at
2 and set for you. variant is implied by the path you post
to — send it only if it matches, or not at all
(a contradiction is a 400). The visual
style is fixed to the Jetski Bay system, so there is no theme to pass.
Two fields are refused outright, not ignored. The shared
generation contract has a vaultImageRefs array and an
options.addToVault switch. Neither is exposed on the public
API, and sending either is a 400
unsupported_field naming the field.
Refusing rather than stripping is the whole point. A silently discarded
vaultImageRefs would hand you a finished, billed artifact that
you believed contained your images and does not — a failure you would only
find by looking at the output closely, long after the request succeeded. An
error you cannot miss is the smaller cost.
Where output lands
Two of the three variants finish at a public address. One does not, and it is worth knowing which before you build a UI around it.
| Variant | Destination | How you reach it |
|---|---|---|
blog |
https://blog.jetskibay.com/posts/<slug> |
Public URL on the finished job. |
page |
https://page.jetskibay.com/p/<id> |
Public URL on the finished job. |
deck |
— | GET /v1/decks/{id}/download, with your key. |
A deck has no public URL. Presentations live behind a passphrase
gate, so there is no link we can hand you that someone else could open. A finished
deck job therefore does not carry a liveUrl, and we do not fabricate one
— the deck is reachable only as a downloadable artifact through
GET /v1/decks/{id}/download, authenticated
with the key that made it. If your product needs a shareable link, generate a
page, not a deck.
Errors
Every error, from every endpoint, is the same JSON shape:
{ "error": "…", "code": "…" }. The error string is written to
be shown to a person. The code is the stable thing to branch on.
One code carries a third field. invalid_request is what a body
the generation contract rejects comes back as, and it lists every problem it
found in an issues array — all of them at once, so you are not
fixing one field per round trip:
{
"error": "The request body is not valid: mode rewrite is not valid for page; options.tone must be one of insightful, direct, optimistic.",
"code": "invalid_request",
"issues": [
"mode rewrite is not valid for page",
"options.tone must be one of insightful, direct, optimistic"
]
}
| Status | Code | Means | Do |
|---|---|---|---|
| 400 | invalid_body |
The body was not a JSON object at all. | Send an object, and a content-type to match. |
| 400 | invalid_request |
The body is an object but the generation contract rejects it. Carries issues[], above. |
Read issues; fix them all before retrying, since the retry costs a request. |
| 409 | invalid_request |
An idempotency-key you already used, with a different body. |
Use a new key, or resend the original body. |
| 400 | variant_conflict |
The body set a variant that contradicts the path. |
Drop the field, or make it match the endpoint. |
| 400 | unsupported_field |
vaultImageRefs or options.addToVault — real contract fields, not exposed here. |
Remove the field. It is not being honoured quietly. |
| 400 | invalid_entity_id |
An entityId outside 1–180 characters of [A-Za-z0-9_.:-]. |
Send the id exactly as a finished job reported it. |
| 400 | invalid_variant |
GET /v1/skills with a missing or unknown ?variant=. |
Pass blog, page or deck. |
| 401 | missing_api_key |
No x-api-key and no bearer token on the request. |
Send the key. |
| 401 | invalid_api_key |
The credential is not one of ours. | Check for a truncated paste before you re-mint. |
| 401 | expired_api_key |
Past its 24 hours. | Mint another at the same address. |
| 401 | revoked_api_key |
Revoked — by you, or by minting a newer key for the address. | Use the newest key you were mailed. |
| 429 | quota_exhausted |
The key's requests are used up. | Wait for a new key, or ask us about production volume. |
| 429 | mint_limit_reached |
Too many key requests from one address or network. | Stop asking for keys; use the one you have. |
| 429 | allowance_exhausted |
This address has spent its whole trial allowance in the last 24 hours, so there is nothing left to issue a key against. | Wait for the window to roll forward, or talk to us about more than a trial. |
| 400 | invalid_email |
The address on POST /v1/keys is not deliverable-looking. |
Fix the address. |
| 502 | email_failed |
The key was minted but the mail could not be sent — so the key was revoked again rather than left alive behind an address nobody reads. | Retry. It is our failure, not yours, and it cost you no allowance. |
| 404 | not_found |
An unknown path under /v1; a job or entity id that does not belong to your address; an entity from a job you never polled to a terminal state. |
Check the path against GET /v1, and check you polled the job that made it. |
| 405 | method_not_allowed |
The path exists but not with that method. | POST to create, GET everything else. |
| 400 / 413 | bad_request |
A malformed idempotency-key, a body over the 32 MB ceiling, or a skillIds selection that cannot be resolved. |
Read the message — it names the header, the limit, or the skill. |
| 4xx | rejected |
The generator itself refused the request, and its own status is passed through. | Read the message — it comes from the service that said no. |
| 5xx | upstream_error |
A generator failed, timed out, or accepted the work without returning a job. Refunded automatically. | Retry. If it persists, check status. |
| 500 | internal_error |
We broke in a way we had no name for. Refunded automatically. | Retry, and quote the x-request-id if you write to us. |
| 503 | key_secret_unavailable |
This deployment has no key-signing secret, so it can neither issue nor verify keys. | Nothing you can do. Tell us; every keyed route is down. |
| 503 | keys_unavailable |
The key store is not configured on this deployment. | Same: ours to fix. |
| 503 | delivery_unavailable |
No mail binding, so a minted key could not be delivered — and it is never returned in the response instead. | Same: ours to fix. |
| 503 | api_not_configured |
Nothing on this deployment can authenticate anybody. | Same: ours to fix. |
| 401 | — | Any 401 also carries www-authenticate: Bearer realm="Jetski Bay Content API". |
— |
Every response, including every error, carries an x-request-id.
It is the one thing worth logging on a failure and the one thing worth
quoting to us.
Quota rules
A trial key is 10 requests over 24 hours. The rules for what counts are short, and they are worth reading once rather than discovering.
- A request is charged when work starts, not when it finishes. Posting a brief spends one, whether the result delights you or not.
- Refunds happen only when we 5xx. If the platform itself fails, the request goes back on your allowance automatically. Nothing to claim.
- Your own 4xx still costs a request once work has started. Validate the body before you send it.
-
Polling is free, always.
GET /v1/jobs/{id}costs nothing, no matter how many times you call it. So doGET /v1/keys/currentandDELETE /v1/keys/current, and so do the three unauthenticated catalogue endpoints. - Re-minting never tops you up. A new key carries the remainder of what its address has spent in the last 24 hours and revokes any predecessor — and revoking first changes nothing, because spend counts against the address whether or not the key that spent it still exists. Losing a key costs nothing; deleting one buys nothing.
-
There is no remaining-quota response header. Nothing here
emits one, and a browser client could not read one if it did: the only
response headers exposed across origins are
x-request-idandretry-after. AskGET /v1/keys/currentinstead — it is free, it is authoritative, and polling it costs nothing.
Need more than a trial — real volume, a longer-lived key, your own vault projects? Say so at hello@jetskibay.com, or go and break the playground first.