Skip to main content

CoverScale API

Generate release assets from one piece of artwork. Keys are minted in your CoverScale account under API access; this API is how an integration uses them.

Per-endpoint request and response schemas are in the API Reference. The same contract is served live at /v1/openapi.json.

Authenticating

Every endpoint takes an API key as a bearer token:

Authorization: Bearer cs_live_<key_id>_<secret>

An API key is the only credential this API accepts. There is no browser session, no ID token, and no App Check header — a request is authenticated entirely by the key in that one header, which is what makes it callable from your backend.

GET /v1/me confirms a key works and reports the scopes, per-minute budget, and tier it carries:

curl -s https://api.coverscale.app/v1/me \
-H "Authorization: Bearer cs_live_..."

Against staging, use https://api-staging.coverscale.app.

API tier

Access is granted per account, and by default an account has none — a key cannot be minted until it is. Two tiers:

  • basic — still asset types, and the ken_burns and split_layer motions. Every request stays on our own hardware, so this is the tier for an integration you are building or testing: no call ever reaches a video-generation provider.
  • full — every pipeline the API supports.

A request outside the account's tier is refused with 403 tier_forbidden before anything is charged. GET /v1/generations/{type}/estimate lists every price for that type and, for video, marks which motions this tier can submit.

Generating

There is one endpoint per asset type, each accepting only the fields that apply to it:

EndpointDelivers
POST /v1/generations/avatarsProfile artwork at every DSP's avatar size
POST /v1/generations/bannersHeader artwork for every DSP banner shape
POST /v1/generations/vinylA physical-media mockup of the cover
POST /v1/generations/spotify-canvasA 9:16 looping Spotify Canvas clip, up to 8 seconds
POST /v1/generations/youtube-visualizerA 16:9 visualizer, about 8 to 12 seconds, optionally muxed to a track
POST /v1/generations/apple-motion15-second Apple Music motion artwork, both shapes
POST /v1/generations/reelsA 15-second 9:16 clip for Reels and Shorts

Each returns immediately with a job. Poll GET /v1/jobs/{job_id} until status is succeeded, failed, or cancelled, or supply a webhook_url and be told.

Every endpoint has a GET /estimate twin — GET /v1/generations/spotify-canvas/estimate, and so on — which takes no body. Video types return one price per motion. Vinyl returns the front-only price and the price with a back cover. Avatars and banners have a single price. The artwork itself never changes the charge, and neither do prompt, audio_url, typography, vinyl color, or label art. high, calm, and custom_prompt are the same price.

curl -s https://api.coverscale.app/v1/generations/avatars \
-H "Authorization: Bearer cs_live_..." \
-H "Content-Type: application/json" \
-d '{"image_url":"https://example.com/cover.jpg"}'

POST /v1/jobs/{job_id}/cancel asks a job to stop. A job that has not started yet is cancelled and refunded immediately. A job that is already generating stops at its next stage boundary and is not refunded.

GET /v1/jobs lists this account's jobs, newest first, for reconciling against your own records or finding a job_id you never managed to store. It returns a summary per job — status, asset type, credits charged — and no files; fetch GET /v1/jobs/{job_id} for the signed outputs. Page with limit and starting_after:

curl -s "https://api.coverscale.app/v1/jobs?limit=20" \
-H "Authorization: Bearer cs_live_..."

Delivered files

A finished job carries outputs, one entry per platform spec. Each url is signed and short-lived; poll again for a fresh one rather than storing it.

Video outputs are silent by default. Each endpoint chooses the length: Spotify Canvas stays within 8 seconds, the YouTube visualizer is about 8 to 12 seconds, and Apple Motion and Reels / Shorts are 15 seconds. /v1/generations/youtube-visualizer is the exception on audio: supply its audio_url and the delivered video is looped and muxed to that track's own length instead.

/v1/generations/apple-motion delivers both a 1x1 and a 3x4 file from one generation and one charge.

Vinyl mockups

/v1/generations/vinyl composites your cover art into a physical-media mockup rather than resizing it. sleeve12 has no back cover. A back cover is a second render from its own source (so it bills a second credit). Custom label art is only accepted for a side that actually shows a label. A contradictory combination is a 400 invalid_option naming which field to change, before anything is charged.

Webhooks

When a job reaches a terminal state we POST the job to your webhook_url with two headers:

  • CoverScale-Eventgeneration.succeeded, generation.failed, or generation.cancelled
  • CoverScale-Signaturet=<unix>,v1=<hex>

Verify it by recomputing HMAC-SHA256 over "{t}.{raw_body}" with the webhook_secret you were given when the key was created, and comparing against v1 in constant time. Reject anything whose t is older than your chosen tolerance.

Webhook delivery is a convenience with bounded retries. Polling is the guaranteed path — if an endpoint of yours is unreachable, the asset was still produced and GET /v1/jobs/{job_id} will show it.

Retrying a submit

A submit charges. If a submit times out or the connection drops, you do not know whether it landed, and sending it again is a second charge for the same asset. There are two ways not to pay twice.

The plain one needs nothing from you: call GET /v1/jobs and look at the most recent job before resubmitting. Anything the first attempt created is there, whether or not you ever saw the response.

The other is an optional Idempotency-Key header — any string you can reproduce, usually a UUID your side already has:

Idempotency-Key: 3f1c0b2e-7a4d-4c1e-9b2a-6d5e8f0a1c23

A retry carrying the same key returns the original job instead of charging again, so the retry needs no lookup and no branch. Reusing a key with a different body, or against a different endpoint, is a 409 rather than a silent hand-back of an asset you did not ask for.

Everything else in this API is already safe to repeat: GET is a read, and cancelling an already-cancelled job returns it unchanged.

Errors

Every failure carries {"error": "<code>", "message": "<prose>"}. Branch on error; message is for humans and may change.

StatuserrorMeaning
401unauthorizedThe API key is missing, unknown, or revoked
429rate_limitedThe key's per-minute budget is spent
402insufficient_creditsThe account is out of credits
403api_access_disabled / tier_forbiddenThe account or its tier does not cover this request
422invalid_requestThe body does not match the endpoint's schema
400invalid_optionEvery field is valid but the combination is not (vinyl rules)

422 is a field that does not belong on that endpoint (vinyl_color on /avatars). 400 is a valid field whose combination is impossible. Both are raised before anything is charged.