Brand Plugin API

Base URL: https://app.elnez.com

Audience: backend engineers integrating an approved Elnez Brand account.

Last updated: August 23, 2026

The Brand Plugin puts Elnez campaigns and bounties inside your own product. Your members see the work, submit their clips, and watch a balance grow in your currency, never in dollars. Elnez verifies the views, applies its fraud checks, and pays your Brand account in USD. How you then credit the member is your decision, made in your own system.

Overview

Three moving parts make up an integration.

Nothing about the direct Elnez creator experience changes. A plugin member is a separate, externally owned identity that exists only inside your integration.

The one sentence to remember. Elnez credits your Brand account in USD, the widget only ever shows your display currency, and the webhook tells you how much to credit the member on your side.

The four access gates

Every runtime call checks all four. Miss any one and new member sessions and submissions stop, with existing data left readable.

GateWhat it meansWho controls it
Brand accountYour Elnez account type is BRAND, not CLIPPERElnez admin
Approved applicationYour Brand Plugin application is APPROVEDElnez owner, by hand
Active integrationYour integration is ACTIVE, not SUSPENDEDElnez owner
Current PremiumYour Premium subscription has not lapsedYou

Premium is never approval. Paying for Premium does not approve a plugin application, and an approved application does not survive a lapsed Premium. They are two independent switches and you need both on.

Setup, end to end

  1. Apply from your Brand accountSign in and open /user/brands/plugin. One application per Brand account. Submitting sends the Elnez owner an email and a bell notification.
  2. Wait for the owner reviewA person reads it. On approval you get an email, an integration is created, and a public key plus a webhook signing secret are generated. On rejection the email carries the reason and you may apply again.
  3. Configure the integrationSet your webhook URL, currency name, currency symbol, optional currency logo, and the bounty conversion value. All of it lives on the same plugin page.
  4. Create a server API keyName it, copy it once. The plaintext is never shown again. Store it the way you store a payment key.
  5. Ask the owner to allowlist campaignsCampaign access is granted one campaign at a time, by an Elnez owner, with a display reward per 1,000 views expressed in your currency.
  6. Wire your backend and your webhookThe rest of this page.

What the application asks for

FieldRulesWhy it matters later
business_namerequired, max 120Shown to members inside the widget
website_urlrequired, HTTPS, max 500Review evidence
platform_typeWEB, MOBILE or BOTHReview evidence
allowed_origins1 to 10 HTTPS origins, no path, query or fragmentLoad bearing. Sets the iframe frame-ancestors, and every member session must name one of these exactly
android_package_nameoptional, max 255Review evidence only, never a substitute for API authentication
ios_bundle_idoptional, max 255Review evidence only, never a substitute for API authentication
audience_sizerequired integer, 1 or moreReview evidence
monthly_active_usersrequired integer, 1 or moreReview evidence
use_caserequired, 30 to 3,000 charactersThe part the owner actually reads
onboarding_modeAPI, WIDGET or BOTHDeclares how you plan to integrate
webhook_urlrequired, HTTPS, max 500, passes the URL guard belowCopied onto the integration at approval

Origins are normalised before storage: lowercased, trailing slash removed, duplicates dropped. https://App.YourBrand.com/ and https://app.yourbrand.com are the same entry. A pending or already approved application blocks a second submission, so edit before you apply.

Integration settings you control after approval

SettingRulesEffect
webhook_urlrequired, HTTPS, max 500Where every earnings event is delivered
currency_namerequired, max 40The full name, for example Skill Coins
currency_symbolrequired, max 12The short label the widget prints, for example SC
currency_logo_urloptional, HTTPS, max 500Your coin artwork
bounty_display_units_per_usdrequired, up to 4 decimals, minimum 0.0001How many of your units equal one USD of bounty reward

Bounties stay locked until the conversion value is saved. Members see open bounties immediately, but every entry is refused with "The Brand must finish its bounty reward setting before members can enter" until bounty_display_units_per_usd is a positive number. The widget bootstrap exposes this as bounty_settings_ready.

Your three credentials

CredentialShapeLivesUsed for
Server API keybpk_ + 48 charsYour backend onlyCreating member sessions
Member tokenbps_ + 48 charsThe browser, 15 minutesEvery widget call
Webhook secretbpwh_ + 48 charsYour backend onlyVerifying webhook signatures
Public keybpp_ + 32 charsYour page markupThe iframe URL. Not a secret

