Skip to content
Developers

API integration guide

Upload, list, embed and play media with deployment-specific API examples.

On this page

Before you start

Use the API from your server to upload media, read your library and create playback links. The examples below use this deployment’s API and playback addresses. Request and response bodies are JSON unless an endpoint documents a different format; file bytes go directly to storage.

SettingValue
API base addresshttps://lanternaut.com/v1
Playback domainhttps://media.lanternaut.com
API keyCreate one in API keys. Send it as Authorization: Bearer lnt_….
Set your server-side environment
export LANTERNAUT_API="https://lanternaut.com/v1"
export LANTERNAUT_KEY="lnt_..." # Replace with your API key.
ScopeAllows
assets:readRead assets, thumbnails, analytics and settings, and create playback or embed links.
assets:writeRead assets, upload, encode, update and delete media, and make thumbnails, GIFs and stickers.
usage:readRead account usage and balance.
settings:writeChange allowed playback domains and the defaults for new uploads.
billing:writeCreate checkouts and purchase wallet credit.

The hosts a viewer’s browser contacts. Every URL the API returns for viewers is on the playback domain: HLS playlists and segments, captions, posters, image links and the short-lived links they redirect to, embed pages and the player module. It is the only host to allow in a Content-Security-Policy:

DirectiveAllow
frame-srchttps://media.lanternaut.com — embeds
connect-src · media-srchttps://media.lanternaut.com (and blob: in media-src) — your own player fetching HLS
img-srchttps://media.lanternaut.com — images and posters
script-srchttps://media.lanternaut.com — only if you load the Lanternaut player module

Inside an embed (its own document, not your page) the player may load Google’s Cast library from www.gstatic.com when a viewer casts. The API address is for your server only. An older *.workers.dev address still answers so that embeds made before the playback domain existed keep working; don’t build on it, and treat any other host in a link as an error.

DefaultValue
Playback token lifetime6 hours, unless you send ttlSeconds (at most 24 hours). Read expiresAt in the answer: an operator can change the default.
API rate limit300 requests a minute per key and 1,200 per account. Past either: 429 with Retry-After.
Upload part URLsValid for one hour each.
Unfinished uploadsClosed after 48 hours, with an upload.expired webhook.

Full endpoint definitions are available in OpenAPI 3.1. For dashboard instructions, visit the documentation home. GIF, sticker and thumbnail tools are available in Creator Studio, and their files can be hosted through the API too (see below).

List your videos and images

GET /assets returns the newest assets first, with 50 per page by default. Set limit up to 200. Pass the response’s nextCursor as cursor to fetch the next page, until it is null.

List ready videos
curl --fail-with-body -sS "$LANTERNAUT_API/assets?status=ready&kind=video" \
  -H "Authorization: Bearer $LANTERNAUT_KEY"
Example response
{
  "assets": [
    {
      "id": "01J9Z…",
      "title": "Launch film",
      "kind": "video",
      "status": "ready",
      "durationSeconds": 134.2,
      "width": 3840,
      "height": 2160,
      "statusDetail": {
        "playable": true,
        "message": "Ready to play.",
        "nextStep": "none"
      }
    }
  ],
  "nextCursor": null
}

Use kind=image for images, or omit kind for both. A status filter can contain several states, such as status=queued,processing. Set project_id=main or a project ID to select a location. A project-restricted key only sees its own location.

statusDetail.nextStepWhat to do
noneThe asset needs no action.
waitProcessing is in progress; check again later.
finish_uploadSend the remaining parts and complete the upload.
encodeStart or retry with POST /assets/{id}/encode.
top_upAdd wallet credit, then request an encode.
upload_againUpload a new source file.
contact_supportContact support with the asset ID and request ID.

Read statusDetail.message for the explanation. For background integrations, subscribe to asset.ready and asset.errored webhooks instead of continuously polling. Webhook configuration requires an account-wide key.

statusMeans
createdJust made; an upload is being opened for it.
uploadingParts are being sent. Closed after 48 hours.
uploadedStored, not encoding: sent with encode:false, or its encoded files were removed.
queuedWaiting for an encoder.
processingEncoding.
readyPlayable.
erroredFailed; errorReason says why. Encode again, or upload again.
blockedRefused by moderation. Not charged, and cannot be played.
awaiting_fundsThe wallet did not cover the encode. Top up, then POST /assets/{id}/encode.
deletedGone. Listed only when asked for by id or with updated_since.

