☰ MenuPartner API

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/v1 source. 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_url is a path you resolve with your own key per view — exactly as §02's GET …/previews describes — and page_url is a stable hosted-page URL (or null). Never store a previews_url result; 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/.