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.
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.
Request API access.
Submit a pilot access request at acorncompute.com/signup. We review requests manually and email credentials when the account is approved.
# Set the key Acorn sends after approval. export ACORN_API_KEY="acorn_live_8c1f...c93a"
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 install openai from openai import OpenAI client = OpenAI( api_key=os.environ["ACORN_API_KEY"], base_url="https://api.acorncompute.com/v1", )
npm install openai import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.ACORN_API_KEY, baseURL: "https://api.acorncompute.com/v1", });
# Every route accepts Bearer auth. curl https://api.acorncompute.com/api/v1/models \ -H "Authorization: Bearer $ACORN_API_KEY"
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.
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", ... }
# 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"
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.
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
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(); });
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.
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.
Bring-your-own MLX checkpoint path POST /v1/batch is planned, not available. Contact us if you need an early checkpoint evaluation.
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.
from openai import OpenAI client = OpenAI( api_key=os.environ["OPENAI_API_KEY"], )
from openai import OpenAI client = OpenAI( api_key=os.environ["ACORN_API_KEY"], base_url="https://api.acorncompute.com/v1", )
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
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=…andX-Acorn-Timestamp— HMAC-SHA256 overtimestamp.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_idas your idempotency key. - Per job or per account. Set
callback_urlon 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.
{
"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 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.
{
"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.