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.
| Credential | Used for | How to send it |
|---|---|---|
| OAuth access token | Acting 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 secret | Reading 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): putaccess_tokenandconnection_idin the JSON body. Also sendAuthorization: Bearer {access_token}. - GET requests (
/v1/content/video/{video_id}): sendAuthorization: Bearer {access_token}and passconnection_idas a query parameter or anX-Connection-Idheader. Tokens never go in a URL.
Base URL
https://api.bentbox.coGetting 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.
- Send the creator to BentBoxThey click “Connect BentBox” on your site and you redirect them to
/oauth/authorize. - They sign in and approveBentBox shows its sign-in page if needed, then a consent screen listing the scopes you asked for.
- BentBox redirects back with a codeThe creator lands on your
redirect_uriwith a short-livedcodeand yourstate. - Your server exchanges the codeA server-to-server
POST /oauth/tokenreturns theaccess_token,refresh_token,connection_idanduser_id. - Store the credentialsEncrypt the tokens at rest and keep them with your user record.
Token exchange
/oauth/tokenSwap 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)
| Parameter | Description |
|---|---|
grant_type | Always authorization_code. |
code | The code from the callback. |
client_id | Your platform's client ID. |
client_secret | Your platform's client secret. |
redirect_uri | The same redirect_uri you sent to /oauth/authorize. |
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"
}{
"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.
| Parameter | Description |
|---|---|
ref | Your referral code. Letters and numbers only, up to 32 characters. It must be registered with BentBox first. |
src | 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=codeExisting 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));| Situation | Use |
|---|---|
| Creator is new to BentBox | Sign-up first, with ref and src |
| Creator already has an account | state, for your own attribution records |
| You don't know | Sign-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.
- Create the video
POST /v1/content/videowith the title, file details and consent. You get thevideo_idand the video's page links. - Get an upload URL
POST /v1/content/upload-urlchecks the creator's verification and duration limit, then returns a presigned URL. - Upload the filePUT the bytes to that URL. BentBox starts processing automatically.
- 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
/v1/content/videoCreates 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)
| Parameter | Description |
|---|---|
access_token | The creator's access token. Also send it as Authorization: Bearer. |
connection_id | The connection ID from the token exchange. |
title | Video title, up to 200 characters. |
filename | File name with extension: .mp4, .mov, .avi, .wmv, .flv, .mkv or .webm. |
filesize | File size in bytes, up to 2 GB. |
duration | Duration in seconds. Checked against the creator's length limit when you request the upload URL. |
sole_performer | true if the creator is the only person in the video. Required unless you send release_form_id. |
release_form_id | ProntoID reference to a signed release covering the other performers. Required unless sole_performer is true. |
description | Up to 5,000 characters. |
tags | Up to 20 tags, each up to 50 characters. |
price | Price in USD, 0.99 to 999.99. |
is_premium | Marks the video as premium content. |
publish_on_ready | 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
}'{
"access_token": "bx_live_abc123",
"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.
{
"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
/v1/content/upload-urlReturns 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)
| Parameter | Description |
|---|---|
access_token | The creator's access token. Also send it as Authorization: Bearer. |
connection_id | Must be the same connection that created the video. |
video_id | From Create a video. |
title | Video title, up to 255 characters. |
price | Sale price in USD. 0 for free, up to 9,999.99. |
duration | Duration in seconds. The default limit is 15 minutes; some accounts allow longer. |
description | Up to 5,000 characters. |
tags | Up to 20 tags. |
adult_content | Defaults to true. |
{
"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
}{
"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
{upload_url}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"import { createReadStream, statSync } from "node:fs";
const res = await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": "video/mp4", "Content-Length": String(statSync(path).size) },
body: createReadStream(path),
duplex: "half",
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);import requests
with open("session.mp4", "rb") as f:
r = requests.put(upload_url, data=f, headers={"Content-Type": "video/mp4"})
r.raise_for_status()<?php
$fh = fopen($path, 'rb');
$ch = curl_init($uploadUrl);
curl_setopt_array($ch, [
CURLOPT_PUT => true,
CURLOPT_INFILE => $fh,
CURLOPT_INFILESIZE => filesize($path),
CURLOPT_HTTPHEADER => ['Content-Type: video/mp4'],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
$ok = curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200;
curl_close($ch);
fclose($fh);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
/v1/content/video/{video_id}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
| Parameter | Description |
|---|---|
video_id | The video_id from Create a video. |
connection_id | The creator's connection ID. You can send it as an X-Connection-Id header instead. |
Authorization | 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"const res = await fetch(
`https://api.bentbox.co/v1/content/video/${videoId}?connection_id=${connectionId}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const video = await res.json();
if (!video.success) throw new Error(video.error_description);
console.log(video.status, video.is_live);import requests
r = requests.get(
f"https://api.bentbox.co/v1/content/video/{video_id}",
params={"connection_id": connection_id},
headers={"Authorization": f"Bearer {access_token}"},
timeout=10,
)
video = r.json()
print(video["status"], video["is_live"])<?php
$url = 'https://api.bentbox.co/v1/content/video/' . rawurlencode($videoId)
. '?connection_id=' . rawurlencode($connectionId);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accessToken],
]);
$video = json_decode(curl_exec($ch), true);
curl_close($ch);{
"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
| Status | What it means | What to do |
|---|---|---|
| awaiting_upload | Created; no file received yet. | Upload the file. |
| processing | File received and being transcoded. | Wait. Usually a few minutes. |
| draft | Processed 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_review | Published and waiting for BentBox review. | Wait. It goes live when approved. |
| action_required | Approved, but the creator still needs to finish the video's settings. | Send the creator to video_settings_url. |
| live | Public at video_page_url. | Show the link. Stop polling. |
| upload_expired | No file arrived within 24 hours. | Create the video again. |
| processing_failed | The file couldn't be processed. | Check the file, create the video again and re-upload. |
| removed | The video was deleted. | Stop polling. |
Response fields
| Field | Description |
|---|---|
status | One of the values above. Build your logic on this field. |
status_description | A plain-language sentence you can show as-is. |
is_live | true only when status is live. |
processing | awaiting_upload, pending, in_progress, complete or failed. |
published | Whether the video is set to be public. |
approved | Whether BentBox has approved it. |
title | The video title. |
duration | Measured duration in seconds, once processed. |
video_page_url | Public page. Viewers see it only when live; the creator can open it at any time. |
video_settings_url | Where the creator edits, publishes and completes the video. Requires them to be signed in to BentBox. |
created_at | ISO 8601, UTC. Only available for about a week after creation. |
checked_at | When this status was read, ISO 8601, UTC. |
Video IDs and links
The video_id returned by Create a video is the video's public BentBox ID. It's the same value the BentBox pages, Get video data and the video widget use.
| Page | URL | Who it's for |
|---|---|---|
| Video page | https://bentbox.co/video?id={video_id} | Everyone, once the video is live. The creator can preview it earlier. |
| Video settings | https://bentbox.co/video_settings?{video_id} | The creator only. Edit details, publish, or finish anything BentBox asks for. |
The create and status responses include both links as video_page_url and video_settings_url, so you don't need to build them yourself.
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);# pip install flask requests
import os, secrets, requests
from flask import Flask, redirect, request, session, jsonify
API = "https://api.bentbox.co"
CLIENT_ID = os.environ["BENTBOX_CLIENT_ID"]
CLIENT_SECRET = os.environ["BENTBOX_CLIENT_SECRET"]
REDIRECT_URI = "https://yourplatform.com/oauth/callback"
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET"]
# 1. Send the creator to BentBox
@app.get("/connect-bentbox")
def connect():
session["oauth_state"] = state = secrets.token_urlsafe(16)
q = requests.compat.urlencode({
"client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI,
"scope": "upload_video", "state": state, "response_type": "code",
})
return redirect(f"{API}/oauth/authorize?{q}")
# 2. Exchange the code and store the connection
@app.get("/oauth/callback")
def callback():
if request.args.get("state") != session.pop("oauth_state", None):
return "Invalid state", 400
t = requests.post(f"{API}/oauth/token", json={
"grant_type": "authorization_code", "code": request.args["code"],
"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "redirect_uri": REDIRECT_URI,
}, timeout=15).json()
db.save_connection(session["user_id"], connection_id=t["connection_id"],
access_token=encrypt(t["access_token"]),
refresh_token=encrypt(t["refresh_token"]))
return redirect("/dashboard?connected=1")
# 3. Create, get upload URL, upload
@app.post("/upload-video")
def upload_video():
conn = db.get_connection(session["user_id"])
token = decrypt(conn.access_token)
auth = {"Authorization": f"Bearer {token}"}
body = request.json
path, title = body["path"], body["title"]
price, duration = body.get("price", 0), body["duration"]
created = requests.post(f"{API}/v1/content/video", headers=auth, json={
"access_token": token, "connection_id": conn.connection_id,
"title": title, "filename": "video.mp4", "filesize": os.path.getsize(path),
"duration": duration, "price": price, "sole_performer": True,
}, timeout=15).json()
if not created.get("success"):
return jsonify(created), 400
signed = requests.post(f"{API}/v1/content/upload-url", headers=auth, json={
"access_token": token, "connection_id": conn.connection_id,
"video_id": created["video_id"], "title": title, "price": price, "duration": duration,
}, timeout=15).json()
if not signed.get("success"):
return jsonify(signed), 400
with open(path, "rb") as f:
requests.put(signed["upload_url"], data=f,
headers={"Content-Type": "video/mp4"}).raise_for_status()
return {"video_id": created["video_id"], "settings": created["video_settings_url"]}
# 4. Status for your UI
@app.get("/videos/<video_id>/status")
def video_status(video_id):
conn = db.get_connection(session["user_id"])
r = requests.get(f"{API}/v1/content/video/{video_id}",
params={"connection_id": conn.connection_id},
headers={"Authorization": f"Bearer {decrypt(conn.access_token)}"},
timeout=10)
return r.json(), r.status_code<?php
const API = 'https://api.bentbox.co';
const REDIRECT_URI = 'https://yourplatform.com/callback.php';
define('CLIENT_ID', getenv('BENTBOX_CLIENT_ID'));
define('CLIENT_SECRET', getenv('BENTBOX_CLIENT_SECRET'));
function bentbox(string $method, string $path, ?array $body = null, ?string $token = null): array {
$ch = curl_init(API . $path);
$headers = ['Accept: application/json'];
if ($token) $headers[] = 'Authorization: Bearer ' . $token;
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$res = json_decode(curl_exec($ch), true) ?? [];
curl_close($ch);
return $res;
}
// 1. Send the creator to BentBox
function connectBentBox(): void {
$_SESSION['oauth_state'] = bin2hex(random_bytes(16));
header('Location: ' . API . '/oauth/authorize?' . http_build_query([
'client_id' => CLIENT_ID, 'redirect_uri' => REDIRECT_URI,
'scope' => 'upload_video', 'state' => $_SESSION['oauth_state'], 'response_type' => 'code',
]));
exit;
}
// 2. Exchange the code and store the connection
function handleCallback(): void {
if (!hash_equals($_SESSION['oauth_state'] ?? '', $_GET['state'] ?? '')) exit('Invalid state');
$t = bentbox('POST', '/oauth/token', [
'grant_type' => 'authorization_code', 'code' => $_GET['code'],
'client_id' => CLIENT_ID, 'client_secret' => CLIENT_SECRET, 'redirect_uri' => REDIRECT_URI,
]);
saveConnection($_SESSION['user_id'], $t['connection_id'],
encrypt($t['access_token']), encrypt($t['refresh_token']));
header('Location: /dashboard?connected=1');
}
// 3. Create, get upload URL, upload
function uploadVideo(string $path, string $title, float $price, float $duration): array {
$c = getConnection($_SESSION['user_id']);
$token = decrypt($c['access_token']);
$auth = ['access_token' => $token, 'connection_id' => $c['connection_id']];
$created = bentbox('POST', '/v1/content/video', $auth + [
'title' => $title, 'filename' => 'video.mp4', 'filesize' => filesize($path),
'duration' => $duration, 'price' => $price, 'sole_performer' => true,
], $token);
if (empty($created['success'])) return $created;
$signed = bentbox('POST', '/v1/content/upload-url', $auth + [
'video_id' => $created['video_id'], 'title' => $title,
'price' => $price, 'duration' => $duration,
], $token);
if (empty($signed['success'])) return $signed;
$fh = fopen($path, 'rb');
$ch = curl_init($signed['upload_url']);
curl_setopt_array($ch, [
CURLOPT_PUT => true, CURLOPT_INFILE => $fh, CURLOPT_INFILESIZE => filesize($path),
CURLOPT_HTTPHEADER => ['Content-Type: video/mp4'], CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
$ok = curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200;
curl_close($ch);
fclose($fh);
return ['success' => $ok, 'video_id' => $created['video_id'],
'settings' => $created['video_settings_url']];
}
// 4. Status for your UI
function videoStatus(string $videoId): array {
$c = getConnection($_SESSION['user_id']);
return bentbox('GET', '/v1/content/video/' . rawurlencode($videoId)
. '?connection_id=' . rawurlencode($c['connection_id']), null, decrypt($c['access_token']));
}Embed content
Get box data
/v1/api/get-box-dataReturns 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
| Parameter | Description |
|---|---|
boxId | The BentBox box ID. |
promo_code | Promo code to apply to the price. |
GET https://api.bentbox.co/v1/api/get-box-data?boxId=YgyvDwSC
Authorization: Basic base64(api_key:api_secret)
Accept: application/json{
"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
| Field | Description |
|---|---|
selling_price | Price before transaction fees, 2 decimals. |
selling_price_with_transaction_fees | What the buyer pays, including fees. |
user_price | What the creator receives. |
transaction_fees | Transaction fee amount. |
referral_fee | Referral fee, if any. |
discount_value | Discount amount applied. |
discount_percent | Discount percentage applied. |
promo_code_valid | "true" or "false". |
cover_image_url | Signed CloudFront URL, valid for cover_image_expires_in seconds. Don't cache it. |
cover_image_expires_in | Seconds until the cover URL expires (600). |
box_name | Box title. |
box_description | Box description. |
box_tags | Comma-separated tags. |
<?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.
- Your page loads the widgetIt finds each
data-bentbox-widgetelement. - The widget calls your proxyA request to the
data-proxyURL on your server. - Your proxy calls BentBoxIt adds your API key and forwards the
get-box-dataresponse. - The widget renders the cardBuy sends the visitor to BentBox checkout.
1. Add the proxy to your server
<?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>.
<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
| Attribute | Description |
|---|---|
data-box-id | The box to show. |
data-proxy | URL of your proxy. |
data-buy-url | Overrides the Buy link. Defaults to https://bentbox.co/buybox?{boxId}. |
data-utm-source | 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_percentis above 0. - “Free” label when the price is
0.00. - Up to 5 tags.
- Loading and error states.
utm_source,utm_medium=referralandutm_campaign=partner-embedon 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
/v1/api/get-video-dataReturns a video's details, creator, pricing, cover image and a signed preview URL. It powers the video widget.
Query parameters
| Parameter | Description |
|---|---|
videoId | The video ID, for example the video_id from Create a video or the ID in bentbox.co/video?id={videoId}. |
promo_code | Reserved for future use. |
GET https://api.bentbox.co/v1/api/get-video-data?videoId=4taJlrbz
Authorization: Basic base64(api_key:api_secret)
Accept: application/json{
"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
| Field | Description |
|---|---|
videoId | The public video ID. |
title | Video title. |
description | Video description. |
tags | Comma-separated tags. |
username | The creator's BentBox username. |
user_profile_photo | The creator's profile photo. |
duration | Seconds. |
filesize | Bytes. |
width, height | Pixels. |
adult_content | 1 if flagged adult. Use it to drive your age gate. |
freely_available | 1 if the video is free to watch. |
selling_price | USD, 2 decimals. 0.00 if free. |
discount_value | Always 0.00 for now. |
discount_percent | Discount percentage. |
currency | Always USD. |
cover_image_url | Stable public poster URL; falls back to a signed thumbnail before the poster is ready. |
preview_url | Signed preview MP4. Empty when show_video_preview is 0. |
show_video_preview | 0 if the creator turned previews off. |
buy_url | The video page on bentbox.co. |
files_count | 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
<?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
<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
| Attribute | Description |
|---|---|
data-video-id | The video to show. |
data-proxy | URL of your proxy. |
data-buy-url | Overrides the Buy link. Defaults to https://bentbox.co/video?id={videoId}. |
data-utm-source | Your site name for analytics. Defaults to bentbox-video-widget. |
data-autoplay | "1" to autoplay. Browsers only allow muted autoplay. Default "0". |
data-loop | "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
| Method | Endpoint | What it does | Auth |
|---|---|---|---|
| GET | /oauth/authorize | Start the OAuth flow | Browser |
| POST | /oauth/token | Exchange a code for tokens | Client secret |
| POST | /v1/content/video | Create a video | OAuth |
| POST | /v1/content/upload-url | Get a presigned upload URL | OAuth |
| PUT | {upload_url} | Upload the file | Signed URL |
| GET | /v1/content/video/{video_id} | Get a video's status | OAuth |
| GET | /v1/api/get-box-data | Box details, cover and pricing | API key |
| GET | /v1/api/get-video-data | Video details, preview and pricing | API key |
| JS | /widget/bentbox-widget.js | Box widget | Your proxy |
| JS | /widget/bentbox-video-widget.js | Video widget | Your 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."
}| HTTP | error | Meaning |
|---|---|---|
| — | access_denied | The creator cancelled on the consent screen. Arrives as a query parameter on your redirect_uri. |
| 400 | invalid_request | A parameter is missing or invalid. error_description says which. |
| 400 | release_required | Create a video was called without sole_performer or release_form_id. |
| 400 | invalid_grant | The authorization code expired or was already used. |
| 400 | invalid_client | Client credentials or redirect_uri don't match. |
| 400 | invalid_scope | Your platform can't request that scope. |
| 401 | unauthorized | Missing credentials, or the connection wasn't found or isn't active. |
| 401 | invalid_token | The access token is wrong for this connection, or has expired. |
| 403 | forbidden | Not allowed for this token. |
| 403 | insufficient_permissions | The connection doesn't have the upload_video scope. |
| 403 | verification_required | The creator must finish identity verification on BentBox before uploading. |
| 403 | duration_exceeded | The video is longer than the creator's limit. Includes max_duration_seconds and declared_duration_seconds. |
| 404 | not_found | The box, video or connection doesn't exist, or the video belongs to another creator. |
| 429 | rate_limit_exceeded | Too many requests. Retry after the Retry-After header. |
| 500 | server_error | Something failed on our side. Retry with backoff. |
{
"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
stateon every OAuth callback. - Encrypt access and refresh tokens at rest. Keep them out of URLs, logs and browser code.
- Keep
client_secretand 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
successbefore using a response. - On
invalid_token, ask the creator to reconnect. - Retry
429and5xxwith exponential backoff. Don't retry4xxunchanged.
Uploads
- Check file type and size, and read the duration from the file, before creating the video.
- Show upload progress; the
PUTreturns200when done. - If the
PUTfails, request a fresh upload URL for the samevideo_idand 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
draftoraction_required, link straight to itsvideo_settings_url.