Polling many assets at once. ids= takes up to 200 comma-separated asset IDs and answers for all of them in one request, deleted ones included. updated_since= (an ISO 8601 time) lists only what changed since then, deleted assets included. Every list answer has asOf: keep the asOf of the first page and send it as the next poll’s updated_since. It is set a little before the read, so a poll can repeat an asset; compare updatedAt.

What changed since the last poll
curl --fail-with-body -sS -G "$LANTERNAUT_API/assets" \
  --data-urlencode "updated_since=2026-10-24T08:00:00Z" \
  -H "Authorization: Bearer $LANTERNAUT_KEY"

A video with an encode under way carries encoding in the list, as in GET /assets/{id}: its stage, percent, etaSeconds and watchableSeconds. Otherwise it is null.

Embed a video

Request a ready video’s embed once, then paste the returned html into your site. The embed address has no scheduled expiry and obtains fresh playback tokens for viewers. Account status, deletion and allowed-domain restrictions still apply.

Get an embed
# Replace this value with a ready video ID from your library.
ASSET_ID="your-video-asset-id"
curl --fail-with-body -sS "$LANTERNAUT_API/assets/$ASSET_ID/embed" \
  -H "Authorization: Bearer $LANTERNAUT_KEY"
Example embed response
{
  "url": "https://media.lanternaut.com/e/01J9Z…?a=…&s=…",
  "posterUrl": "https://media.lanternaut.com/poster/01J9Z…?a=…&s=…",
  "html": "<iframe …></iframe>"
}

posterUrl never expires either: it always shows the video’s current poster (the thumbnail you chose, or a frame from the video), only on your allowed domains. Use it for a list of lessons or a link preview. Change the poster with PATCH /assets/{id}/thumbnail.

Append these options to the returned URL without changing its signed parameters:

URL optionBehavior
&autoplay=1&muted=1Start automatically when the browser permits muted autoplay.
&loop=1Restart when the video ends.
&background=1Muted, autoplaying and looping; fills its container without controls.
&color=ff0066The accent colour, for this embed.
&title=0Hide (or =1 show) the title; also byline, portrait, logo.
&controls=0No controls; also hide one: progress_bar, volume, cc, speed, quality_selector, pip, fullscreen, chapters, transcript.
&t=1m30sStart at that moment.
&skipping_forward=0Viewers can’t skip past what they have watched.
&max_quality=1080pCap (or floor, with min_quality) the stream the player picks.
&dnt=1No viewing statistics, nothing remembered.

These are Vimeo’s names, so an embed moved over from Vimeo keeps its look. The full list, and the look for every video, are under Customise the player.

Preserve the iframe’s allow="autoplay; fullscreen" attribute. For interactive videos, use a descriptive iframe title so viewers using assistive technology can identify the player.

Customise the player

The dashboard’s Player page sets how every video looks: colours, which controls show, the title, your name and picture, your logo, how it starts and what shows when a video ends. Each video’s page adds its chapters, cards, its own end screen and an email form. The API changes the same settings, and embeds already on your sites update within a minute.

Brand colour, title on, your logo
curl --fail-with-body -sS -X PATCH "$LANTERNAUT_API/player" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" -H "Content-Type: application/json" \
  -d '{"colors":{"accent":"#ff0066"},"show":{"title":true},
       "logoImageId":"your-image-asset-id","logoUrl":"https://example.com"}'
Chapters and an email form on one video
curl --fail-with-body -sS -X PATCH "$LANTERNAUT_API/assets/$ASSET_ID/player" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" -H "Content-Type: application/json" \
  -d '{"chapters":[{"start":0,"title":"Intro"},{"start":"1:30","title":"Setup"}],
       "leadForm":{"at":"end","heading":"Get the next lesson","askName":true}}'

One look per project. Your account’s look is the global one: every video plays with it unless its project has a look of its own. PATCH /projects/{id}/player takes the same settings and gives the project its own look, starting from the account’s; GET on the same address says which look the project uses (own), and DELETE puts it back on the account’s. In the dashboard, pick the project at the top of the Player page.

A different accent colour for one project
curl --fail-with-body -sS -X PATCH "$LANTERNAUT_API/projects/$PROJECT_ID/player"   -H "Authorization: Bearer $LANTERNAUT_KEY" -H "Content-Type: application/json"   -d '{"colors":{"accent":"#2f80ed"}}'

