Brand Plugin API

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

Audience: backend engineers integrating an approved Elnez Brand account.

Last updated: August 26, 2026

The Brand Plugin puts campaigns and bounties inside your own product. Use the Elnez-paid path when Elnez is responsible for the reward, or the tracking-only path when another platform pays the member. Both paths use the same Plugin, Premium plan, member session, social-account verification, and view tracking.

Overview

Three moving parts make up an integration.

Choose a payment mode for each campaign

The Brand Plugin supports both models without creating a second integration.

ModeWhat members seeWhat Elnez doesWho pays
Paid through ElnezCampaigns enabled for your integration and open bounties created by Elnez staffCollects the submission, measures verified views, applies fraud controls, credits your Brand account, and sends signed earnings webhooksElnez pays your Brand account. Your system then credits the member
Tracking onlyOutside-paid campaigns submitted by your Brand and approved by ElnezMeasures verified views from the campaign baseline and shows performance historyThe outside platform pays the member

Access requirements

The Plugin remains available while all four requirements are met. If one is missing, new member sessions and submissions pause while existing history remains available.

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

Plugin access requires both an approved application and an active Premium subscription.

Setup, end to end

  1. Apply from your Brand accountSign in and open /user/brands/plugin. One application may be active per Brand account.
  2. Wait for reviewWhen approved, you receive an email and your public key and webhook signing secret become available. If rejected, the email includes 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. Add the work your members should seeFor Elnez-paid campaigns, request campaign access and set the display reward. For outside-paid work, submit a tracking-only campaign from the same Plugin page and wait for approval.
  6. Connect your backend and webhookComplete the member-session, widget, and webhook setup described below.

What the application asks for

FieldRulesWhy it matters later
business_namerequired, max 120Shown to members inside the widget
website_urlrequired, HTTPS, max 500Your public website
platform_typeWEB, MOBILE or BOTHWhere you will embed the Plugin
allowed_origins1 to 10 HTTPS origins, no path, query or fragmentDefines where the widget may be embedded. Every member session must name one of these origins exactly
android_package_nameoptional, max 255Your Android application identifier
ios_bundle_idoptional, max 255Your iOS application identifier
audience_sizerequired integer, 1 or moreYour total audience size
monthly_active_usersrequired integer, 1 or moreYour monthly active members
use_caserequired, 30 to 3,000 charactersHow you plan to use the Plugin
onboarding_modeAPI, WIDGET or BOTHDeclares how you plan to integrate
webhook_urlrequired, HTTPS, max 500, passes the URL guard belowReceives earnings events after approval

Origins are matched after lowercasing and trailing-slash removal, and duplicates are ignored. https://App.YourBrand.com/ and https://app.yourbrand.com are the same entry. A pending or approved application blocks a second submission, so make any changes before applying.

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

The API key is shown once at creation, and the webhook secret is shown once at creation or rotation. Afterwards, the Plugin page displays only a masked value.

Never put the API key or a member token in a URL, a query string, or client-side JavaScript. Keep the API key on your server.

Revoking an API key takes effect on the next request without affecting existing members or history. Rotating the webhook secret invalidates the previous secret immediately, so deploy the new secret before rotating it. The API key scope 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
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 });
});

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

Social account verification

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>

The widget renders only on your approved origins. A public key alone cannot be used to embed it on another site.

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;

    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. Never use '*' as the target.

What the member can do inside the widget

Webhooks: how delivery works

Elnez sends a signed earnings event when an Elnez-paid campaign or bounty changes a member's credited amount.

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. Capture the body before your framework parses it.

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

app.post('/webhooks/elnez',
  express.raw({ type: 'application/json' }),
  (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);
    }

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.sendStatus(401);
    }

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

    if (alreadyHandled(eventId)) return res.sendStatus(200);
    creditMember(event.data);
    markHandled(eventId);

    res.sendStatus(200);
  });
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);
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. Parse them with a decimal type, not a float.

Retries, idempotency, and replay

Verify the signature, record the event, return 200, and complete longer processing in a background job.

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, currency, linked accounts, Elnez-paid campaigns, approved tracking-only campaigns, open bounties, and both kinds of 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"
    }
  ],
  "external_tracking_enabled": true,
  "external_campaigns": [
    {
      "id": 12,
      "title": "Outside creator campaign",
      "external_campaign_url": "https://campaign.example/work/launch",
      "description": "Payment stays on the campaign platform.",
      "accepted_platforms": ["TIKTOK", "INSTAGRAM"],
      "content_requirements": "Keep the product visible.",
      "starts_at": "2026-08-25T00:00:00.000000Z",
      "ends_at": "2026-09-15T23:59:59.000000Z"
    }
  ],
  "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
    }
  ],
  "tracked_posts": [
    {
      "id": 704,
      "external_campaign_id": 12,
      "campaign_title": "Outside creator campaign",
      "platform": "TIKTOK",
      "social_url": "https://www.tiktok.com/@adaclips/video/7420...",
      "status": "TRACKING",
      "verified_views": 8300,
      "baseline_views": 1200,
      "measured_views": 7100,
      "last_fetched_at": "2026-08-26T14:00:00.000000Z",
      "created_at": "2026-08-25T10:03:11.000000Z"
    }
  ],
  "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" }.

POST /api/v1/brand-plugin/widget/tracked-posts

Adds a member's social post to an approved tracking-only campaign for view measurement. Send JSON.

Bearer bps_ · 20 / minute

FieldRules
external_campaign_idrequired, must belong to this integration, be approved, and be within its optional start and end dates
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, unique across Elnez-paid submissions and tracking-only posts, host must match the platform
titleoptional, max 255
posted_atoptional, not in the future. Defaults to now
acknowledgementrequired, must be truthy

Response: 201 with { "tracked_post_id": 704, "status": "TRACKING" }. The first verified view count becomes the baseline when the post predates the campaign start; otherwise the baseline is zero. The bootstrap returns both the verified total and the measured views above that baseline.

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. Bounties are created and managed by Elnez staff.

Display currency and the money model

Elnez calculates verified views and USD earnings for Elnez-paid work. The widget presents those earnings in your configured display currency.

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

Tracking-only campaigns

Use Elnez to view measured performance for tracking-only campaigns and use the outside platform for their payment records. A Brand may use Elnez-paid campaigns, tracking-only campaigns, or both.

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/tracked-posts20 / minute
POST /widget/bounty-submissions20 / minute

Exceeding a limit returns 429 with a Retry-After header. Contact Elnez support if you need a higher limit.

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
422This tracking-only campaign is not available.It does not belong to this integration, has not been approved, or has been paused or rejected
422This tracking-only campaign has not started yet.Wait until its configured start time
422This tracking-only campaign has ended.The configured tracking window is closed
422This tracking-only campaign does not accept that platform.Choose one of the platforms approved for that campaign
422Verify the matching social account before submitting.The linked account is unverified, or belongs to another platform
422Verify the matching social account before adding this post.The tracking-only post used an unverified account, another member's account, or the wrong 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, tracked-post, and bounty submissionsRefused with 403
Bio-code verificationRefused with 403
The widget bootstrapStill answers, with read_only: true
Existing members, paid submissions, tracked posts, and historyKept and readable
USD already being earnedContinues to settle to your Brand account
Queued webhooksContinue to deliver

Restoring Premium, or Elnez lifting a suspension, resumes the same integration with the same public key, keys, members, and history.

Go-live checklist