Skip to content
Euphona

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

Capability endpoints return 202 and a job resource. There are no synchronous processing endpoints and there never will be: a three-minute song at mastering quality isn't a request/response cycle, and an API that pretends otherwise teaches every integration to be written wrong.

Audio goes around the API, not through it

POST /v1/files returns a signed, resumable upload target on files.euphonaai.com. Results come back as signed, expiring download URLs. The API itself is a JSON control plane and never carries a byte of audio.

This API is the product, not a byproduct

Euphona Studio speaks a different, first-party API. That's deliberate: the two are separate products sharing one engine, so a change the app wants can't reach this contract, and this contract’s twelve-month support window isn't a tax the app has to pay. The honest consequence is that the app may do things this API doesn't: when that matters to you, ask, and it becomes a roadmap item rather than a surprise.

The document is the contract

The OpenAPI 3.1 document is the source of truth. Server routes and types are generated from it, and a deploy-gated test holds the served routes equal to the declared operations - so drift between what is documented and what is served fails the build rather than being a thing we promise to watch for. SDKs generated from the same document arrive with the developer preview.

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.

Endpoints in the v1 contract
MethodPathWhat it does
POST/v1/filesCreate 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}/downloadA signed, expiring download URL.
GET/v1/jobsThe unified read surface, cursor-paginated.
GET/v1/jobs/{id}Status, stage, progress, metering and the measured report.
POST/v1/jobs/{id}/cancelCancel; no charge before the process stage starts.
POST/v1/jobs/{id}/inputAnswer what an awaiting_input job asks, and resume it. Not metered.
GET/v1/jobs/{id}/eventsServer-sent events for the life of the job.
POST/v1/projectsGroup files and jobs; the Track Graph is per project.
GET/v1/projects/{id}One project, with its file and job ids.
GET/v1/usageQuota state and the two metering pools.
GET/v1/webhook-endpointsYour webhook endpoints, in this key’s mode.
POST/v1/webhook-endpointsRegister 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-secretA new signing secret; the old one keeps signing during an overlap.
GET/v1/eventsThirty 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.canceled and job.awaiting_input. Each is an event whose data is the job resource exactly as polling returns it.
Workspace events
file.expiring when a stored upload is given a date on which it may be removed, with the file as data, and usage.threshold when 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:manage scope, 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 in GET /v1/events. Redirects are never followed and order is not guaranteed: reconcile against the job.
Signatures
Euphona-Signature carries 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_… and eu_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-readable code, a human detail and the request id. Branch on code, never on the prose: the prose is localized and the code is a contract.
Idempotency
Idempotency-Key on 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/usage reports 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 with storage_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.
Errors
codetitle
unauthenticatedAuthentication required
invalid_keyInvalid API key
key_revokedAPI key revoked
insufficient_scopeInsufficient scope
email_verification_requiredEmail verification required
malformed_requestMalformed request
validation_failedRequest validation failed
unsupported_media_typeUnsupported media type
idempotency_key_reuseIdempotency key reused with a different body
not_foundNot found
job_not_cancelableJob cannot be canceled
job_not_awaiting_inputJob is not awaiting input
file_not_readyFile is not ready
file_expiredFile has expired
file_in_useFile in use
file_disabledFile unavailable for legal reasons
unsupported_formatUnsupported audio format
unsupported_sample_rateUnsupported sample rate
unsupported_channel_countUnsupported channel count
file_too_longAudio is too long
file_too_shortAudio is too short
file_too_largeFile is too large
corrupt_mediaAudio could not be decoded
rate_limitedRate limit exceeded
too_many_active_jobsToo many active jobs
quota_exceededQuota exceeded
spend_cap_reachedSpend cap reached
test_mode_cap_reachedTest-mode cap reached
storage_fullStorage full
purchase_requiredPurchase required
payment_method_requiredPayment method required
format_not_in_planFormat not included in this plan
webhook_url_not_httpsWebhook URL must be HTTPS
webhook_url_not_publicWebhook URL must be publicly routable
webhook_verification_failedWebhook ownership verification failed
internal_errorInternal error
service_unavailableService unavailable
processing_failedProcessing failed
capacity_unavailableCapacity unavailable
capability_pausedTemporarily paused
workspace_suspendedWorkspace suspended
uploads_blockedUploads paused
reauthentication_requiredConfirm 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