Answers to email forms are at GET /leads, and each new one fires the lead.captured webhook. The whole list of settings is in the API reference.

Use a background video

Place the iframe behind your hero content. Add background=1 to the embed URL returned by the API. In this example, replace the sample src with that complete signed URL.

Hero section with background video
<section class="hero">
  <iframe class="hero-video"
          src="https://media.lanternaut.com/e/01J9Z…?a=…&s=…&background=1"
          title="" allow="autoplay; fullscreen"
          tabindex="-1" aria-hidden="true"></iframe>
  <div class="hero-text"><h1>Your headline</h1></div>
</section>

<style>
  .hero { position: relative; min-height: 80vh; overflow: hidden; }
  .hero-video { position: absolute; inset: 0; width: 100%; height: 100%;
                border: 0; pointer-events: none; }
  .hero-text { position: relative; z-index: 1; }
</style>

The player crops the video to fill the section and adapts stream quality to its displayed size and available bandwidth. pointer-events: none keeps buttons over the background clickable.

Show a responsive image

The embed endpoint also works for ready images. It returns a permanent link for each available width and an <img> with srcset, allowing the browser to choose the appropriate size.

Get an image embed
ASSET_ID="your-image-asset-id"
curl --fail-with-body -sS "$LANTERNAUT_API/assets/$ASSET_ID/embed" \
  -H "Authorization: Bearer $LANTERNAUT_KEY"
Example image response
{
  "url": "https://media.lanternaut.com/image/01JA2…/1920.webp?a=…&s=…",
  "html": "<img …>",
  "variants": [
    { "width": 320, "height": 180, "url": "…" }
  ]
}

Each permanent link redirects to a short-lived image address and respects allowed domains. Keep the generated srcset, adjust sizes to match your layout, and set descriptive alt text. GIFs and stickers saved as hosted images use the image delivery flow once processing is complete.

To make an image in the dashboard, open Creator Studio. A local download must be saved to your library before it has a hosted embed URL.

Use playback links with your own player

Mint a short-lived playback link on your server with POST /assets/{id}/playback-token. Video links are HLS streams suitable for hls.js, Video.js or Safari’s native player. Return the playback URL to your browser, keeping the API key on the server.

Create a one-hour playback link
ASSET_ID="your-video-asset-id"
curl --fail-with-body -sS -X POST "$LANTERNAUT_API/assets/$ASSET_ID/playback-token" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ttlSeconds": 3600, "domains": ["www.example.com"]}'
Example playback response
{
  "token": "eyJ…",
  "url": "https://media.lanternaut.com/v/01J9Z…/master.m3u8?token=eyJ…",
  "posterUrl": "https://media.lanternaut.com/v/01J9Z…/…/poster.jpg?token=eyJ…",
  "expiresIn": 3600,
  "expiresAt": "2026-10-10T19:00:00.000Z",
  "domains": ["www.example.com"]
}
FieldBehavior
ttlSecondsPositive integer, up to 86,400 seconds. Omitted: the current deployment default.
domainsOptional hostnames. These can narrow the account’s allowed domains, never widen them.
expiresAtRenew the link before this timestamp if playback needs to continue.

posterUrl is the video’s poster on the same lifetime (null while a first encode is under way). Renewing without a rebuffer: every playlist and segment request is checked on its own, and any valid token for the same video is accepted. So keep playing and swap the token in each request: fetch a new link from your server before expiresAt, and have hls.js put its token on every request from then on. Nothing reloads and the position is kept. (Safari’s built-in player can’t do this; give it a lifetime longer than the lesson.)

hls.js: use the newest token on every request
let token = firstLink.token; // from your server
const hls = new Hls({
  xhrSetup(xhr, url) {
    xhr.open('GET', url.replace(/([?&]token=)[^&]*/, (_, key) => key + encodeURIComponent(token)), true);
  },
});
hls.loadSource(firstLink.url);
hls.attachMedia(video);
// A few minutes before expiresAt, ask your server for a new link.
async function renew() {
  const next = await fetch('/my-server/playback-link?video=' + videoId).then((r) => r.json());
  token = next.token;
  setTimeout(renew, Date.parse(next.expiresAt) - Date.now() - 5 * 60_000);
}
setTimeout(renew, Date.parse(firstLink.expiresAt) - Date.now() - 5 * 60_000);
Play the URL returned by your server
<video id="v" playsinline controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
  // Set playbackUrl to the URL returned by your own server.
  const playbackUrl = "PASTE_PLAYBACK_URL_HERE";
  const video = document.getElementById('v');
  if (Hls.isSupported()) {
    const hls = new Hls({ capLevelToPlayerSize: true });
    hls.loadSource(playbackUrl);
    hls.attachMedia(video);
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = playbackUrl;
  }
