Publish to BentBox from your platform

Connect a creator's BentBox account with OAuth, upload their videos, follow each one until it's live, and embed BentBox boxes and videos on your own pages.

Start

Authentication

BentAPI uses two kinds of credentials, depending on whose data you're touching.

CredentialUsed forHow to send it
OAuth access tokenActing for a creator: creating videos, uploading, reading their status. Get one per creator through the OAuth flow.access_token and connection_id on every content request (see below)
API key and secretReading public data for widgets: box data and video data.HTTP Basic auth: Authorization: Basic base64(key:secret)

Sending the access token

Content endpoints identify the creator by the pair you received from the token exchange: the access_token and the connection_id.

  • POST requests (/v1/content/video, /v1/content/upload-url): put access_token and connection_id in the JSON body. Also send Authorization: Bearer {access_token}.
  • GET requests (/v1/content/video/{video_id}): send Authorization: Bearer {access_token} and pass connection_id as a query parameter or an X-Connection-Id header. Tokens never go in a URL.

Base URL

https://api.bentbox.co

Getting credentials

Email api@bentbox.co with your platform name, the redirect URIs you'll use, and what you plan to build. You'll receive a client_id and client_secret for OAuth, and an API key and secret if you'll embed content. Request the upload_video scope for everything in Publish a video.

Connect an account

How OAuth works here

Each creator connects their BentBox account to your platform once. You get back a token and a connection ID, which you store and use for every request you make on their behalf.

  1. Send the creator to BentBoxThey click “Connect BentBox” on your site and you redirect them to /oauth/authorize.
  2. They sign in and approveBentBox shows its sign-in page if needed, then a consent screen listing the scopes you asked for.
  3. BentBox redirects back with a codeThe creator lands on your redirect_uri with a short-lived code and your state.
  4. Your server exchanges the codeA server-to-server POST /oauth/token returns the access_token, refresh_token, connection_id and user_id.
  5. Store the credentialsEncrypt the tokens at rest and keep them with your user record.

Authorize

GET/oauth/authorize
Called by
the creator's browser (redirect)

Redirect the creator here to start the flow. If they aren't signed in to BentBox, they're sent to the sign-in page first and returned to the consent screen afterwards with all your parameters intact. You don't need to handle that case.

Query parameters

ParameterDescription
client_id
string Required
Your platform's client ID.
redirect_uri
string Required
One of your registered redirect URIs. It must match exactly: same scheme, host and path, no trailing slash.
scope
string Required
Comma-separated scopes, for example upload_video.
response_type
string Required
Always code.
state
string Optional
A random value you generate and check when the creator returns. Strongly recommended: it protects against CSRF.
Redirect the creator to
GET https://api.bentbox.co/oauth/authorize
    ?client_id=mp_live_abc123
    &redirect_uri=https%3A%2F%2Fyourplatform.com%2Fcallback
    &scope=upload_video
    &state=8f1c2e9a7b
    &response_type=code

After approval, BentBox redirects to your redirect_uri:

Callback
https://yourplatform.com/callback?code=auth_abc123&state=8f1c2e9a7b

If the creator cancels, the callback carries error=access_denied instead of a code.

Token exchange

POST/oauth/token
Called by
your server

Swap the authorization code for the creator's credentials. Codes are single-use and expire quickly, so exchange them as soon as the callback arrives.

Request body (JSON)

ParameterDescription
grant_type
string Required
Always authorization_code.
code
string Required
The code from the callback.
client_id
string Required
Your platform's client ID.
client_secret
string Required
Your platform's client secret.
redirect_uri
string Required
The same redirect_uri you sent to /oauth/authorize.
Request
POST https://api.bentbox.co/oauth/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "code": "auth_abc123",
  "client_id": "mp_live_abc123",
  "client_secret": "mp_secret_xyz",
  "redirect_uri": "https://yourplatform.com/callback"
}
Response (201 Created)
{
  "access_token": "bx_live_abc123",
  "refresh_token": "bx_refresh_xyz",
  "token_type": "Bearer",
  "expires_in": 86400,
  "connection_id": "conn_abc123",
  "user_id": "55-1420773031-54af",
  "scopes": ["upload_video"]
}

Referral tracking

