Skip to main content
Fast and Standard transcription routes are live. Catalog rates match /pricing. Transcription details →
Home / Developers

An API you've already written against.

Acorn speaks the OpenAI transcription API — same request shape, response shape, and error codes — so an existing client works with a one-line base-URL change. Everything else runs through a small native jobs API. The whole reference fits on one page.

Base URL
api.acorncompute.com
Auth
Bearer · ACORN_KEY
Default response
JSON · 2xx · 4xx · 5xx
Access
Pilot · by request
Quickstart

Three steps to your first transcript.

If you've never written a line of Acorn code, you'll be done by step three. If you're migrating from OpenAI, skip to step two.

01 — Account

Request API access.

Submit a pilot access request at acorncompute.com/signup. We review requests manually and email credentials when the account is approved.

~/.zshrc
# Set the key Acorn sends after approval.
export ACORN_API_KEY="acorn_live_8c1f...c93a"
02 — Connect

Point your OpenAI client at Acorn. Or call the API directly.

The transcription route speaks the OpenAI HTTP surface, so an existing OpenAI client needs one change — the base URL and the key. Prefer no dependencies? Every route is plain HTTPS with Bearer auth, so curl works too.

pip · openai ≥ 1.30
pip install openai

from openai import OpenAI
client = OpenAI(
  api_key=os.environ["ACORN_API_KEY"],
  base_url="https://api.acorncompute.com/v1",
)
npm · openai ≥ 4.50
npm install openai

import OpenAI from "openai";
const client = new OpenAI({
  apiKey: process.env.ACORN_API_KEY,
  baseURL: "https://api.acorncompute.com/v1",
});
No install · raw HTTP
# Every route accepts Bearer auth.
curl https://api.acorncompute.com/api/v1/models \
  -H "Authorization: Bearer $ACORN_API_KEY"
03 — Submit

Submit your first job.

The native route is POST /api/v1/jobs: hand us a fetchable audio_url and we return 202 with a job_id you poll (or receive by webhook). Small local files can go straight up the OpenAI-compatible multipart route instead.

POST /api/v1/jobs · async · 202
curl https://api.acorncompute.com/api/v1/jobs \
  -H "Authorization: Bearer $ACORN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "audio_url": "https://yourbucket.s3.amazonaws.com/interview.mp3",
    "audio_duration": 1830,
    "model": "whisper-large-v3-turbo-q4",
    "language": "en",
    "callback_url": "https://yourapp.com/hooks/acorn"
  }'

# → 202 { "job_id": "job_3uA9k2", "status": "pending",
#         "resolved_model": "whisper-large-v3-turbo-q4", ... }
POST /v1/audio/transcriptions · multipart
# OpenAI-compatible: upload a local file, get the transcript back inline.
curl https://api.acorncompute.com/v1/audio/transcriptions \
  -H "Authorization: Bearer $ACORN_API_KEY" \
  -F file="@interview.mp3" \
  -F model="whisper-large-v3" \
  -F response_format="verbose_json"
04 — Receive

Receive the webhook.

When a job carries a callback_url (per job, or a default on your account), Acorn POSTs the finished result there. Every payload is signed HMAC-SHA256 over timestamp.body — verify it before trusting the event.

app.py · Flask · stdlib hmac
import hmac, hashlib, time

MAX_SKEW = 300  # reject deliveries outside the five-minute replay window