</script>

The Lanternaut player is also available as an ES module at https://media.lanternaut.com/player/lanternaut-player.js. It exports createPlayer(element, { src, poster, background, autoplay, muted, loop }).

A ready image’s playback-token response includes variants and a URL for the widest image. A video’s first encode can return an early HLS playlist once some footage is watchable; otherwise an unready asset returns 409 not_ready. Embeds require the asset to be ready.

Upload media directly to storage

Upload media in parts directly to storage. The API creates the upload and signs each part; media bytes do not pass through the API. You need an assets:writekey, an allowed upload destination and available storage.

AcceptedFormats
VideoAny video/… content type: MP4, MOV, WebM, MKV, AVI, MPEG-TS, 3GP and more, in H.264, HEVC (H.265), VP8, VP9, AV1 or ProRes.
Imagesimage/jpeg, image/png, image/webp, image/gif (the first frame), image/avif, image/tiff.
Not acceptedHEIC/HEIF photos (convert to JPEG first), SVG, sound-only files, documents.

POST /uploads checks content_type before anything is sent and answers 415 unsupported_format, with what is accepted, for anything else. With a generic application/octet-stream the file name’s extension decides. The encoder reads the file itself to be sure, and an upload it cannot read ends errored without charge.

  1. Start the upload

    Send the filename, byte size and content type to POST /uploads. Save the returned assetId, partSize and partCount.

  2. Sign and upload each part

    For part numbers starting at 1, call POST /uploads/{assetId}/parts/{partNumber}. PUT that part’s bytes to the returned URL.

  3. Complete the upload

    After every part succeeds, call POST /uploads/{assetId}/complete with {}. The server lists and assembles the stored parts.

Run this example in Bash with curl, jq, dd and wc installed. It uploads video.mp4 from the current directory and stops if a request fails. Set the API environment variables from “Before you start” first.

Complete multipart upload example
set -euo pipefail
SIZE=$(wc -c < video.mp4 | tr -d '[:space:]')
BODY=$(jq -n --argjson size "$SIZE" \
  '{filename: "video.mp4", size: $size, content_type: "video/mp4"}')
curl --fail-with-body -sS -X POST "$LANTERNAUT_API/uploads" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" \
  -H "Content-Type: application/json" \
  -d "$BODY" > upload.json
ASSET=$(jq -er '.upload.assetId' upload.json)
PART=$(jq -er '.upload.partSize' upload.json)
COUNT=$(jq -er '.upload.partCount' upload.json)
for ((n = 1; n <= COUNT; n++)); do
  URL=$(curl --fail-with-body -sS -X POST "$LANTERNAUT_API/uploads/$ASSET/parts/$n" \
    -H "Authorization: Bearer $LANTERNAUT_KEY" | jq -er '.url')
  dd if=video.mp4 bs="$PART" skip="$((n - 1))" count=1 2>/dev/null | \
    curl --fail-with-body -sS -X PUT --data-binary @- "$URL"
done
curl --fail-with-body -sS -X POST "$LANTERNAUT_API/uploads/$ASSET/complete" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" \
  -H "Content-Type: application/json" -d '{}'

Alternatively, complete with { "parts": [{ "partNumber": 1, "etag": "…" }] }. Read each PUT response’s ETag header and supply all parts in ascending order, with unique numbers. The S3 field names PartNumber and ETag are also accepted.

Optional start fieldPurpose
project_idOmit to use the key’s upload destination. Null selects Main only if the key permits access to Main.
titleDisplay name; defaults to the filename.
encodeTrue by default. False stores the source for a later encode.
max_height240, 480, 720, 1080, 1440 or 2160. Premium quality requires a plan that includes it.
delivery_modeadaptive or single; omitted uses the account default.
single_resolutionRequested quality for single delivery.
keep_originalRetain the source after processing; omitted uses the account default.
transcribeTrue requests captions with the video’s encode, where supported by the plan.
transcription_languageOptional language with transcribe:true; null or omitted detects the language.