If your platform brings new creators to BentBox, you can have their accounts attributed to you. The BentBox sign-up page accepts two parameters, and any account created from a URL carrying them is credited to your referral.

ParameterDescription
ref
string
Your referral code. Letters and numbers only, up to 32 characters. It must be registered with BentBox first.
src
string
Your partner identifier, for example partnersite. Letters, numbers, hyphens and underscores, up to 32 characters.

New creators: send them to sign-up first

When you know the creator doesn't have a BentBox account yet, send them to the sign-up page with your parameters before starting OAuth. BentBox records the attribution when the account is created; they then go through OAuth as usual.

# 1. Sign-up with attribution
https://bentbox.co/signup?ref=YOUR_REF_CODE&src=partnersite

# 2. Then the normal authorize redirect
https://api.bentbox.co/oauth/authorize?client_id=…&redirect_uri=…&scope=upload_video&state=…&response_type=code

Existing creators: carry it in state

If you use one entry point for everyone, you can round-trip your own attribution data through state, which BentBox returns unchanged. This is for your own records: BentBox only credits referrals at account creation.

// Before redirecting: pack your CSRF token and attribution into state
const state = btoa(JSON.stringify({ csrf: crypto.randomUUID(), ref: "YOUR_REF_CODE", src: "partnersite" }));

// In your callback: unpack it, and check csrf against what you stored
const { csrf, ref, src } = JSON.parse(atob(req.query.state));
SituationUse
Creator is new to BentBoxSign-up first, with ref and src
Creator already has an accountstate, for your own attribution records
You don't knowSign-up first. Existing creators just sign in and continue to OAuth.

Publish a video

Publishing at a glance

Publishing takes three calls and a file transfer. Then you track the video until it's live and link the creator to it.

  1. Create the videoPOST /v1/content/video with the title, file details and consent. You get the video_id and the video's page links.
  2. Get an upload URLPOST /v1/content/upload-url checks the creator's verification and duration limit, then returns a presigned URL.
  3. Upload the filePUT the bytes to that URL. BentBox starts processing automatically.
  4. Track it to livePoll GET /v1/content/video/{video_id} and show the creator where their video stands. Link them to the settings page if it needs their attention.

Create a video

POST/v1/content/video
Auth
OAuth access token
Scope
upload_video

Creates the video on BentBox and reserves its ID. Nothing is public yet: the video only goes live after the file is uploaded, processed and approved.

Request body (JSON)

ParameterDescription
access_token
string Required
The creator's access token. Also send it as Authorization: Bearer.
connection_id
string Required
The connection ID from the token exchange.
title
string Required
Video title, up to 200 characters.
filename
string Required
File name with extension: .mp4, .mov, .avi, .wmv, .flv, .mkv or .webm.
filesize
integer Required
File size in bytes, up to 2 GB.
duration
number Required
Duration in seconds. Checked against the creator's length limit when you request the upload URL.
sole_performer
boolean Conditional
true if the creator is the only person in the video. Required unless you send release_form_id.
release_form_id
string Conditional
ProntoID reference to a signed release covering the other performers. Required unless sole_performer is true.
description
string Optional
Up to 5,000 characters.
tags
string[] Optional
Up to 20 tags, each up to 50 characters.
price
number Optional
Price in USD, 0.99 to 999.99.
is_premium
boolean Optional
Marks the video as premium content.
publish_on_ready
boolean Optional
Defaults to true: the video goes live once it's processed and approved. Send false to keep it as a draft that the creator publishes from BentBox.
curl -X POST https://api.bentbox.co/v1/content/video \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "access_token": "'"$ACCESS_TOKEN"'",
    "connection_id": "conn_abc123",
    "title": "Golden hour session",
    "filename": "session.mp4",
    "filesize": 524288000,
    "duration": 300,
    "description": "Shot on the rooftop at sunset.",
    "tags": ["outdoor", "summer"],
    "price": 9.99,
    "sole_performer": true,
    "publish_on_ready": true
  }'

With other performers in the video, leave out sole_performer and send "release_form_id": "pid_rel_a1b2c3" instead.

Response (201 Created)
{
  "success": true,
  "video_id": "1770903940-db122f8acccf9dfe",
  "status": "awaiting_upload",
  "upload_expires_at": "2026-10-16T09:12:20Z",
  "video_page_url": "https://bentbox.co/video?id=1770903940-db122f8acccf9dfe",
  "video_settings_url": "https://bentbox.co/video_settings?1770903940-db122f8acccf9dfe",
  "message": "Video metadata created. Use video_id to get upload URL."
}