@app.post("/hooks/acorn")
def on_acorn_event():
    ts = request.headers["X-Acorn-Timestamp"]
    body = request.get_data()
    # Reject stale/replayed timestamps before comparing the signature.
    if abs(time.time() - int(ts)) > MAX_SKEW:
        return "", 401
    sig = request.headers["X-Acorn-Signature"].removeprefix("sha256=")
    expected = hmac.new(
        os.environ["ACORN_WEBHOOK_SECRET"].encode(),
        ts.encode() + b"." + body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return "", 401
    event = request.get_json()
    if event["status"] == "completed":
        store(event["job_id"], event["result"])
    return "", 204
server.ts · Express · node:crypto
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_SKEW = 300;  // reject deliveries outside the five-minute replay window

app.post("/hooks/acorn", express.raw({ type: "*/*" }), (req, res) => {
  const ts = req.headers["x-acorn-timestamp"];
  // Reject stale/replayed timestamps before comparing the signature.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > MAX_SKEW) {
    return res.status(401).end();
  }
  const sig = String(req.headers["x-acorn-signature"]).replace(/^sha256=/, "");
  const expected = createHmac("sha256", process.env.ACORN_WEBHOOK_SECRET)
    .update(ts + ".").update(req.body).digest("hex");
  if (sig.length !== expected.length ||
      !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  if (event.status === "completed") enqueue(event.job_id);
  res.status(204).end();
});
Reference

Every endpoint, every status.

The full surface area Acorn exposes today. Beta routes will keep the same request and response shape at GA — only the pricing locks.

POST /api/v1/jobs Submit a transcription job from a fetchable audio_url. Returns 202 + job_id. GA
POST /api/v1/jobs/batch Submit up to 100 jobs in one request. 207 Multi-Status, one result per item. GA
POST /v1/audio/transcriptions OpenAI-compatible multipart upload for a local file. Sync transcript inline. Pilot-supported (see limits below). Pilot
GET /api/v1/jobs/{job_id} Fetch status & result of any submitted job. GA
GET /api/v1/jobs/{job_id}/result Fetch the finished transcript for a job. GA
GET /api/v1/jobs/{job_id}/shards Per-shard breakdown of a long-audio job. GA
GET /api/v1/jobs/{job_id}/events Audit trail of state transitions for a job. GA
GET /api/v1/jobs List recent jobs · paginated · filterable by state. GA
GET /api/v1/models List available backends and models from the catalog. GA
GET /api/v1/usage/summary Usage & spend summary for your account. GA
Roadmap

Not shipping yet.

These routes aren't live on the pilot — they return 404 today. They keep the same OpenAI-compatible request and response shapes when they land; reach out if you want early access.

POST /v1/embeddings Batch text → vectors. Up to 512 inputs per request. Not available
POST /v1/images/generations Async image generation queue. Webhook on completion. Not available
POST /v1/chat/completions Async chat completion. Non-streaming. OpenAI-compatible shape. Not available
POST /v1/batch Submit a multi-job manifest with a bring-your-own MLX checkpoint. Planned · Q4

Bring-your-own MLX checkpoint path POST /v1/batch is planned, not available. Contact us if you need an early checkpoint evaluation.

OpenAI compatibility · pilot

Two lines of diff, then you're routing to Acorn.

POST /v1/audio/transcriptions is a supported pilot path for Whisper-class transcription. Change the base URL and key, and openai.audio.transcriptions.create() routes here. It is not a full OpenAI Audio product clone — see the limits below and the full contract in the API docs.

Before · OpenAI
from openai import OpenAI

client = OpenAI(
  api_key=os.environ["OPENAI_API_KEY"],
)
After · Acorn
from openai import OpenAI

client = OpenAI(
  api_key=os.environ["ACORN_API_KEY"],
  base_url="https://api.acorncompute.com/v1",
)
Supported: multipart upload · json / verbose_json / text / srt / vtt · whisper-* and whisper-1 aliases · client or partner API keys (partner keys only via coordinator-staged multipart on this path) Sync only: HTTP holds open until done or the server deadline (default ACORN_OPENAI_COMPAT_TIMEOUT_SECONDS=600). No webhooks on this path — use POST /api/v1/jobs + callback_url for async When to use this path: local file ≤ 25 MB and you can keep one request open for the full wait. Prefer native jobs when audio may approach 600s wall time, when any proxy/CDN/Fly edge hop has a shorter request lifetime (often 30–120s), or when you need webhooks Timeouts: 408 deadline_exceeded keeps the backend job alive — poll GET /api/v1/jobs/{job_id}, or resubmit long work as {"audio_url":"https://example.com/audio.mp3","callback_url":"https://yourapp.com/hooks/acorn"} Setup: coordinator operators must enable ACORN_ALLOW_LOCAL_AUDIO_JOBS=true for multipart uploads (defaults off; otherwise the path returns 503 local_audio_not_enabled with the same native-jobs guidance) Not on this path: translations · streaming · prompt/temperature effects · GPT-4o audio weights (names alias to Whisper) SDKs: openai-python ≥ 1.30 · openai-node ≥ 4.50 · openai-go ≥ 0.8 · openai-ruby ≥ 0.4 · LangChain / LlamaIndex via OpenAI provider
Webhooks

Notify, don't poll.

For longer jobs — batches, BYO models, large audio — register a webhook and we POST the result the moment a worker hands it back.

HMAC-signed, retried, replayable.

  • Signed. Every delivery carries X-Acorn-Signature: sha256=… and X-Acorn-Timestamp — HMAC-SHA256 over timestamp.body. Reject timestamps outside a five-minute window before comparing the signature.
  • At-least-once. We retry with exponential backoff, then park the delivery in a dead-letter queue. Use the job_id as your idempotency key.
  • Per job or per account. Set callback_url on an individual job, or a default on your account — the job-level value wins.
  • Replayable. Dead-lettered deliveries can be inspected and replayed once your endpoint is back up.
POST https://yourapp.com/hooks/acorn · status: completed
{
  "job_id": "job_3uA9k2",
  "status": "completed",
  "timestamp": "2026-05-21T14:45:12Z",
  "reference_id": "interview-2043",
  "result": {
    "text": "So the way we onboarded the first cohort…",
    "language": "en",
    "duration": 1830.0
  }
}
Errors & retries

Errors look like errors.

Native API errors include a stable code, a human-readable message, and the request ID returned in the X-Request-ID response header. Use that request ID when contacting support. Retry 429 and 503 responses according to Retry-After; fix the request before retrying other 4xx responses.

HTTP 422 · invalid_audio_duration
{
  "error": {
    "code": "invalid_audio_duration",
    "message": "audio_duration must be zero or greater.",
    "request_id": "7f3a92c1b04d4e6f9c1a2b3c4d5e6f70"
  }
}

The docs are above. Request pilot access.

Tell us what you are building and expected volume. We review pilot requests before issuing API credentials.