Elnez stores only a SHA-256 hash of the API key and of every member token, so neither can be read back out of the database. The API key is shown once at creation and the webhook secret once at creation or rotation. Afterwards the list shows a masked form, the first 12 characters and the last 4.

Never put the API key or a member token in a URL, a query string, or client-side JavaScript. The API key can create a session for any member ID you name, so a leaked key is a leaked account.

Revoking an API key takes effect on the next request and deletes nothing: members, submissions, earnings, and queued webhooks are untouched. Rotating the webhook secret invalidates the old one immediately, so deploy the new secret before the next event fires. The only scope an API key carries today is member_sessions:create.

Create a member session from your backend

This is the call that starts everything. Your server proves who it is with the API key, names the member with your own stable identifier, and receives a token the browser can safely hold for fifteen minutes.

POST /api/v1/brand-plugin/member-sessions

Creates or reuses the plugin member, then issues a fresh short-lived session token.

Authorization: Bearer bpk_...  ·  30 requests / minute

Request body

FieldTypeRulesNotes
external_user_idstringrequired, max 191Your stable member ID. The same value always resolves to the same plugin member
originstringrequired, max 500Must match one of your approved origins exactly, after lowercasing and trailing-slash removal
namestringoptional, max 160Display only. Never treated as a verified identity
emailstringoptional, valid email, max 255Lowercased and encrypted at rest. Never used for Elnez login or mail
phonestringoptional, max 40Encrypted at rest
Request · cURL
curl -X POST https://app.elnez.com/api/v1/brand-plugin/member-sessions \
  -H "Authorization: Bearer $ELNEZ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "external_user_id": "user_88213",
    "origin": "https://app.yourbrand.com",
    "name": "Ada",
    "email": "ada@example.com"
  }'
Response · 201 Created
{
  "member_token": "bps_EXAMPLE_MEMBER_TOKEN",
  "expires_at": "2026-08-23T14:12:44+00:00",
  "embed_url": "https://app.elnez.com/brand-plugin/embed/bpp_YOUR_PUBLIC_KEY"
}

Node, in your session route

Node.js
// Server side only. This route must be behind your own auth.
app.post('/elnez/session', requireLogin, async (req, res) => {
  const response = await fetch('https://app.elnez.com/api/v1/brand-plugin/member-sessions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.ELNEZ_API_KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({
      external_user_id: String(req.user.id),
      origin: 'https://app.yourbrand.com',
      name: req.user.displayName,
      email: req.user.email,
    }),
  });

  if (!response.ok) {
    const problem = await response.json().catch(() => ({}));
    return res.status(502).json({ error: problem.message || 'Elnez refused the session' });
  }

  const { member_token, expires_at } = await response.json();
  res.json({ member_token, expires_at });   // never log the token
});

PHP

PHP
$response = Http::withToken(env('ELNEZ_API_KEY'))
    ->acceptJson()
    ->post('https://app.elnez.com/api/v1/brand-plugin/member-sessions', [
        'external_user_id' => (string) $user->id,
        'origin'           => 'https://app.yourbrand.com',
        'name'             => $user->display_name,
        'email'            => $user->email,
    ]);

abort_unless($response->successful(), 502, 'Elnez refused the session');

$token = $response->json('member_token');

What the token is, and is not

The identity boundary

Behind the scenes Elnez creates an external_only internal user for each plugin member, purely so the existing linked-account, view-collection, and fraud systems work unchanged. That account cannot sign in with an Elnez one-time code, is excluded from every Elnez broadcast and marketing audience, and holds no wallet of its own. The name, email, and phone you send are display data, not a verified Elnez identity, and are encrypted at rest.

Social account ownership is still proven inside Elnez, by putting a one-time code in the public bio. The global rule that a platform and handle pair can belong to only one account anywhere on Elnez still applies, so a handle already linked elsewhere is refused.

Embed the widget

The widget is a full page served from Elnez and framed by you. It renders in your currency, your brand name, and your allowlisted campaigns, and it never displays a dollar figure.

GET /brand-plugin/embed/{public_key}

The iframe target. Public, no authentication. An unknown key returns 404.

Content-Security-Policy: frame-ancestors 'self' <your approved https origins>

Because frame-ancestors is built from your approved origins, the page simply refuses to render on any other site. That is deliberate: it means a stolen public key is not enough to host your widget somewhere else.

