Developers

Reelvane API

Upload and manage your story videos programmatically over a small REST API, and receive signed webhook events when visitors convert.

Overview

The Reelvane REST API lets you upload, edit, replace, list, and delete the short videos that power your story widget — the same operations the dashboard performs, but from your own backend, a script, or CI. Webhooks push a signed JSON payload to your endpoint whenever a visitor triggers a conversion event.

The REST API and webhooks are a Pro feature, included with your subscription. Requests from an account without an active subscription return 403. See pricing.
  • Base URL: https://reelvane.com (replace with your own Reelvane host if you self-host).
  • All responses are JSON, except successful DELETE which returns { "ok": true }.
  • Requests are server-to-server: no cookies and no wildcard CORS. Authenticate with a Bearer API key.
  • The Pro plan allows up to 10 videos per account.

Authentication

Create an API key in the dashboard under Settings → REST API (the API keys panel). Key creation is Pro-gated.

A key is shown exactly once at creation time — copy it immediately. Only a SHA-256 hash is stored at rest, so a database read can never recover a usable key. Keys look like sk_ followed by 40 hex characters. You can revoke a key at any time from the same panel; revocation takes effect immediately.

Send the key on every request in the Authorization header:

Authorization: Bearer sk_your_key_here

Auth error responses

The auth gate is checked in this order: bad key → 401, then plan → 403, then rate limit → 429.

StatusWhenBody
401Missing or malformed Authorization header, or an unknown key.{ "error": "Invalid or missing API key" }
403Valid key, but the account is not on the Pro plan.{ "error": "API access requires the Pro plan" }
429Too many requests (see Rate limits).{ "error": "Rate limit exceeded. Try again shortly." }

REST API reference

All video endpoints live under /api/v1/videos. The serialized video object returned by every write and the list endpoint has this shape:

Video object

{
  "id": "clw...",              // string, Reelvane video id
  "order": 0,                   // number, 0-based position in the story sequence
  "src": "https://reelvane.com/uploads/<userId>/<file>.mp4",   // absolute video URL
  "originalName": "clip.mp4",   // string, uploaded filename
  "mimeType": "video/mp4",      // "video/mp4" | "video/webm" | "video/quicktime"
  "size": 1048576,              // number, bytes
  "caption": "New drop",        // string | null (<= 80 chars)
  "ctaLabel": "Shop now",       // string | null (<= 24 chars)
  "ctaUrl": "https://...",      // string | null (http/https only)
  "posterUrl": "https://reelvane.com/uploads/<userId>/<file>.jpg", // string | null
  "publishAt": "2026-07-01T12:00:00.000Z", // ISO-8601 string | null
  "expiresAt": null              // ISO-8601 string | null
}
Upload constraints (enforced server-side by magic-byte sniffing — the client-supplied MIME type and filename are never trusted): max file size 60MB, accepted types MP4, WebM, MOV. An optional poster image must be JPEG or PNG and 2MB or smaller; an invalid poster is silently ignored and never fails the upload.
GET/api/v1/videos

List all of your videos, ordered by their story position.

Details: Auth: Bearer key required. No request body.

Status codes

  • 200Videos returned.
  • 401 / 429Auth / rate-limit (see Authentication).

curl

curl https://reelvane.com/api/v1/videos \
  -H "Authorization: Bearer sk_your_key_here"

Response 200

{ "videos": [ /* array of video objects */ ] }
POST/api/v1/videos

Upload a new video. The new video is appended to the end of the story sequence.

Details: Auth: Bearer key required. Body: multipart/form-data with fields file (required, the video) and poster (optional, a JPEG/PNG thumbnail).

Status codes

  • 200Created; returns the video object.
  • 400No file, empty file, file too large ("File too large (60MB max)"), an unsupported type, or the plan video cap was reached.
  • 401 / 403 / 429Auth / plan / rate-limit.

When the account is already at its plan video cap, the response is 400 with a message like Video limit reached. Your Pro plan allows up to 10 videos. (the response also includes "upgrade": true).

