The API is in closed beta. Access is granted per account — book a demo with us, or reach out via the in-app support chat to request access.
Developers · Beta
One More Shot API
Bring lyric transcription, lyric videos and narrative music videos into your own product. The API is a paid feature of your One More Shot account: calls are charged in tokens from your regular balance, with no separate contract or plan needed.
Getting started
- Create a One More Shot account and make your first purchase. Beta API access must also be enabled for your account.
- Open Settings → API in the app and create an API key.
- Copy the key immediately: it is shown only once at creation.
Authentication
Authenticate every request by passing your API key as a Bearer token in the
Authorization header.
Keys look like oms_live_….
curl https://api.onemoreshot.ai/v1/account \
-H "Authorization: Bearer oms_live_..." Rate limits
Requests are rate limited per key — 60 requests per
minute by default. When you exceed the limit the API responds with
429 rate_limit_exceeded; wait a
full Retry-After interval (currently 60 seconds) before retrying. Reads and cancellation requests count too; the limit uses calendar-minute windows. If you need higher limits for production traffic,
contact us.
Idempotency & queued work
Send an Idempotency-Key on every new POST: 1–128 printable non-space ASCII characters. Reuse the same key and identical body after a timeout. Keys are scoped to the account and operation for 24 hours. Replays return the existing job with 202 and Idempotency-Replayed: true; the current status and charge may have changed. Different input with the same key returns 409. Without a key, retries can create separate jobs and charges. The optional idempotency_key body field is also accepted; if both forms are provided, they must match.
A new 202 response means queued admission, not successful media validation or a completed charge. Inspect GET and signed webhooks for asynchronous failures such as insufficient_credits, invalid media or provider errors. Keep the job ID when a client times out; do not submit a new job to check progress. API responses specify Cache-Control: private, no-store. Poll every 15–30 seconds and honor Retry-After.
Errors
Errors always use the same JSON envelope, with a machine-readable
code and a human-readable
message:
{
"error": {
"code": "invalid_request",
"message": "song_url is required."
}
} | Status | Code | Description |
|---|---|---|
400 | invalid_request | A parameter is missing or invalid — the message explains which one. |
401 | missing_api_key | No Bearer API key in the Authorization header. |
401 | invalid_api_key | Invalid or revoked API key. |
403 | insufficient_scope | The key doesn't have the scope required by this endpoint. |
404 | not_found | Unknown resource, or a job that belongs to another account. |
429 | rate_limit_exceeded | Honor the Retry-After header (60 seconds). |
409 | idempotency_conflict | The same idempotency key was used for different input. |
409 | not_cancellable_yet | not_cancellable | Cancellation is too early (honor Retry-After) or the job is already terminal. |
503 | api_unavailable | rate_limit_unavailable | New jobs are disabled, or request limits cannot be checked. Retry later. |
5xx | internal_error | Server-side failure. Retry POST with the same idempotency key and body. |
Pricing
API calls spend tokens from your One More Shot balance — the same tokens you buy in the app. New jobs are queued with zero initial charge. Workers validate media and balance, then charge before provider execution. Invalid media or insufficient tokens fails the job asynchronously without a charge. Charged failures and confirmed cancellations are refunded. Rates below are defaults; configured rates may differ. Video cost is measured seconds × rate, rounded up: 180 seconds of basic lyric video costs 5,940 tokens.
| Feature | Model | Cost |
|---|---|---|
Transcription | flat | 5 tokens per song. |
Lyric video (basic) | per second | 33 tokens per second of audio. |
Lyric video (pro) | per second | 40 tokens per second of audio. |
Narrative video (basic) | per second | 50 tokens per second of audio. |
Narrative video (pro) | per second | 80 tokens per second of audio. |
/account
Returns your current token balance and the limits of the key making the request. Useful for checking available credits before dispatching work.
{
"credits": 4200,
"scopes": ["lyric_videos", "narrative_videos"],
"rate_limit_rpm": 60,
"webhook_secret": "3f1a…9c"
} | Field | Type | Description |
|---|---|---|
credits | number | Current token balance of the account. |
scopes | string[] | Scopes granted to this API key. |
rate_limit_rpm | number | Requests per minute allowed for this key. |
webhook_secret | string | null | Secret for verifying webhook signatures. Null until your first job with a webhook_url; available after its initial 202 admission. |
/lyric-videos/transcriptions
Transcribe a song into time-coded lyric segments — the same segments used to drive lyric video generation. Present them to your users for review and editing before generating the final video.
Cost: 5 tokens per song, refunded automatically if the transcription fails.
Request body
| Field | Type | Description |
|---|---|---|
song_url required | string | Public http(s) audio URL; up to 100 MB and 420 seconds. Trim bounds must fit the original file. |
trim_start optional | number | Start of the section to transcribe, in seconds. Requires trim_end. |
trim_end optional | number | End of the section to transcribe, in seconds. Must be greater than trim_start. |
webhook_url optional | string | Public https URL notified on terminal outcomes. See Webhooks. |
curl -X POST https://api.onemoreshot.ai/v1/lyric-videos/transcriptions \
-H "Authorization: Bearer oms_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: YOUR_UNIQUE_REQUEST_ID" \
-d '{
"song_url": "https://your-cdn.com/song.mp3",
"trim_start": 0,
"trim_end": 60
}' {
"id": "aBc123XyZ",
"status": "processing",
"credits_charged": 0
}
Transcription is queued; timing depends on queue and provider load. Poll
GET /lyric-videos/transcriptions/{id}
until status becomes
completed or
failed.
/lyric-videos/transcriptions/{id}
Returns the status of a transcription job. When completed, the response includes the detected language and the time-coded segments with per-word timings.
{
"id": "aBc123XyZ",
"status": "completed",
"credits_charged": 5,
"credits_refunded": 0,
"language": "en",
"segments": [
{
"text": "First line of the song",
"start": 8.5,
"end": 10.6,
"duration": 2.1,
"wordCount": 5,
"timedWords": [
{ "text": "First", "start": 8.5, "end": 8.9 }
]
}
],
"error": null,
"created_at": "2026-01-01T12:00:00.000Z",
"completed_at": "2026-01-01T12:00:14.000Z"
} | Field | Type | Description |
|---|---|---|
status | string | One of 'processing', 'completed' or 'failed'. |
language | string | Detected language code (completed jobs only). |
segments | object[] | Lyric lines with start/end times, word count and per-word timings. |
error | object | null | Code/message object on failure, otherwise null. Inspect credits_refunded for returned tokens. |
/lyric-videos
Turn a song into a full lyric video. Optionally pass
segments from the
transcription endpoint (edited or not) to control the displayed lyrics; without them
the song is transcribed automatically as part of the generation.
Cost: priced per second of audio — we probe the song's real duration server-side, so the charge always matches the audio you send. Refunded automatically if the generation fails.
Request body
| Field | Type | Description |
|---|---|---|
song_url required | string | Public http(s) audio URL; up to 100 MB and 10–420 seconds. |
quality optional | string | 'basic' (720p) or 'pro' (1080p). Default 'basic'. |
aspect_ratio optional | string | '9:16' (default), '16:9' or '1:1'. |
style_preset optional | string | Visual style for the generated scenes. Must be one of the supported preset keys listed below; omit for the default look. |
song_genre optional | string | Genre hint, at most 500 characters. |
singer_image_url optional | string | Public http(s) photo URL; at most 10 MB and 25 megapixels. |
segments optional | object[] | 1–1000 ordered, non-overlapping segments within the audio duration. Each includes text, start, end, duration, wordCount and timedWords; word times must fit their parent segment. |
webhook_url optional | string | Public https URL notified on terminal outcomes. See Webhooks. |
Segment text is limited to 10,000 characters. Duration must equal end minus start within 0.05 seconds; wordCount is a non-negative integer and timedWords has at most 1,000 entries per segment. Media-dependent validation happens after admission. Request JSON is limited to 1 MB.
Style presets
style_preset accepts exactly
one of these keys — any other value is rejected with a
400 invalid_request.
80S_ILLUSTRATION 80s Illustration ART_BRUT Art Brut ART_POSTER Art Poster BAUHAUS Bauhaus BLUEPRINT Blueprint C4D_CARTOON C4D Cartoon CHILDRENS_BOOK Children's Book COLLAGE Collage DOODLE Doodle EMOTIONAL_MINIMAL Emotional Minimal FLAT_ART Flat Art FLAT_VECTOR Flat Vector GRAFFITI_I Graffiti GRAFFITI_II Graffiti II MINIMAL_ILLUSTRATION Minimal MIXED_MEDIA Mixed Media PAINT_GESTURE Paint Gesture POP_ART Pop Art RETRO_ETCHING Retro Etching WATERCOLOR Watercolor WEIRD Weird WOODBLOCK_PRINT Woodblock Print curl -X POST https://api.onemoreshot.ai/v1/lyric-videos \
-H "Authorization: Bearer oms_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: YOUR_UNIQUE_REQUEST_ID" \
-d '{
"song_url": "https://your-cdn.com/song.mp3",
"quality": "basic",
"aspect_ratio": "9:16",
"song_genre": "pop"
}' {
"id": "dEf456UvW",
"status": "processing",
"credits_charged": 0
}
Rendering is queued. Initial admission has no measured duration or charge; these
appear after successful preparation. Poll every 15–30 seconds or use webhooks;
queue/provider delays vary and a client timeout does not cancel the job. Poll
GET /lyric-videos/{id}
until status becomes
completed or
failed or
canceled.
/lyric-videos/{id}
Returns the status of a lyric video job. When completed, the response includes the final video and thumbnail URLs.
{
"id": "dEf456UvW",
"status": "completed",
"credits_charged": 5940,
"credits_refunded": 0,
"duration_seconds": 180,
"video_url": "https://storage.googleapis.com/.../final.mp4",
"thumbnail_url": "https://storage.googleapis.com/.../thumb.jpg",
"error": null,
"created_at": "2026-01-01T12:00:00.000Z",
"completed_at": "2026-01-01T12:07:41.000Z"
} | Field | Type | Description |
|---|---|---|
status | string | One of 'processing', 'completed', 'failed' or 'canceled'. |
credits_refunded | number | Tokens returned after failure or confirmed cancellation. |
cancellation_requested | boolean | Present as true after a cancellation request; it is not terminal confirmation. |
duration_seconds | number | Probed duration of the song, which the price was based on. |
video_url | string | Final rendered video (completed jobs only). |
thumbnail_url | string | Video thumbnail (completed jobs only). |
error | object | null | Code/message object on failure, otherwise null. Inspect credits_refunded for returned tokens. |
/lyric-videos/{id}
Cancel a queued video immediately, without charge, before a worker first claims it for media preparation. Cancellation and that claim are checked atomically; if cancellation wins, the job cannot start or charge. Once preparation has started (including retries and waiting for the next generation task), the configured waiting period from job creation applies (60-minute fallback), independently of the displayed ETA. Earlier requests return 409 not_cancellable_yet with Retry-After in seconds. An eligible request is queued and returns 202 with cancellation_requested: true. Poll GET or wait for job.canceled to confirm the canceled status and full refund of charged tokens; completion can win the race. Repeating DELETE after confirmed cancellation returns 204 with no body. Other terminal states return 409 not_cancellable. Transcription has no public cancellation endpoint.
curl -X DELETE https://api.onemoreshot.ai/v1/lyric-videos/dEf456UvW \
-H "Authorization: Bearer oms_live_..." { "id": "dEf456UvW", "status": "processing", "cancellation_requested": true } POST /music-videos
Create a complete narrative music video from a song, prompt and cast. Existing API keys work here too. Creative assets are generated internally; no separate preview approval is needed.
| Field | Type | Description |
|---|---|---|
song_url required | string | Public audio URL; 10–420 seconds and at most 100 MB. |
prompt required | string | Nonblank creative direction, at most 10,000 characters. |
characters required | array | 1–4 characters. Each needs name (1–100 characters), image_url and role (singer or character). Exactly one singer. Optional description: 1–2,000 characters. Each image: at most 10 MB / 25 megapixels. No duets. |
quality | string | basic (default): 720p, 50 tokens/second. pro: 1080p, 80 tokens/second. |
aspect_ratio | string | 9:16 (default), 16:9 or 1:1. |
style_preset | string | 90s-anime, ghibli, yellow-family, pixar-3d, ps2-low-poly, disney-90s, lego, cartoon-network, pixel-art. Omit for realistic styling. |
webhook_url | string | Optional public HTTPS URL for signed terminal notifications. |
idempotency_key | string | Optional alternative to the Idempotency-Key header; both must match when provided. |
{
"song_url": "https://example.com/song.mp3",
"prompt": "Two friends reunite in a neon-lit city.",
"quality": "basic",
"aspect_ratio": "9:16",
"style_preset": "90s-anime",
"characters": [
{
"name": "Alex",
"role": "singer",
"image_url": "https://example.com/alex.jpg"
},
{
"name": "Sam",
"role": "character",
"image_url": "https://example.com/sam.jpg",
"description": "Alex’s childhood friend"
}
],
"webhook_url": "https://example.com/webhooks/oms"
} Send to https://api.onemoreshot.ai/v1/music-videos with Bearer authentication, Content-Type: application/json and an Idempotency-Key. Unknown fields, creative seeds, preview assets and provider settings are rejected. Returns 202 with a job ID and initially zero charged tokens. Workers snapshot media before charging ceil(measured seconds × rate). Retries retain the job’s chosen renderer configuration.
GET /music-videos/{id}
Uses the same status, duration, charge/refund, timestamps, error and completed video_url/thumbnail_url fields as lyric-video jobs. Signed webhooks use type: narrative_video with the existing delivery IDs and terminal events. Poll every 15–30 seconds.
DELETE /music-videos/{id}
Cancel immediately before the first media-preparation claim. After preparation starts, the configured delay from job creation applies (60-minute fallback). Returns 202 while queued; poll or wait for job.canceled to confirm. Confirmed repeat cancellations return 204. Charged failures and confirmed cancellations receive one full refund. All narrative endpoints share existing API rate limits and queues.
Webhooks
Instead of polling, pass a webhook_url
(https only) in the body of
POST /lyric-videos or
POST /lyric-videos/transcriptions.
When the job completes, fails or is canceled, we POST its terminal status and result,
charges/refunds, error and timestamps, plus a type field.
Events & headers
| Header | Value | Description |
|---|---|---|
X-OMS-Event | job.completed | job.failed | job.canceled | What happened to the job. |
X-OMS-Delivery | string | Equals the job id — use it to deduplicate deliveries. |
X-OMS-Signature | sha256=<hex> | HMAC-SHA256 of the raw request body, keyed with your webhook secret. |
POST https://your-server.com/oms-webhook
Content-Type: application/json
X-OMS-Event: job.completed
X-OMS-Delivery: dEf456UvW
X-OMS-Signature: sha256=6ac1…e2
{
"id": "dEf456UvW",
"type": "lyric_video",
"status": "completed",
"credits_charged": 5940,
"credits_refunded": 0,
"duration_seconds": 180,
"video_url": "https://storage.googleapis.com/.../final.mp4",
"thumbnail_url": "https://storage.googleapis.com/.../thumb.jpg",
"error": null,
"created_at": "2026-01-01T12:00:00.000Z",
"completed_at": "2026-01-01T12:07:41.000Z"
} Verifying signatures
Your webhook secret is created automatically with your first webhook job and returned
by GET /account
as webhook_secret. Compute an
HMAC-SHA256 of the raw request body and compare it to
X-OMS-Signature — reject the
request if they differ.
const crypto = require('crypto');
const express = require('express');
const app = express();
// Verify against the RAW request body — the exact bytes we signed.
app.post('/oms-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.OMS_WEBHOOK_SECRET) // from GET /account
.update(req.body)
.digest('hex');
const signature = req.headers['x-oms-signature'] || '';
const actualBytes = Buffer.from(signature, 'utf8');
const expectedBytes = Buffer.from(expected, 'utf8');
const valid = actualBytes.length === expectedBytes.length &&
crypto.timingSafeEqual(actualBytes, expectedBytes);
if (!valid) return res.status(401).end();
const job = JSON.parse(req.body);
// Dedupe on req.headers['x-oms-delivery'] (equals job.id).
console.log(job.status, job.video_url);
res.status(200).end();
}); Delivery & retries
Respond with any 2xx within
10 seconds. Normally there are up to three total HTTP attempts (initial delivery plus
two retries). Webhooks are best-effort; keep polling available as a fallback.
Retries and crash recovery can produce duplicates: persistently deduplicate by
X-OMS-Delivery before applying side effects, and acknowledge duplicates with 2xx.
Support
The API is in beta and evolving quickly. For questions, higher rate limits or volume pricing, reach out via the in-app support chat or our help center.