Developers
The Euphona Engine, as an API
One REST contract over api.euphonaai.com/v1. Async jobs with live SSE progress, signed file transfer, prefixed keys and a free test mode: a first-party product with its own twelve-month support window.
Keys are created in Studio settings: eu_test_… immediately and free, eu_live_… from the same screen once a card is on file, metered from its first job. Test mode runs the full contract.
Everything is a job
curl -X POST https://api.euphonaai.com/v1/masters \
-H "Authorization: Bearer $EUPHONA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"file_id": "file_8Kd2mQTz3XaV7pLw"}'
HTTP/2 202
{
"id": "job_7Vd2mHs5Yq8Tz3Xa",
"object": "job",
"type": "mastering",
"status": "queued",
"stage": null,
"metering": {
"pool": "payg",
"charged_minutes": 0,
"test_mode": false
}
}Design
How it's designed
Four decisions that aren't negotiable, because every one of them is the difference between an integration that survives its first outage and one that doesn't.
Everything is a job
Audio goes around the API, not through it
This API is the product, not a byproduct
The document is the contract
Walkthrough
The shape of an integration
Create a file, send the bytes to the signed target, submit a job, and watch it finish over SSE. Four steps, and the third one is the only part that differs per capability.
1. Create the file and get an upload target
curl -X POST https://api.euphonaai.com/v1/files \
-H "Authorization: Bearer $EUPHONA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filename":"take-07.wav","bytes":58210044,"content_type":"audio/wav"}'
{
"id": "file_8Kd2mQ...",
"object": "file",
"status": "awaiting_upload",
"upload": { "protocol": "...", "url": "https://files.euphonaai.com/...", "expires_at": "..." }
}2. Send the bytes to files., not to us
# Read upload.protocol, do not assume one. The resumable mechanism is an
# implementation choice inside the contract and can change without a major
# version bump, which is exactly why it is a field and not documentation.3. Watch the job
curl -N https://api.euphonaai.com/v1/jobs/job_4Rb.../events \
-H "Authorization: Bearer $EUPHONA_API_KEY"
event: job.progress
data: {"status":"analyzing","stage":"analyze","progress":{"fraction":0.4}}
event: job.completed
data: {"status":"succeeded","result":{...},"metering":{"pool":"payg","charged_minutes":3.4}}4. Reconcile against the job itself
curl https://api.euphonaai.com/v1/jobs/job_4Rb... \
-H "Authorization: Bearer $EUPHONA_API_KEY"
{"id":"job_4Rb...","status":"succeeded","result":{...},
"metering":{"pool":"payg","charged_minutes":3.4}}
# The job resource is the state machine's current truth: whatever a stream
# showed or missed, read it back before you act on it. A dropped SSE
# connection reconnects with Last-Event-ID and loses nothing.Reference
The surface
The platform surface every capability shares. Each capability adds one creation endpoint; the job it returns flows through everything below: the same lifecycle, the same events, the same report.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/files | Create a file and get a signed, resumable upload target. |
| GET | /v1/files/{id} | Ingest status, measured duration, sample rate, channels, format. |
| DELETE | /v1/files/{id} | Delete a file and what was made from it; refused while a job is still using it. |
| GET | /v1/files/{id}/download | A signed, expiring download URL. |
| GET | /v1/jobs | The unified read surface, cursor-paginated. |
| GET | /v1/jobs/{id} | Status, stage, progress, metering and the measured report. |
| POST | /v1/jobs/{id}/cancel | Cancel; no charge before the process stage starts. |
| POST | /v1/jobs/{id}/input | Answer what an awaiting_input job asks, and resume it. Not metered. |
| GET | /v1/jobs/{id}/events | Server-sent events for the life of the job. |
| POST | /v1/projects | Group files and jobs; the Track Graph is per project. |
| GET | /v1/projects/{id} | One project, with its file and job ids. |
| GET | /v1/usage | Quota state and the two metering pools. |
| GET | /v1/webhook-endpoints | Your webhook endpoints, in this key’s mode. |
| POST | /v1/webhook-endpoints | Register an https URL; it answers a signed challenge before anything is stored. |
| GET | /v1/webhook-endpoints/{id} | One endpoint. Its secret is never shown again. |
| PATCH | /v1/webhook-endpoints/{id} | Change its events, description or URL; a new URL is verified again. |
| DELETE | /v1/webhook-endpoints/{id} | Delete it; undelivered events for it are dropped. |
| POST | /v1/webhook-endpoints/{id}/rotate-secret | A new signing secret; the old one keeps signing during an overlap. |
| GET | /v1/events | Thirty days of events, with each delivery’s outcome. |
| GET | /v1/events/{id} | One event as it was delivered, with its deliveries. |
Webhooks
Hear when a job is done
Signed notifications when a job finishes or is waiting for your answer, so an integration does not have to hold a stream open or poll.
- Events
job.completed,job.failed,job.canceledandjob.awaiting_input. Each is an event whosedatais the job resource exactly as polling returns it.- Workspace events
file.expiringwhen a stored upload is given a date on which it may be removed, with the file asdata, andusage.thresholdwhen a usage pool reaches 80% or 100% of its limit. Both reach endpoints of either mode, once per file or once per threshold and period.- Registration
- Needs a key with the
webhooks:managescope, which new keys do not have. The URL must use https and resolve only to public addresses, and it must answer a signed challenge with a 2xx response that echoes it before anything is stored. Deliveries are sent over IPv4 only, so the host must have an IPv4 address. - Delivery
- At least once, so deduplicate on
Euphona-Event-Id. Anything but a 2xx within 10 seconds is retried with exponential backoff for 24 hours, then dead-lettered and kept inGET /v1/events. Redirects are never followed and order is not guaranteed: reconcile against the job. - Signatures
Euphona-Signaturecarries a timestamp and an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint’s secret. Verify it before parsing, compare in constant time, and refuse a timestamp more than five minutes from your clock.- Source addresses
- Deliveries are sent from
65.21.69.162. Allow them if your firewall filters incoming traffic, and verify the signature all the same: the address a request comes from does not prove who sent it.
Verifying a delivery (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";
// Pass the raw body exactly as it arrived: JSON parsed and serialised again
// is not the message that was signed.
export function verifyEuphonaSignature(rawBody, header, secret, now = Date.now() / 1000) {
const parts = header.split(",").map((part) => part.trim().split("="));
const t = Number(parts.find(([key]) => key === "t")?.[1]);
const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((value) => {
const given = Buffer.from(value, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}Operations
The things that bite at three in the morning
The six behaviors worth reading before you write the integration rather than after it wakes you up.
- Keys
eu_live_…andeu_test_…, owned by a workspace, shown once at creation and hashed at rest. Rotate with an overlap window of up to thirty days, so a rotation is not an outage. Test keys are free against their own monthly cap and run in their own scheduling class; they return a preview rather than a master, so an integration can be built end to end without buying anything.- Errors
- RFC 9457
application/problem+json, always, with a stable machine-readablecode, a humandetailand the request id. Branch oncode, never on the prose: the prose is localized and the code is a contract. - Idempotency
Idempotency-Keyon every POST. The same key with the same body replays the original response for as long as the resource it created exists; the same key with a different body is refused. A retry after a timeout therefore returns the original job rather than starting a second one you will be charged for.- Limits
- Per-key sliding window with standard
RateLimit-*headers; 60 requests a minute on pay-as-you-go, higher under a committed-use contract. The limiter governs request frequency only: throughput is governed by your quota and the queue. - Two meters, never one
- API-key jobs meter to the pay-as-you-go pool; Studio-session jobs draw plan minutes. Neither pool can drain the other, and
GET /v1/usagereports both. Spend caps default to on. Storage is one allowance, not two: files uploaded with a key and the results of API jobs count against the workspace's plan storage allowance, 2 GB on the free plan, together with everything in the Studio, and an upload or job that would not fit is refused withstorage_full. - Request-Id
- On every response, success or failure. Quote it in a support request and it resolves end to end: through the job, through every stage, to whatever actually happened.
code | title |
|---|---|
| unauthenticated | Authentication required |
| invalid_ | Invalid API key |
| key_ | API key revoked |
| insufficient_ | Insufficient scope |
| email_ | Email verification required |
| malformed_ | Malformed request |
| validation_ | Request validation failed |
| unsupported_ | Unsupported media type |
| idempotency_ | Idempotency key reused with a different body |
| not_ | Not found |
| job_ | Job cannot be canceled |
| job_ | Job is not awaiting input |
| file_ | File is not ready |
| file_ | File has expired |
| file_ | File in use |
| file_ | File unavailable for legal reasons |
| unsupported_ | Unsupported audio format |
| unsupported_ | Unsupported sample rate |
| unsupported_ | Unsupported channel count |
| file_ | Audio is too long |
| file_ | Audio is too short |
| file_ | File is too large |
| corrupt_ | Audio could not be decoded |
| rate_ | Rate limit exceeded |
| too_ | Too many active jobs |
| quota_ | Quota exceeded |
| spend_ | Spend cap reached |
| test_ | Test-mode cap reached |
| storage_ | Storage full |
| purchase_ | Purchase required |
| payment_ | Payment method required |
| format_ | Format not included in this plan |
| webhook_ | Webhook URL must be HTTPS |
| webhook_ | Webhook URL must be publicly routable |
| webhook_ | Webhook ownership verification failed |
| internal_ | Internal error |
| service_ | Service unavailable |
| processing_ | Processing failed |
| capacity_ | Capacity unavailable |
| capability_ | Temporarily paused |
| workspace_ | Workspace suspended |
| uploads_ | Uploads paused |
| reauthentication_ | Confirm it is you |
Rates
What it costs
Per processed minute, times the capability multiplier, billed monthly. No subscription required, an API workspace can be on the free plan. Test mode is free.
- First 100 minutes a month
- $0.35
- Up to 1,000
- $0.25
- Up to 10,000
- $0.15
- Beyond that, or committed use
- Talk to us