curl

curl -X POST https://reelvane.com/api/v1/videos \
  -H "Authorization: Bearer sk_your_key_here" \
  -F [email protected] \
  -F [email protected]

Response 200

{ "video": { /* video object */ } }
PATCH/api/v1/videos/{id}

Update a video’s caption, CTA, and scheduling. Send only the fields you want to change; omitted fields are left untouched. The request body is JSON.

Details: Auth: Bearer key required.

FieldTypeRules
captionstring | nullTrimmed; max 80 chars. Empty string or null clears it.
ctaLabelstring | nullTrimmed; max 24 chars. Setting a non-empty CTA requires the Pro plan (403).
ctaUrlstring | nullMust be a valid http(s) URL. Setting a non-empty CTA requires the Pro plan (403).
publishAtstring | nullISO-8601 date string, or null/empty to clear (live now).
expiresAtstring | nullISO-8601 date string, or null/empty to clear (never expires). Must be after publishAt.

Status codes

  • 200Updated; returns the video object.
  • 400Invalid JSON, a value over its length limit, a non-http(s) ctaUrl, an unparseable date, or expiresAt not after publishAt.
  • 403Setting a non-empty CTA (ctaLabel / ctaUrl) on a non-Pro plan.
  • 404No video with that id belongs to you.
  • 401 / 429Auth / rate-limit.

curl

curl -X PATCH https://reelvane.com/api/v1/videos/VIDEO_ID \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Summer sale",
    "ctaLabel": "Shop now",
    "ctaUrl": "https://example.com/sale",
    "publishAt": "2026-07-01T09:00:00.000Z",
    "expiresAt": "2026-07-08T09:00:00.000Z"
  }'

Response 200

{ "video": { /* updated video object */ } }
DELETE/api/v1/videos/{id}

Delete a video and its stored file(s). Owner-scoped and idempotent.

Details: Auth: Bearer key required. No request body.

Status codes

  • 200Deleted.
  • 404No video with that id belongs to you.
  • 401 / 429Auth / rate-limit.

curl

curl -X DELETE https://reelvane.com/api/v1/videos/VIDEO_ID \
  -H "Authorization: Bearer sk_your_key_here"

Response 200

{ "ok": true }
POST/api/v1/videos/{id}/replace

Swap a video’s underlying file while keeping the same id, order, caption, CTA, schedule, and analytics history. This does not add a video, so it never counts against the plan cap.

Details: Auth: Bearer key required. Body: multipart/form-data with fields file (required, the new video) and poster (optional). When no poster is supplied, the previous poster is cleared (it came from the old clip). Same size / type validation as upload.

Status codes

  • 200Replaced; returns the video object.
  • 400No file, empty file, too large, or unsupported type.
  • 404No video with that id belongs to you.
  • 401 / 429Auth / rate-limit.

curl

curl -X POST https://reelvane.com/api/v1/videos/VIDEO_ID/replace \
  -H "Authorization: Bearer sk_your_key_here" \
  -F [email protected]

Response 200

{ "video": { /* video object */ } }
GET/api/v1/stats

Fetch your aggregate analytics — the same numbers the dashboard shows. Optional ?days= selects the range and normalizes to 7, 30 (default), or 90.

Details: Auth: Bearer key required. Analytics is Pro-only. No request body.

Status codes

  • 200Stats returned.
  • 403Plan lacks API or analytics access.
  • 401 / 429Auth / rate-limit.

curl

curl "https://reelvane.com/api/v1/stats?days=30" \
  -H "Authorization: Bearer sk_your_key_here"

Response 200

{
  "rangeDays": 30,
  "totals": { "impression": 0, "open": 0, "view": 0, "complete": 0, "click": 0 },
  "rates": { "openRate": null, "completionRate": null, "ctr": null },
  "perVideo": [
    { "videoId": "…", "label": "Caption", "views": 0, "completes": 0, "clicks": 0 }
  ],
  "series": [
    { "date": "2026-07-01", "impression": 0, "open": 0, "view": 0, "complete": 0, "click": 0 }
  ]
}