The video_id is the video's public BentBox ID. Keep it: you'll use it for the upload, for status checks, and in links. The upload window is 24 hours from creation. See Video IDs and links.

Get an upload URL

POST/v1/content/upload-url
Auth
OAuth access token
Scope
upload_video

Returns a presigned URL for uploading the file straight to BentBox storage. This is also where BentBox checks that the creator's account is verified and that the video fits their length limit.

Request body (JSON)

ParameterDescription
access_token
string Required
The creator's access token. Also send it as Authorization: Bearer.
connection_id
string Required
Must be the same connection that created the video.
video_id
string Required
From Create a video.
title
string Required
Video title, up to 255 characters.
price
number Required
Sale price in USD. 0 for free, up to 9,999.99.
duration
number Required
Duration in seconds. The default limit is 15 minutes; some accounts allow longer.
description
string Optional
Up to 5,000 characters.
tags
string[] Optional
Up to 20 tags.
adult_content
boolean Optional
Defaults to true.
Request body
{
  "access_token": "bx_live_abc123",
  "connection_id": "conn_abc123",
  "video_id": "1770903940-db122f8acccf9dfe",
  "title": "Golden hour session",
  "price": 9.99,
  "duration": 300,
  "description": "Shot on the rooftop at sunset.",
  "tags": ["outdoor", "summer"],
  "adult_content": true
}
Response (200 OK)
{
  "success": true,
  "upload_url": "https://bentbox-api-uploads.s3.amazonaws.com/api-uploads/…",
  "upload_method": "PUT",
  "expires_in": 86400,
  "expires_at": "2026-10-16T09:12:20Z",
  "filesize": 524288000,
  "content_type": "video/*",
  "s3_key": "api-uploads/55-1420773031-54af/1770903940-db122f8acccf9dfe/session.mp4",
  "message": "Upload URL valid for 24.0 hours"
}

The URL is valid until the upload window closes, at most 24 hours. You can request a new one for the same video_id until the file arrives.

Upload the file

PUT{upload_url}
Auth
none: the URL is signed

Send the file bytes with an HTTP PUT to the presigned URL. A 200 means the file arrived. Don't add an Authorization header to this request.

curl -X PUT --upload-file ./session.mp4 \
  -H "Content-Type: video/mp4" \
  "$UPLOAD_URL"

Once the file lands, BentBox transcodes it and makes a preview and thumbnail. No further call is needed. With publish_on_ready left at true, the video goes live after BentBox reviews it. Use Get video status to follow it.

Get video status

GET/v1/content/video/{video_id}
Auth
OAuth access token
Scope
upload_video

Tells you where a video is: waiting for its file, processing, in review, live, or needing something from the creator. Use it to show progress on your side and to know when to send the creator to BentBox.

Request

ParameterDescription
video_id
path Required
The video_id from Create a video.
connection_id
query Required
The creator's connection ID. You can send it as an X-Connection-Id header instead.
Authorization
header Required
Bearer {access_token}.

You can only read videos that belong to the connected creator. Any other ID returns 404 not_found.

curl "https://api.bentbox.co/v1/content/video/1770903940-db122f8acccf9dfe?connection_id=conn_abc123" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response (200 OK)
{
  "success": true,
  "video_id": "1770903940-db122f8acccf9dfe",
  "status": "pending_review",
  "status_description": "Published and waiting for BentBox review.",
  "is_live": false,
  "title": "Golden hour session",
  "processing": "complete",
  "published": true,
  "approved": false,
  "duration": 300.04,
  "video_page_url": "https://bentbox.co/video?id=1770903940-db122f8acccf9dfe",
  "video_settings_url": "https://bentbox.co/video_settings?1770903940-db122f8acccf9dfe",
  "created_at": "2026-10-15T09:12:20Z",
  "checked_at": "2026-10-15T09:31:02Z"
}

Status values