Resume using GET /uploads/{assetId} and GET /uploads/{assetId}/parts, then send missing parts. Abandon an open upload with DELETE /uploads/{assetId}. Unfinished uploads are closed after 48 hours: the asset becomes errored (upload_abandoned) and the upload.expired webhook fires. GET /assets?status=uploading lists the uploads still open. Completion can be retried for the same asset without starting another encode.

Sign every part in one call (your server)
PARTS=$(seq 1 "$COUNT" | jq -cs '{partNumbers: .}')
curl --fail-with-body -sS -X POST "$LANTERNAUT_API/uploads/$ASSET/parts" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" -H "Content-Type: application/json" \
  -d "$PARTS"
# → { "parts": [{ "partNumber": 1, "url": "…", "method": "PUT", "expiresAt": "…" }, …] }

Retrying safely. Send an Idempotency-Key header (any unique string, such as a UUID) with POST /uploads, POST /imports or POST /projects. If the connection drops, send the same request with the same key: you get the first answer back (marked Idempotent-Replayed: true) instead of a second upload. Keys are kept for 24 hours. The same key with a different request is refused 422 idempotency_key_reused.

If a file’s processing stops moving, send it again with POST /assets/{id}/requeue (assets:write). It goes back in line and an encoder is asked for at once; nothing new is charged. The answer is requeued or waiting, or running when an encoder is working on it right now. Lost jobs are also put back automatically every minute.

Set a video’s thumbnail

A video’s thumbnail is the picture its player shows before it plays. Upload any image (JPEG, PNG or WebP), wait until it is ready, then make it the thumbnail. It applies at once, in every embed.

Use an image as the thumbnail
curl -X PATCH "$LANTERNAUT_API/assets/VIDEO_ID/thumbnail" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image_id":"IMAGE_ID"}'

Send {"image_id":null} to go back to the automatic frame. GET /assets/{id}/thumbnail returns the current thumbnail with a permanent link to each of its sizes, or null while the automatic frame shows. Reading takes assets:read; changing it assets:write. A key limited to one location can only use images from that location.

Host thumbnails, GIFs and stickers

Prepared a thumbnail, sticker or GIF yourself? Host it like the dashboard’s Create tools do: start a creation, upload its bytes with the ordinary multipart upload, then queue it. We keep a WebP copy in several sizes.

kindFileLimits
thumbnailA still PNG16 to 2,048 pixels a side
stickerA still PNG, transparency kept16 to 2,048 pixels a side
gifAn animated GIFAt most 640 pixels a side, 15 seconds and 180 frames
  1. Start the creation

    POST /creator/uploads with kind, title and size (bytes, at most 20 MB). For a thumbnail, add thumbnail_for with the video’s ID. The answer’s upload is an ordinary multipart upload.

  2. Upload and complete it

    Sign and send each part, then complete the upload, exactly as in Upload media directly to storage.

  3. Queue it and wait

    POST /creator/{assetId}/queue, then read GET /creator/{assetId} until status is ready (or failed, with error). A thumbnail then applies itself to its video.

GET /creator lists your newest creations and GET /creator/status says whether hosting is available. At most five can be in progress at once. Reading takes assets:read; the rest assets:write.

Read video analytics

Plays and watch time come from the player’s own reports, in embeds and the Lanternaut player. They are kept for 90 days, in UTC days. Both calls take assets:read; a key limited to one location counts only its videos.

The last 7 days
curl "$LANTERNAUT_API/analytics?days=7" \
  -H "Authorization: Bearer $LANTERNAUT_KEY"
FieldMeaning
playsSessions that started playing.
watchedSecondsTotal time watched.
completedPlays that reached the end.
errorsPlayback errors.

GET /analytics returns totals, previous (the period before; null for 90 days), daily and videos ranked by plays, 50 a page with page and hasMore. days is 7, 30 (the default) or 90. GET /assets/{id}/analytics returns the same for one video.

Manage webhooks

Webhooks tell your server when a video is ready, fails or is deleted, and when captions are ready, instead of polling. They belong to the account, so a key limited to one location cannot manage them. You can also manage them on the Webhooks page.