Webhooks

Register a webhook endpoint in the dashboard under Settings → Webhook. On the first save, Reelvane mints a signing secret you use to verify deliveries. Webhooks are Pro-gated.

Events

A delivery fires when a visitor triggers a conversion event on your widget:

  • click — the CTA / swipe-up link on a story was clicked.
  • complete — a story played through to the end.

Payload

The body is JSON. The type is also mirrored in an X-Reelvane-Event header.

POST body

{
  "id": "evt_...",        // the analytics event id
  "type": "click",         // "click" | "complete"
  "videoId": "clw...",     // the video id, or null
  "siteKey": "your-site-key",
  "createdAt": "2026-07-01T12:00:00.000Z"
}

Verifying the signature

Every request carries an X-Reelvane-Signature header of the form sha256=<hex>, where the hex is an HMAC-SHA256 of the raw request body keyed with your signing secret. Recompute it over the exact bytes you received and compare with a timing-safe check:

Node.js verification

import crypto from "node:crypto";

// `rawBody` MUST be the exact bytes received (do not re-serialize the JSON).
function verifyReelvaneSignature(rawBody, header, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  const a = Buffer.from(header || "", "utf8");
  const b = Buffer.from(expected, "utf8");
  // timingSafeEqual throws if lengths differ, so guard first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Example (Express, with the raw body preserved):
app.post("/reelvane-webhook", (req, res) => {
  const sig = req.header("X-Reelvane-Signature");
  if (!verifyReelvaneSignature(req.rawBody, sig, process.env.STORYPOP_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.rawBody.toString("utf8"));
  // ... handle event.type: "click" | "complete"
  res.sendStatus(200);
});

Delivery semantics

  • Fire-and-forget. Deliveries are best-effort and are not retried; the visitor-facing analytics beacon never waits on them.
  • Each attempt has a 3-second timeout.
  • Deliveries are capped at 60 per minute per account; events beyond that are recorded but do not trigger a webhook.
  • Redirects are not followed — respond with a 2xx directly from your endpoint.

Endpoint URL rules (SSRF)

  • Must be an http or https URL; in production it must be https.
  • Must resolve to a public host. Loopback, private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local / cloud-metadata (169.254.0.0/16), unique-local IPv6, *.local, and bare single-label hosts are rejected.
  • The host is re-resolved and re-validated before every send (defense against DNS rebinding), and the outbound connection is pinned to the vetted IP.

Widget embed

Drop this single script tag onto any page (just before the closing </body>tag). It renders a floating, Instagram-style story bubble that expands into a full-screen player — entirely inside a Shadow DOM so your site’s CSS can’t leak in or out. Replace YOUR_SITE_KEY with the site key from your dashboard.

Embed snippet

<script src="https://reelvane.com/widget.js" data-site-key="YOUR_SITE_KEY" async></script>

The widget reads its configuration (videos, accent color, position, branding) from the site key on the data-site-key attribute and talks back to the Reelvane host it was served from. Multiple embeds with different keys can coexist on one page.

Prefer a full-page player? Reelvane also hosts one per site key:

Hosted page

https://reelvane.com/s/YOUR_SITE_KEY

Rate limits

Limits use a sliding one-minute window. Exceeding an API limit returns 429; the public analytics and webhook paths fail soft instead of erroring.

Endpoint groupLimitKeyed by
REST API (/api/v1/*)120 / minAuthenticated user
REST API pre-auth guard300 / minClient IP
Analytics beacon (/api/e)120 / min per IP · 600 / min per site keyIP + site key
Webhook deliveries60 / minAccount (user)
Stats (/api/stats)60 / minAuthenticated user
Login (/api/auth/login)10 / min per IP · 10 / min per emailIP + email
Signup (/api/auth/signup)5 / min per IP · 10 / min per emailIP + email

Limits are enforced per running server instance. In a multi-instance deployment the effective limit scales with the number of instances.