StatusWhat it meansWhat to do
awaiting_uploadCreated; no file received yet.Upload the file.
processingFile received and being transcoded.Wait. Usually a few minutes.
draftProcessed but not published, because you sent publish_on_ready: false or the creator unpublished it.Send the creator to video_settings_url to publish.
pending_reviewPublished and waiting for BentBox review.Wait. It goes live when approved.
action_requiredApproved, but the creator still needs to finish the video's settings.Send the creator to video_settings_url.
livePublic at video_page_url.Show the link. Stop polling.
upload_expiredNo file arrived within 24 hours.Create the video again.
processing_failedThe file couldn't be processed.Check the file, create the video again and re-upload.
removedThe video was deleted.Stop polling.

Response fields

FieldDescription
status
string
One of the values above. Build your logic on this field.
status_description
string
A plain-language sentence you can show as-is.
is_live
boolean
true only when status is live.
processing
string
awaiting_upload, pending, in_progress, complete or failed.
published
boolean
Whether the video is set to be public.
approved
boolean
Whether BentBox has approved it.
title
string | null
The video title.
duration
number | null
Measured duration in seconds, once processed.
video_page_url
string
Public page. Viewers see it only when live; the creator can open it at any time.
video_settings_url
string
Where the creator edits, publishes and completes the video. Requires them to be signed in to BentBox.
created_at
string | null
ISO 8601, UTC. Only available for about a week after creation.
checked_at
string
When this status was read, ISO 8601, UTC.

Complete examples

The whole flow in one place: connect, create, upload, then check status. These are starting points: add your own storage, encryption and error handling.

// npm install express
import express from "express";
import crypto from "node:crypto";
import { createReadStream, statSync } from "node:fs";

const API = "https://api.bentbox.co";
const CLIENT_ID = process.env.BENTBOX_CLIENT_ID;
const CLIENT_SECRET = process.env.BENTBOX_CLIENT_SECRET;
const REDIRECT_URI = "https://yourplatform.com/oauth/callback";

const app = express();
app.use(express.json());

// 1. Send the creator to BentBox
app.get("/connect-bentbox", (req, res) => {
  const state = crypto.randomBytes(16).toString("hex");
  req.session.oauthState = state;
  const q = new URLSearchParams({
    client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
    scope: "upload_video", state, response_type: "code",
  });
  res.redirect(`${API}/oauth/authorize?${q}`);
});