The handshake

  1. The iframe loads and posts { type: 'ELNEZ_PLUGIN_READY' } to its parent.
  2. Your page hears that, fetches a member token from your own backend, and posts { type: 'ELNEZ_MEMBER_SESSION', token } back into the frame.
  3. The widget checks the message origin against your approved list and the token prefix, then bootstraps itself.
Your page
<iframe
  id="elnez-plugin"
  src="https://app.elnez.com/brand-plugin/embed/bpp_YOUR_PUBLIC_KEY"
  title="Earn with Elnez"
  style="width:100%;height:900px;border:0;border-radius:16px"></iframe>

<script>
  const ELNEZ_ORIGIN = 'https://app.elnez.com';
  const frame = document.getElementById('elnez-plugin');

  window.addEventListener('message', async (event) => {
    if (event.origin !== ELNEZ_ORIGIN) return;
    if (event.data?.type !== 'ELNEZ_PLUGIN_READY') return;

    // Your own endpoint, which calls Elnez with the server API key.
    const res = await fetch('/elnez/session', { method: 'POST', credentials: 'same-origin' });
    const { member_token } = await res.json();

    frame.contentWindow.postMessage(
      { type: 'ELNEZ_MEMBER_SESSION', token: member_token },
      ELNEZ_ORIGIN
    );
  });
</script>

Always target the exact Elnez origin in postMessage, and always check event.origin on the way in. Passing '*' as the target broadcasts the member token to whatever happens to be in the frame.

What the member can do inside the widget

Webhooks: how delivery works

Every time money moves for one of your members, Elnez writes an immutable earning event and a delivery record in the same database transaction as the ledger change, then sends it to your endpoint. The money is already correct before the first delivery attempt, and a failed delivery never rolls it back.

Request shape

HeaderValue
Content-Typeapplication/json
X-Elnez-Event-IdThe event UUID, identical to id in the body. Use it to deduplicate
X-Elnez-TimestampUnix seconds, as a string, generated per attempt
X-Elnez-Signaturesha256= followed by the hex HMAC

The method is always POST. Elnez waits up to 10 seconds for your response and does not follow redirects. Any 2xx marks the delivery DELIVERED; anything else is a failure and will be retried.

Rules your webhook URL must satisfy

The URL is validated when you save it and again, with a fresh DNS lookup, before every single delivery attempt. It must:

The address that passed validation is then pinned for the connection, so a DNS answer that changes in between cannot redirect the delivery to an internal host.

Verify the signature

The signed string is the timestamp, a literal dot, and the raw request body, exactly as received.

The recipe
signature = "sha256=" + hex( HMAC_SHA256( key = webhook_secret,
                                          message = timestamp + "." + raw_body ) )

Sign the raw bytes, not a re-serialised object. Parsing the JSON and stringifying it again changes key order and spacing, and the signature will never match. Capture the body before your framework parses it.

Node.js · Express
const crypto = require('crypto');

app.post('/webhooks/elnez',
  express.raw({ type: 'application/json' }),   // keeps the raw body
  (req, res) => {
    const timestamp = req.get('X-Elnez-Timestamp') || '';
    const received  = req.get('X-Elnez-Signature') || '';
    const eventId   = req.get('X-Elnez-Event-Id') || '';

    const expected = 'sha256=' + crypto
      .createHmac('sha256', process.env.ELNEZ_WEBHOOK_SECRET)
      .update(timestamp + '.' + req.body.toString('utf8'))
      .digest('hex');

    const a = Buffer.from(expected), b = Buffer.from(received);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.sendStatus(401);
    }

    // Optional freshness check against replay of an old capture.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.sendStatus(401);
    }

    const event = JSON.parse(req.body.toString('utf8'));

    // Idempotent: the same event id may arrive more than once.
    if (alreadyHandled(eventId)) return res.sendStatus(200);
    creditMember(event.data);
    markHandled(eventId);

    res.sendStatus(200);   // answer fast, do the slow work in a queue
  });
PHP
$raw       = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ELNEZ_TIMESTAMP'] ?? '';
$received  = $_SERVER['HTTP_X_ELNEZ_SIGNATURE'] ?? '';
$eventId   = $_SERVER['HTTP_X_ELNEZ_EVENT_ID'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, getenv('ELNEZ_WEBHOOK_SECRET'));