RequestDoes
POST /webhooksAdd one; its signing secret is shown once.
GET /webhooksList them.
PATCH /webhooks/{id}Change its url or events, or pause with {"active": false} and resume with true. A paused one gets nothing.
POST /webhooks/{id}/rotate-secretA new signing secret, shown once. Deliveries are signed with it from then on.
DELETE /webhooks/{id}Remove one.
GET /webhooks/deliveriesThe latest deliveries and how each went: pending, delivered or failed.
POST /webhooks/deliveries/{id}/resendSend a failed delivery again, unchanged, within a minute.

Each delivery is a POST of { "id", "type", "createdAt", "data" }. data by event:

Eventdata
asset.readyassetId, jobId, kind (initial or reencode), durationSeconds
asset.erroredassetId, jobId, kind, status (errored or blocked), errorReason
asset.deletedassetId
transcript.readyassetId, jobId, language, segmentCount
transcript.erroredassetId, jobId, errorReason
lead.capturedleadId, assetId, email, name, atSeconds, createdAt
upload.expiredassetId, title, declaredBytes, startedAt, errorReason (upload_abandoned)
usage.warningdimension (storage, delivery or encode), stage (near at 80%, reached at 100%, throttled), used, limit, unit, percent, message, walletAvailableUsd, periodStart, periodEnd
balance.lowavailableUsd, heldUsd, overageSpendUsd, topupMinimumUsd, message, periodStart, periodEnd

usage.warning and balance.low are each sent once per stage per billing cycle, checked every 15 minutes: subscribe to them to hear before delivery is slowed or encodes wait. A field may be added to data at any time; ignore what you don’t know.

Check the signature before trusting a delivery. X-Lanternaut-Signature-V2: t=<unix seconds>,v2=<hex> is the HMAC-SHA256, with your secret, of the time, a dot and the raw body. Refuse it when t is more than 5 minutes from your clock: an old delivery replayed by someone else fails. (X-Lanternaut-Signature: sha256=<hex>, the body alone, is still sent for receivers written before.)

Verify a delivery (Node)
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body exactly as received, before any JSON parsing.
function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(parts.v2 ?? '');
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
// verify(rawBody, req.headers['x-lanternaut-signature-v2'], process.env.LANTERNAUT_WEBHOOK_SECRET)

Answer with any 2xx within 5 seconds. Otherwise the delivery is tried again 1 minute, 5 minutes, 30 minutes and 2 hours after the attempt before: the first attempt and four retries, five in all, the last about 2.5 hours after the first. Every attempt has the same event id (use it to skip one you have handled) and a fresh t. After rotating a secret, accept the old and the new one for a few minutes; a delivery your endpoint refused meanwhile is retried on the schedule above.

Read usage and balance

GET /usage (usage:read) returns this billing cycle against your plan, in the same terms the dashboard shows: what you store now, what has been delivered and encoded, the plan’s limits, what is left, and the wallet.

This cycle’s usage
curl --fail-with-body -sS "$LANTERNAUT_API/usage" \
  -H "Authorization: Bearer $LANTERNAUT_KEY"
FieldMeaning
plan, planName, periodThe plan and the cycle’s start and end.
storageGbStored now, the figure the storage allowance checks.
deliveryGb, encodeAllowanceMinutesUsedDelivered this cycle, and standard encoding minutes used.
limits, remainingThe allowances (with deliveryGraceGb, the free delivery past the allowance) and what is left of each.
over, throttledWhich allowances are used up, and whether delivery is slowed now (past allowance and grace with an empty wallet).
uploadsBlocked, uploadsBlockedReasonWhether new uploads are refused, and why (storage or nonpayment).
balanceavailableUsd, heldUsd (reserved by encodes under way) and overageSpendUsd this cycle.

To be told rather than ask, subscribe a webhook to usage.warning and balance.low (above). Past the plan, delivery uses its free grace, then the wallet; with the wallet empty delivery is slowed, never cut, and new encodes wait in awaiting_funds. Prices are in GET /catalog and on the pricing page.

Import from a link or from Vimeo

Instead of uploading, give Lanternaut the address of a file and it fetches it into your storage, then encodes it like any upload. Imports need an assets:write key and count toward your storage like uploads.

Import a file from a link
curl --fail-with-body -sS -X POST "$LANTERNAUT_API/imports" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/launch.mp4", "title": "Launch"}'