// 2. Exchange the code and store the connection
app.get("/oauth/callback", async (req, res) => {
  if (req.query.state !== req.session.oauthState) return res.status(400).send("Invalid state");
  const r = await fetch(`${API}/oauth/token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code", code: req.query.code,
      client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: REDIRECT_URI,
    }),
  });
  const t = await r.json();
  await db.saveConnection(req.user.id, {
    connectionId: t.connection_id, bentboxUserId: t.user_id,
    accessToken: encrypt(t.access_token), refreshToken: encrypt(t.refresh_token),
  });
  res.redirect("/dashboard?connected=1");
});

// 3. Create, get upload URL, upload
app.post("/upload-video", async (req, res) => {
  const conn = await db.getConnection(req.user.id);
  const token = decrypt(conn.accessToken);
  const auth = { "Content-Type": "application/json", Authorization: `Bearer ${token}` };
  const { path, title, price = 0, duration } = req.body;

  const created = await (await fetch(`${API}/v1/content/video`, {
    method: "POST", headers: auth,
    body: JSON.stringify({
      access_token: token, connection_id: conn.connectionId,
      title, filename: "video.mp4", filesize: statSync(path).size, duration,
      price, sole_performer: true,
    }),
  })).json();
  if (!created.success) return res.status(400).json(created);

  const signed = await (await fetch(`${API}/v1/content/upload-url`, {
    method: "POST", headers: auth,
    body: JSON.stringify({
      access_token: token, connection_id: conn.connectionId,
      video_id: created.video_id, title, price, duration,
    }),
  })).json();
  if (!signed.success) return res.status(400).json(signed);

  const put = await fetch(signed.upload_url, {
    method: "PUT", headers: { "Content-Type": "video/mp4" },
    body: createReadStream(path), duplex: "half",
  });
  if (!put.ok) return res.status(502).json({ error: "upload_failed" });

  res.json({ video_id: created.video_id, settings: created.video_settings_url });
});

// 4. Status for your UI
app.get("/videos/:id/status", async (req, res) => {
  const conn = await db.getConnection(req.user.id);
  const r = await fetch(
    `${API}/v1/content/video/${encodeURIComponent(req.params.id)}?connection_id=${conn.connectionId}`,
    { headers: { Authorization: `Bearer ${decrypt(conn.accessToken)}` } }
  );
  res.status(r.status).json(await r.json());
});

app.listen(3000);

Embed content

Get box data

GET/v1/api/get-box-data
Auth
API key (Basic)

Returns a box's title, description, tags, a signed cover image URL and pricing. It powers the box widget, and you can call it from your own server too.

Query parameters

ParameterDescription
boxId
string Required
The BentBox box ID.
promo_code
string Optional
Promo code to apply to the price.
Request
GET https://api.bentbox.co/v1/api/get-box-data?boxId=YgyvDwSC
Authorization: Basic base64(api_key:api_secret)
Accept: application/json
Response (200 OK)
{
  "selling_price": "13.99",
  "selling_price_with_transaction_fees": "15.38",
  "user_price": "8.99",
  "transaction_fees": "1.39",
  "referral_fee": "0.00",
  "discount_value": "0.00",
  "discount_percent": "0.00",
  "promo_code_valid": "false",
  "cover_image_url": "https://d6ijsqg8e9unz.cloudfront.net/…",
  "cover_image_expires_in": 600,
  "box_name": "My Box Title",
  "box_description": "Box description here",
  "box_tags": "tag1,tag2,tag3"
}

Response fields

FieldDescription
selling_price
string
Price before transaction fees, 2 decimals.
selling_price_with_transaction_fees
string
What the buyer pays, including fees.
user_price
string
What the creator receives.
transaction_fees
string
Transaction fee amount.
referral_fee
string
Referral fee, if any.
discount_value
string
Discount amount applied.
discount_percent
string
Discount percentage applied.
promo_code_valid
string
"true" or "false".
cover_image_url
string
Signed CloudFront URL, valid for cover_image_expires_in seconds. Don't cache it.
cover_image_expires_in
integer
Seconds until the cover URL expires (600).
box_name
string
Box title.
box_description
string
Box description.
box_tags
string
Comma-separated tags.
PHP
<?php
$ch = curl_init('https://api.bentbox.co/v1/api/get-box-data?boxId=' . urlencode($boxId));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Basic ' . base64_encode("$apiKey:$apiSecret"),
        'Accept: application/json',
    ],
]);
$box = json_decode(curl_exec($ch), true);
curl_close($ch);

Box widget

Show a BentBox box on your site with one <div> and one script. The widget renders the cover, price (with any discount), tags and a Buy button. It gets its data through a small proxy on your server, so your API key never reaches the browser. Buy links carry UTM parameters so the traffic is credited to you.

  1. Your page loads the widgetIt finds each data-bentbox-widget element.
  2. The widget calls your proxyA request to the data-proxy URL on your server.
  3. Your proxy calls BentBoxIt adds your API key and forwards the get-box-data response.
  4. The widget renders the cardBuy sends the visitor to BentBox checkout.

1. Add the proxy to your server

bentbox-proxy.php
<?php
// bentbox-proxy.php: the only place your API key lives
$apiKey    = getenv('BENTBOX_API_KEY');
$apiSecret = getenv('BENTBOX_API_SECRET');

header('Content-Type: application/json');

$boxId = preg_replace('/[^a-zA-Z0-9]/', '', $_GET['boxId'] ?? '');
if ($boxId === '') { http_response_code(400); exit; }

$ch = curl_init('https://api.bentbox.co/v1/api/get-box-data?boxId=' . urlencode($boxId));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Basic ' . base64_encode("$apiKey:$apiSecret"),
        'Accept: application/json',
    ],
]);
$response = curl_exec($ch);
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
curl_close($ch);
echo $response;

2. Embed the widget

Add one <div> per box, and load the script once before </body>.

HTML
<div data-bentbox-widget
     data-box-id="YgyvDwSC"
     data-proxy="/bentbox-proxy.php"
     data-utm-source="your-site-name"></div>

<script src="https://api.bentbox.co/widget/bentbox-widget.js"></script>

Attributes

AttributeDescription
data-box-id
Required
The box to show.
data-proxy
Required
URL of your proxy.
data-buy-url
Optional
Overrides the Buy link. Defaults to https://bentbox.co/buybox?{boxId}.
data-utm-source
Optional
Your site name for analytics. Defaults to bentbox-widget.

What the widget handles

  • Cover image from the signed URL, hidden cleanly if missing.
  • Discount badge with original and discounted price when discount_percent is above 0.
  • “Free” label when the price is 0.00.
  • Up to 5 tags.
  • Loading and error states.
  • utm_source, utm_medium=referral and utm_campaign=partner-embed on the Buy link.
  • No dependencies; styles are injected on first load.

If you add widget elements after the page loads, call window.BentBoxWidget.init() to pick them up.

Get video data

GET/v1/api/get-video-data
Auth
API key (Basic)

Returns a video's details, creator, pricing, cover image and a signed preview URL. It powers the video widget.

Query parameters

ParameterDescription
videoId
string Required
The video ID, for example the video_id from Create a video or the ID in bentbox.co/video?id={videoId}.
promo_code
string Optional
Reserved for future use.
Request
GET https://api.bentbox.co/v1/api/get-video-data?videoId=4taJlrbz
Authorization: Basic base64(api_key:api_secret)
Accept: application/json
Response (200 OK)
{
  "videoId": "4taJlrbz",
  "title": "Behind the scenes",
  "description": "Studio session footage",
  "tags": "studio,bts,fashion",
  "username": "sandeee",
  "user_profile_photo": "https://d6ijsqg8e9unz.cloudfront.net/…",
  "duration": 128.5,
  "filesize": 28471293,
  "width": 1920,
  "height": 1080,
  "adult_content": 0,
  "freely_available": 0,
  "selling_price": 4.99,
  "discount_value": 0.00,
  "discount_percent": 0,
  "currency": "USD",
  "cover_image_url": "https://images.bentbox.co/covers/…",
  "preview_url": "https://d….cloudfront.net/preview.mp4?…",
  "show_video_preview": 1,
  "buy_url": "https://bentbox.co/video?id=4taJlrbz",
  "files_count": 1
}

Response fields

FieldDescription
videoId
string
The public video ID.
title
string
Video title.
description
string
Video description.
tags
string
Comma-separated tags.
username
string
The creator's BentBox username.
user_profile_photo
string
The creator's profile photo.
duration
number
Seconds.
filesize
number
Bytes.
width, height
number
Pixels.
adult_content
integer
1 if flagged adult. Use it to drive your age gate.
freely_available
integer
1 if the video is free to watch.
selling_price
number
USD, 2 decimals. 0.00 if free.
discount_value
number
Always 0.00 for now.
discount_percent
integer
Discount percentage.
currency
string
Always USD.
cover_image_url
string
Stable public poster URL; falls back to a signed thumbnail before the poster is ready.
preview_url
string
Signed preview MP4. Empty when show_video_preview is 0.
show_video_preview
integer
0 if the creator turned previews off.
buy_url
string
The video page on bentbox.co.
files_count
integer
Always 1 for videos.

Video widget

Embed a playable BentBox preview with the cover, creator, price and a Buy button. Like the box widget, it reads data through a proxy on your server and tags Buy links for attribution.

1. Add the proxy to your server

bentbox-video-proxy.php
<?php
// bentbox-video-proxy.php: the only place your API key lives
$apiKey    = getenv('BENTBOX_API_KEY');
$apiSecret = getenv('BENTBOX_API_SECRET');

header('Content-Type: application/json');

// Video IDs may contain letters, digits, '-' and '_' (API-created IDs use '-')
$videoId = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['videoId'] ?? '');
if ($videoId === '') { http_response_code(400); exit; }

$ch = curl_init('https://api.bentbox.co/v1/api/get-video-data?videoId=' . urlencode($videoId));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Basic ' . base64_encode("$apiKey:$apiSecret"),
        'Accept: application/json',
    ],
]);
$response = curl_exec($ch);
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
curl_close($ch);
echo $response;

2. Embed the widget

HTML
<div data-bentbox-video-widget
     data-video-id="4taJlrbz"
     data-proxy="/bentbox-video-proxy.php"
     data-utm-source="your-site-name"
     data-autoplay="0"
     data-loop="1"></div>

<script src="https://api.bentbox.co/widget/bentbox-video-widget.js"></script>

Attributes

AttributeDescription
data-video-id
Required
The video to show.
data-proxy
Required
URL of your proxy.
data-buy-url
Optional
Overrides the Buy link. Defaults to https://bentbox.co/video?id={videoId}.
data-utm-source
Optional
Your site name for analytics. Defaults to bentbox-video-widget.
data-autoplay
Optional
"1" to autoplay. Browsers only allow muted autoplay. Default "0".
data-loop
Optional
"1" to loop the preview. Default "0".

What the widget handles

  • A native HTML5 player (playsinline, preload="metadata"), with no player library.
  • The poster image, with a signed thumbnail as fallback.
  • A “Preview not available” overlay when the creator has turned previews off.
  • Duration as m:ss, discount badge, and “Free” with a Watch button for free videos.
  • The creator's photo and @username, linked to their BentBox page.
  • Up to 5 tags, loading and error states, and UTM parameters on the Buy link.

Call window.BentBoxVideoWidget.init() after adding widget elements dynamically.

Reference

All endpoints

MethodEndpointWhat it doesAuth
GET/oauth/authorizeStart the OAuth flowBrowser
POST/oauth/tokenExchange a code for tokensClient secret
POST/v1/content/videoCreate a videoOAuth
POST/v1/content/upload-urlGet a presigned upload URLOAuth
PUT{upload_url}Upload the fileSigned URL
GET/v1/content/video/{video_id}Get a video's statusOAuth
GET/v1/api/get-box-dataBox details, cover and pricingAPI key
GET/v1/api/get-video-dataVideo details, preview and pricingAPI key
JS/widget/bentbox-widget.jsBox widgetYour proxy
JS/widget/bentbox-video-widget.jsVideo widgetYour proxy

Errors

Errors use standard HTTP status codes and a JSON body. Branch on error; show error_description only in logs or admin views.

{
  "success": false,
  "error": "verification_required",
  "error_description": "User verification is required before uploading videos."
}
HTTPerrorMeaning
—access_deniedThe creator cancelled on the consent screen. Arrives as a query parameter on your redirect_uri.
400invalid_requestA parameter is missing or invalid. error_description says which.
400release_requiredCreate a video was called without sole_performer or release_form_id.
400invalid_grantThe authorization code expired or was already used.
400invalid_clientClient credentials or redirect_uri don't match.
400invalid_scopeYour platform can't request that scope.
401unauthorizedMissing credentials, or the connection wasn't found or isn't active.
401invalid_tokenThe access token is wrong for this connection, or has expired.
403forbiddenNot allowed for this token.
403insufficient_permissionsThe connection doesn't have the upload_video scope.
403verification_requiredThe creator must finish identity verification on BentBox before uploading.
403duration_exceededThe video is longer than the creator's limit. Includes max_duration_seconds and declared_duration_seconds.
404not_foundThe box, video or connection doesn't exist, or the video belongs to another creator.
429rate_limit_exceededToo many requests. Retry after the Retry-After header.
500server_errorSomething failed on our side. Retry with backoff.
Example: duration_exceeded
{
  "success": false,
  "error": "duration_exceeded",
  "error_description": "Video duration exceeds the maximum allowed length of 15 minutes.",
  "max_duration_seconds": 900,
  "declared_duration_seconds": 1240.5
}

Best practices

Security

  • Use HTTPS everywhere, and check state on every OAuth callback.
  • Encrypt access and refresh tokens at rest. Keep them out of URLs, logs and browser code.
  • Keep client_secret and API keys in environment variables or a secrets manager.
  • Use the proxy pattern for widgets so API keys stay on your server.

Errors and retries

  • Check success before using a response.
  • On invalid_token, ask the creator to reconnect.
  • Retry 429 and 5xx with exponential backoff. Don't retry 4xx unchanged.

Uploads

  • Check file type and size, and read the duration from the file, before creating the video.
  • Show upload progress; the PUT returns 200 when done.
  • If the PUT fails, request a fresh upload URL for the same video_id and try again.

Creator experience

  • Explain what you're asking for before sending creators to the consent screen.
  • Let creators disconnect BentBox from your platform at any time.
  • Show each video's status, and tell creators when it's live, using Get video status.
  • When a video is draft or action_required, link straight to its video_settings_url.