Partner API Reference
Redpoint Engine · Tier-3 Integration
Run the full order → upload → review → delivery loop from your own backend. Your server holds the key; the engine handles money, large-file ingest, editor dispatch, QC, and delivery underneath.
This is the versioned source-of-truth for the tier-3 partner API. It is transcribed from the published reference and cross-checked against the live
/api/v1source. Field names, status keys, and the signature scheme are authoritative; human labels may change.
| Base URL | https://wakecut.com |
| Auth | Bearer rpk_… |
| Format | JSON |
| Webhooks | HMAC-SHA256 |
01 · Authentication
Every request carries a bearer token your operator mints for your tenant. Keys are stored hashed — the plaintext is shown once at mint and is unrecoverable, so keep it server-side only and never ship it to a browser.
Authorization: Bearer rpk_AbC…43chars
The token is rpk_ + 43 characters. Tenant identity comes solely from the key — any Host, x-tenant-slug, or body/query hint is ignored, so one key can only ever act within its own tenant. Verification is constant-time against the stored hash.
Error envelope
Failures return a stable machine code and a human message (the message may change; switch on code):
{ "error": { "code": "unauthorized", "message": "invalid API key" } }
| Status | Code |
|---|---|
401 |
unauthorized |
403 |
forbidden |
404 |
not_found |
400 |
bad_request |
402 |
payment_required |
500 |
server_error |
Conventions. Success responses wrap the result in
{ "data": … }. Request bodies use camelCase (packageId,returnUrl); response bodies use snake_case (order_id,status_key).
02 · Endpoints
Six routes cover the whole loop. A tenant key acts as the buying client — it can create orders, upload original footage, read status & previews, and approve / request-revision / cancel. Editor and admin actions are not reachable.
GET /api/v1/tenant
Wiring self-check — returns the tenant your key is scoped to.
{ "data": { "tenant": { "id": "…", "slug": "expocut", "name": "ExpoCut" } } }
POST /api/v1/orders
Create an order; returns a Stripe Checkout URL the buyer completes. The job is minted after payment — you learn its id from the first webhook (or by polling with your externalRef).
| Field | Type | Notes | |
|---|---|---|---|
packageId |
string | required | An active package in your tenant. |
briefData |
object | cond. | Validated against your active brief template; failures return the missing fields. |
externalRef |
string | optional | Your own order reference; echoed back on every job read (≤200 chars). |
buyerEmail |
string | optional | Prefills Stripe receipt email. |
returnUrl / cancelUrl |
string | optional | Send the buyer back to your own front. Honoured only if the origin is pre-registered (see §04 / §05). |
isRush, extraIds |
bool, string[] | optional | Rush flag; add-on package ids (≤50). |
buyerIsBusiness, vatNumber, poReference |
bool, string | optional | B2B invoicing details. |
{
"data": {
"order_id": "…",
"checkout_url": "https://checkout.stripe.com/…"
}
}
GET /api/v1/jobs/{jobId}
Read a job's current state. status_key is the stable token to switch on; status is a human label that may change.
{
"data": {
"job": {
"id": "…",
"status": "Ready for your review",
"status_key": "ready_for_review",
"created_at": "2026-07-16T10:00:00Z",
"delivered_at": null,
"revision_count": 1,
"external_ref": "partner-order-123"
}
}
}
Fares, editor identity, and price internals are never returned — the response is an explicit allowlist (
id,status,created_at,delivered_at,revision_count,external_ref), not the raw row.
POST /api/v1/jobs/{jobId}/transition
The client-loop edge. Body: { "to": "APPROVED" | "REVISION" | "CANCELLED", "note"?: string }.
| to | From | Effect |
|---|---|---|
APPROVED |
ready_for_review |
Captures the authorization (the money moment) and pays the editor. |
REVISION |
ready_for_review |
Requests changes within the package's included revisions. |
CANCELLED |
any pre-capture state | Voids the uncaptured authorization. |
Capture failure at approval returns 402 payment_required — retry once the buyer fixes their card. Response mirrors the job read (id, status, status_key).
POST /api/v1/jobs/{jobId}/uploads
Two actions on one route, by body.action. A tenant key may only upload original footage.
1 · presign — action: "create"
// → { "action": "create", "filename": "clip.mov", "contentType": "video/quicktime" }
{ "data": { "upload_url": "https://…r2 signed PUT…", "key": "…", "expires_in": 21600 } }
2 · complete — action: "complete"
// PUT the bytes to upload_url, then:
// → { "action": "complete", "key": "…" }
{ "data": { "file": { "key": "…", "kind": "original", "size_bytes": 10485760 } } }
Single PUT caps at 5 GB (the signed URL lives 6 h). Complete verifies the object landed before registering it.
GET /api/v1/jobs/{jobId}/previews
Watermarked review handles, newest-first, each a short-lived signed URL. Raw drafts and finals are never exposed here.
{
"data": {
"job_id": "…",
"previews": [
{ "id": "…", "current": true, "created_at": "…", "url": "https://…signed…", "expires_in": 600 }
]
}
}
These signed URLs expire in 10 minutes — resolve them on demand per view; never store them in a webhook payload or a database.
03 · Status vocabulary
Switch on status_key from the REST API. Internal QC rework (IN_REVIEW, QC_REJECTED) deliberately collapses to quality_check so your buyer never sees churn.
| status_key | Human label | Meaning |
|---|---|---|
received |
Order received | Paid, awaiting an editor. |
in_production |
In production | Assigned / being edited. |
quality_check |
Quality check | Human QC before you ever see it. |
ready_for_review |
Ready for your review | A preview is available — fetch previews, then transition. |
in_revision |
Revisions under way | Your change request is being actioned. |
approved |
Approved | You approved; payment captured. |
delivered |
Delivered | Final files released. |
cancelled |
Cancelled | Order cancelled pre-capture. |
04 · Webhooks
Subscribe an endpoint per tenant to receive signed, deduplicated events. Four event types fire — a deliberate subset of transitions, with internal QC excluded.
| Event | Fires on |
|---|---|
draft.ready |
A preview is ready for review (also emits job.status_changed). Carries a review handle. |
job.delivered |
Final files released (also emits job.status_changed). Carries a delivery handle. |
job.status_changed |
Assignment and revision-requested transitions. Carries a review handle when it lands in CLIENT_REVIEW, or a delivery handle when it lands in DELIVERED. |
job.cancelled |
Job cancelled (emitted on its own — never a paired job.status_changed). |
Payload
draft.ready POST body:
{
"id": "<deliveryId>",
"type": "draft.ready",
"created_at": "2026-07-16T12:00:00.000Z",
"data": {
"job": {
"id": "…",
"status": "CLIENT_REVIEW",
"external_ref": "partner-order-123",
"created_at": "…",
"delivered_at": null,
"revision_count": 0,
"status_label": "Ready for review"
},
"transition": { "from": "IN_REVIEW", "to": "CLIENT_REVIEW", "from_label": "…", "to_label": "…" },
"review": {
"previews_url": "/api/v1/jobs/…/previews",
"page_url": "https://your-front.example/jobs/…/review"
}
}
}
The id is stable across retries — it is your dedupe key.
The webhook payload ships the raw enum in
job.status/transition.to(e.g.CLIENT_REVIEW), unlike the REST API. Same allowlist though — no fares, no editor identity.
Review & delivery handles
An event that lands the job in a state you act on carries a handle telling you where to reach the cut or the final. The handle is keyed on the destination state, so it rides every event landing there — draft.ready and its paired job.status_changed→CLIENT_REVIEW, job.delivered and its paired job.status_changed→DELIVERED.
data.review — present iff the event lands the job in CLIENT_REVIEW:
| Field | Value |
|---|---|
previews_url |
A relative API path — /api/v1/jobs/{id}/previews. GET it with your own rpk_ key to mint fresh short-lived signed preview URLs on demand (see §02). Relative by design: a domainless pure-API tenant resolves it against the same API base it already calls. |
page_url |
The engine-hosted review page, present only if your tenant fronts a public domain. null for a domainless / pure-API tenant — you drive your own front from previews_url. |
data.delivery — present iff the event lands the job in DELIVERED:
| Field | Value |
|---|---|
page_url |
The engine-hosted delivery/review page, or null for a domainless tenant. There is no finals API path yet — a headless tenant reaches finals via its own front. |
A handle is never a signed URL. Webhook payloads are stored and retried for up to 6 hours; a short-lived signed R2 URL baked into one would be dead on arrival for any retry and would sit at rest in our ledger. So
previews_urlis a path you resolve with your own key per view — exactly as §02'sGET …/previewsdescribes — andpage_urlis a stable hosted-page URL (ornull). Never store apreviews_urlresult; fetch it on demand.
Signature verification
Every delivery carries an X-Redpoint-Signature header. Recompute the HMAC over "{timestamp}.{rawBody}" using your endpoint's shared secret, and reject anything older than the tolerance window.
X-Redpoint-Signature: t=1752667200,v1=4f3c…hexHmacSha256
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const t = Number(parts.t);
// 1 · freshness — reject replays, past OR future
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
// 2 · recompute over "<t>.<rawBody>" — use the EXACT bytes received
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1, "hex");
// 3 · constant-time compare
return a.length === b.length && timingSafeEqual(a, b);
}
Verify against the raw request body, before any JSON re-serialization — the signature is over the exact bytes sent. Then dedupe on
payload.id: delivery is at-least-once, so a timeout-then-retry can legitimately re-send the same event.
Delivery & retries
| 2xx | delivered |
| retry | 1m → 5m → 30m → 2h → 6h |
| max | 5 attempts |
| timeout | 10s |
One inline attempt, then a cron retries with a fresh timestamp on the same body. Return any 2xx to acknowledge.
05 · Getting a key
Keys and webhook endpoints are provisioned by Redpoint, not self-serve. To onboard a seat:
| Step | Who |
|---|---|
Mint an rpk_ key for your tenant — shown once, kept server-side. |
Redpoint |
| Register a webhook endpoint + its signing secret. | Redpoint |
Pre-register your front's origin(s) so returnUrl/cancelUrl are honoured. |
Redpoint |
Drive the loop from your backend; dedupe webhooks on payload.id. |
You |
A lost key is unrecoverable — it's revoked and re-minted, never revealed. Treat
rpk_tokens and webhook secrets like passwords: server-side only, never in a browser bundle.
Redpoint Engine — tier-3 headless integration. Reference generated from the live /api/v1 surface. Field names, status keys, and the signature scheme are authoritative; human labels may change. See also the working partner integration + runbook in examples/expo-seat/.