The link must download the file itself and say its size; a web page, or a link that needs a sign-in, fails with the reason in error. The answer is the import, status waiting. Follow it with GET /imports/{id}: it moves to copying (with percent), then copied, when assetId is the new file, and done once it plays. The usual asset.ready webhook fires as well.

CallWhat it does
GET /importsNewest first; filter with status=failed and so on.
POST /imports/{id}/retryTries a failed import again from the start.
DELETE /imports/{id}Cancels one still waiting or copying.
POST /imports/vimeoWith { "videos": [{ "id": "76979871" }] }: imports from the Vimeo account connected on the dashboard’s Import page.
GET /imports/vimeo/videos?page=1A page of that Vimeo library, each video marked if already imported.

Imports take the same optional fields as an upload: project_id, encode, max_height, delivery_mode, single_resolution and keep_original. A Vimeo video brings its title, its captions and its custom thumbnail once it plays.

Projects and API keys

Main is the default library. Create projects to organize related videos and images. Each file belongs to one location; moving it preserves its asset ID and embed links. Each API key has an upload destination and an optional restriction to that location. A new key with a selected project starts restricted by default.

OperationEndpoint or field
List or create projectsGET /projects · POST /projects
Read progress or rename a projectGET /projects/{id} · PATCH /projects/{id}
Move a filePATCH /assets/{id}/project
Create an additional keyPOST /keys
Change a key’s settingsPATCH /keys/{id}
Rotate only the selected keyPOST /keys/{id}/rotate

On upload, omit project_id to use the key’s destination. Set it to a project ID, or null for Main, only when the key permits that location. On listing, use project_id=main or a project ID. Without a filter, an unrestricted key lists all locations and a restricted key lists only its own.

A key’s raw secret is shown only when it is created or rotated. Store the new secret securely and update the integration when rotating; the old key is revoked. Key creation accepts projectId, restrictToProject, scopes and rateLimitPerMinute. New keys omit billing:write by default.

Project deletion requires the exact project name in confirmationName. It revokes linked keys and runs cleanup in the background. Read the project to check progress; repeat deletion after deletion_failed to retry. Main cannot be deleted as a project. See OpenAPI for complete request schemas.

Limit playback to allowed domains

Open Settings → Allowed domains and enter the sites where your media may appear, one per line. An empty list allows any site. The restriction applies to video and image playback.

EntryMatches
example.comOnly example.com.
*.example.comSubdomains such as www.example.com, but not example.com itself.
www.example.comOnly www.example.com.
localhost · 127.0.0.1Local development. Other IP addresses are not supported.

List both example.com and *.example.com if you need the root and its subdomains. Entries name hosts; schemes, ports and paths are ignored. A playback token’s optional domains list may narrow these permissions, never widen them.

Set the allowed domains through the API
curl -X PATCH "$LANTERNAUT_API/settings" \
  -H "Authorization: Bearer $LANTERNAUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"allowed_domains":["example.com","*.example.com"]}'

PATCH /settings replaces the whole list and needs a key for the whole account with the settings:write scope; keys made before 12 October 2026 need it added on the API keys page. The same call sets the defaults for new uploads (keep_original, delivery_mode), and GET /settings reads them all.

Handle errors and retries

API errors contain a machine-readable error code. A human-readable message is optional; validation errors can include issuesinstead. Every API response carries an x-request-id header. Keep it with the asset ID when contacting support.

Example validation error
{
  "error": "invalid_request",
  "issues": [
    { "path": ["size"], "message": "Expected number, received string" }
  ]
}
StatusAction
400Correct the request fields or incomplete part list; inspect error and issues.
401Check that the bearer key is valid and has not been revoked.
402Read the error details and check wallet balance or payment status before retrying.
403Check key scopes, project restrictions, account status and plan features. Uploads may also require email confirmation.
404Check the endpoint, asset ID and key’s permitted location. Files outside the key’s boundary also return 404.
405Use a method listed in the Allow response header.
409The current state prevents the operation. For not_ready, read statusDetail.
410The upload expired. Start a new upload and send the file again.
413The upload exceeds its size limit or available storage.
415unsupported_format: the file type is not one we can read; see accepted.
422The stored upload is larger than its declared size (start again with the correct byte count), or idempotency_key_reused.
429Wait the number of seconds in Retry-After, then retry within the rate limit.
5xxA server operation failed. Record the request ID and apply the retry guidance below.
Questions about this guide?Get in touch
API integration guide — Lanternaut docs