if (! hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
// deduplicate on $eventId, then credit, then:
http_response_code(200);
Python · Flask
import hmac, hashlib, os
from flask import request, abort

@app.post("/webhooks/elnez")
def elnez_webhook():
    raw       = request.get_data()                       # bytes, unparsed
    timestamp = request.headers.get("X-Elnez-Timestamp", "")
    received  = request.headers.get("X-Elnez-Signature", "")
    event_id  = request.headers.get("X-Elnez-Event-Id", "")

    expected = "sha256=" + hmac.new(
        os.environ["ELNEZ_WEBHOOK_SECRET"].encode(),
        timestamp.encode() + b"." + raw,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, received):
        abort(401)

    event = request.get_json()
    if not already_handled(event_id):
        credit_member(event["data"])
        mark_handled(event_id)
    return "", 200

Event payloads

There is one event type today, member.earnings.updated. What separates a credit from a clawback is data.event_kind.

event_kindSignFires when
ACCRUALpositiveAn approved clip earns from newly verified views. Fires repeatedly as views grow
REVERSALnegativeAn approval is withdrawn after payout and the earnings are clawed back
DEDUCTIONnegativeAccrued earnings are removed before payout, for example on a fraud finding
BOUNTY_APPROVEDpositiveA staff reviewer approves your member's bounty entry and the reward is paid

Campaign earnings

member.earnings.updated · ACCRUAL
{
  "id": "0f9a5c6e-3d21-4a77-9b4c-2f6b1d8e5a30",
  "type": "member.earnings.updated",
  "created_at": "2026-08-23T13:41:07+00:00",
  "data": {
    "member_id": "user_88213",
    "submission_id": 4821,
    "campaign_id": 37,
    "event_kind": "ACCRUAL",
    "display_delta": "125.0000",
    "display_total": "1875.0000",
    "currency": {
      "name": "Skill Coins",
      "symbol": "SC"
    },
    "completed": false
  }
}
member.earnings.updated · REVERSAL
{
  "id": "b71c4d02-8e55-49af-8f1a-6c0d9e2b4713",
  "type": "member.earnings.updated",
  "created_at": "2026-08-23T16:02:55+00:00",
  "data": {
    "member_id": "user_88213",
    "submission_id": 4821,
    "campaign_id": 37,
    "event_kind": "REVERSAL",
    "display_delta": "-1875.0000",
    "display_total": "0.0000",
    "currency": {
      "name": "Skill Coins",
      "symbol": "SC"
    },
    "completed": false
  }
}

Bounty earnings

A bounty event carries source_type, a null submission_id, and the two bounty identifiers instead.

member.earnings.updated · BOUNTY_APPROVED
{
  "id": "5c8e1f34-96b7-4c02-a1d8-77e3b5904cf6",
  "type": "member.earnings.updated",
  "created_at": "2026-08-23T18:20:11+00:00",
  "data": {
    "member_id": "user_88213",
    "source_type": "BOUNTY",
    "submission_id": null,
    "bounty_submission_id": 312,
    "bounty_id": 44,
    "campaign_id": null,
    "event_kind": "BOUNTY_APPROVED",
    "display_delta": "5000.0000",
    "display_total": "5000.0000",
    "currency": {
      "name": "Skill Coins",
      "symbol": "SC"
    },
    "completed": true
  }
}

Field by field

FieldTypeMeaning
iduuidStable event identifier. Same value as X-Elnez-Event-Id. Deduplicate on this
typestringAlways member.earnings.updated today. Treat an unknown type as ignorable, not as an error
created_atISO 8601When the event was recorded, not when this attempt was sent
data.member_idstringYour own external_user_id, unchanged
data.source_typestringPresent as BOUNTY on bounty events. Absent on campaign events, where you should read it as CAMPAIGN
data.submission_idint or nullThe Elnez clip submission. Null on bounty events
data.bounty_submission_idintBounty events only. The member's entry
data.bounty_idintBounty events only
data.campaign_idint or nullThe campaign. Null for a global bounty
data.event_kindstringOne of the four kinds above
data.display_deltastring, 4 dpThe change to apply, in your units. Negative on REVERSAL and DEDUCTION
data.display_totalstring, 4 dpRunning total in your units for that submission, floored at zero
data.currencyobjectThe name and symbol snapshotted when the member submitted
data.completedbooleanTrue when nothing more will be earned: the submission hit its cap, or the bounty was paid

Amounts arrive as strings. They are fixed-point decimals with four places, produced by Elnez's decimal money helpers. Parse them with a decimal type, not a float, if you are going to add them up.

Retries, idempotency, and replay

Answer 200 first, work afterwards. Verify the signature, record the event, return 200, and do the crediting in a background job. A handler that does slow work inline is the usual cause of a retry storm.

Endpoint reference

All widget endpoints authenticate with a member token: Authorization: Bearer bps_.... Send Accept: application/json.

POST /api/v1/brand-plugin/member-sessions

Server to server. Documented in full above.

Bearer bpk_ · 30 / minute

GET /api/v1/brand-plugin/widget/bootstrap

Everything the widget renders: the member, your currency, their linked accounts, your allowlisted campaigns, the open bounties, and their history. This is the one endpoint that still answers when your integration is paused, so history stays readable.

Bearer bps_ · 120 / minute

Response · 200 OK, abbreviated
{
  "read_only": false,
  "member": { "external_user_id": "user_88213" },
  "currency": { "name": "Skill Coins", "symbol": "SC", "logo_url": null },
  "accounts": [
    {
      "id": 91,
      "platform": "TIKTOK",
      "handle": "adaclips",
      "verification_code": null,
      "verified_status": "VERIFIED",
      "verified_at": "2026-08-22T09:14:02.000000Z"
    }
  ],
  "campaigns": [
    {
      "id": 37,
      "title": "Summer drop, short form",
      "description": "Clip the launch stream.",
      "accepted_platforms": ["TIKTOK", "INSTAGRAM"],
      "content_requirements": "Keep the logo visible.",
      "require_demographics": true,
      "display_reward_per_1k": "250.0000"
    }
  ],
  "bounty_settings_ready": true,
  "bounties": [
    {
      "id": 44,
      "title": "Best behind the scenes cut",
      "description": "One minute maximum.",
      "campaign_id": null,
      "campaign_title": null,
      "attachment_url": null,
      "attachment_name": null,
      "winner_slots": 3,
      "remaining_slots": 2,
      "display_reward_per_winner": "5000.0000"
    }
  ],
  "submissions": [
    {
      "id": 4821,
      "campaign_id": 37,
      "platform": "TIKTOK",
      "social_url": "https://www.tiktok.com/@adaclips/video/7412...",
      "status": "APPROVED",
      "verified_views": 7500,
      "payout_status": "PAID",
      "created_at": "2026-08-22T10:03:11.000000Z",
      "display_total": "1875.0000",
      "completed": true
    }
  ],
  "bounty_submissions": [
    {
      "id": 312,
      "bounty_id": 44,
      "bounty_title": "Best behind the scenes cut",
      "url": "https://www.tiktok.com/@adaclips/video/7419...",
      "note": null,
      "status": "APPROVED",
      "created_at": "2026-08-23T08:44:50.000000Z",
      "display_reward": "5000.0000",
      "currency_symbol": "SC"
    }
  ]
}
POST /api/v1/brand-plugin/widget/bio-code/start

Begins social account verification. Body: platform (TIKTOK, INSTAGRAM, YOUTUBE, X) and url, either a profile URL or a bare handle. Returns the code the member must paste into their public bio.

Bearer bps_ · 10 / minute

Response · 201 Created
{
  "account_id": 91,
  "platform": "TIKTOK",
  "handle": "adaclips",
  "code": "ELNEZ-4KQ2"
}
POST /api/v1/brand-plugin/widget/bio-code/{account_id}/verify

Reads the public bio and confirms the code is there. Returns { "verified": true }. A missing code answers 422 so the member can save the bio and try again.

Bearer bps_ · 10 / minute

POST /api/v1/brand-plugin/widget/submissions

Submits a clip. Sent as multipart/form-data because of the screenshot.

Bearer bps_ · 20 / minute

FieldRules
campaign_idrequired, must be enabled for your integration and currently live
platformrequired, one of TIKTOK, INSTAGRAM, YOUTUBE, X, FACEBOOK, and accepted by that campaign
linked_account_idrequired, must belong to this member, match the platform, and be VERIFIED
social_urlrequired HTTPS URL, max 500, globally unique across all Elnez submissions, host must match the platform
titleoptional, max 255
posted_atoptional, not in the future. Defaults to now
demographics_screenshotJPEG or PNG, max 5 MB. Required when the campaign sets require_demographics
acknowledgementrequired, must be truthy

The clip must be submitted within the campaign's posting window, thirty minutes by default, measured from posted_at. Response: 201 with { "submission_id": 4821, "status": "PENDING" }. View collection starts immediately in the background.

POST /api/v1/brand-plugin/widget/bounty-submissions

Enters an open bounty. JSON body: bounty_id, url, optional note up to 500 characters. Response 201 with { "bounty_submission_id": 312, "status": "PENDING" }.

Bearer bps_ · 20 / minute

A member may hold only one PENDING or APPROVED entry per bounty. Global bounties appear in every plugin automatically; a campaign-specific bounty appears only when that campaign is enabled for your integration. Your Brand cannot create, edit, open, or close bounties: Elnez staff own that surface, and your members simply see the open ones.

Brand settings, from your signed-in Elnez session

These are ordinary web routes on /user/brands/plugin, not part of the token API. They exist here so you know what the buttons on that page do.

RouteDoes
POST /applicationSubmits the plugin application
PATCH /settingsSaves webhook URL, currency, and bounty conversion
POST /api-keysCreates a key and shows the plaintext once
POST /api-keys/{id}/revokeRevokes a key, deleting nothing
POST /webhook-secret/rotateIssues a new signing secret and shows it once

Display currency and the money model

Elnez stays the single authority on verified views and on USD. Your currency is a presentation layer that Elnez calculates for you and never treats as a second wallet.

Campaigns

When a member submits, Elnez snapshots the campaign's display rate onto that submission. Every later accrual is converted with the snapshot, so a rate change tomorrow never rewrites what was earned yesterday.

Conversion
display_delta = (usd_delta / campaign_reward_per_1k_usd) * snapshotted_display_reward_per_1k

So a campaign paying $2.00 per 1,000 views, allowlisted to you at 250 SC per 1,000 views, turns a $1.00 accrual into 125.0000 SC.

Bounties

Conversion
display_delta = usd_paid * snapshotted_bounty_display_units_per_usd

The conversion value is snapshotted at entry, and the webhook is calculated from the exact USD share actually paid, including the rounding remainder that falls to the final winner. The widget shows an estimated per-winner reward before entry and the real figure afterwards.

Who gets paid

Approval, the 24-hour payout buffer, per-video caps, campaign budget caps, bot filtering, and the review ladder all work exactly as they do for direct creators.

Rate limits

Limits are per minute and keyed by calling IP address, since these endpoints are reached before any Elnez login. The prefix limit applies on top of each route's own limit.

EndpointLimit
Everything under /api/v1/brand-plugin120 / minute
POST /member-sessions30 / minute
GET /widget/bootstrap120 / minute
POST /widget/bio-code/start and /verify10 / minute each
POST /widget/submissions20 / minute
POST /widget/bounty-submissions20 / minute

Exceeding a limit returns 429 with a Retry-After header. If your traffic legitimately needs more headroom, ask before you engineer around it.

Errors

Errors come back as JSON with a message, and validation failures add an errors object keyed by field.

StatusMessage you will seeWhat to do
401A valid Brand Plugin API key is required.The header is missing, or the key does not start with bpk_
401This Brand Plugin API key is invalid or revoked.Wrong key, revoked key, or an expiry that has passed
401A valid member session is required.The widget call had no bps_ token
401This member session has expired.Past 15 minutes, or revoked. Mint a new token
403This Brand Plugin is paused.Approval, active status, or Premium is missing. Check all four gates
403This origin is not approved.The origin you sent is not in the approved list. Compare it character by character
403This member is blocked.The member's status is not ACTIVE
404Not foundUnknown public key on the embed URL
422This campaign is not available in the Brand Plugin.Not allowlisted for you, or the entry is disabled
422This campaign is not accepting submissions.The campaign is not live, or it requires an application
422Verify the matching social account before submitting.The linked account is unverified, or belongs to another platform
422Submit within N minutes of posting.The posting window has closed for that clip
422That social account is already linked to another member.Handles are globally unique across Elnez
422The Brand must finish its bounty reward setting before members can enter.Save bounty_display_units_per_usd
422You already have a pending or approved entry for this bounty.One live entry per bounty per member
429Too Many RequestsBack off and honour Retry-After

Suspension and Premium lapse

Both states behave the same way, and neither destroys anything.

BehaviourWhile paused
New member sessionsRefused with 403
New clip and bounty submissionsRefused with 403
Bio-code verificationRefused with 403
The widget bootstrapStill answers, with read_only: true
Existing members, submissions, and historyKept and readable
USD already being earnedContinues to settle to your Brand account
Queued webhooksContinue to deliver

Restoring Premium, or an owner lifting a suspension, resumes the same integration with the same public key, keys, members, and history. Nothing needs rebuilding.

Go-live checklist