Skip to content
On this page and tools

Partner program

Partner events

Send the events your own product records to Bynefit, in batches, and read the totals back. This is how a partner sees how people use its listings, pages or app, with totals by event and by category in the partner console.

Partners only. Partner events come with the partner program, which Bynefit sets up for one Space at a time. By approval If you are not a partner yet, ask through support. The partner guide walks the whole path from enrolment to your first event.

What you need first

A manager of your Space sets these up on the Keys page and the Manage events tab of the Events page of the partner console at /dash/partner/keys. Each change works straight away.

You needWhat it is
A partner keyStarts with pbk_. It says who you are and is safe to put in a web page. It is not a secret.
Registered eventsThe event names you may send, and the details each may carry. Anything else is refused.
A registered websiteOnly for sending from a browser. The address a page is served from, like https://example.com.
A signing secretOnly for sending from a server or app. It is shown once when it is made. Lost it? Make a new one on the Keys page.
Program statusdraft while you set up (test events work), live once agreed, paused to stop everything. A paused program accepts nothing.

Three ways to send

WayUse it fromHow it proves it is youEndpoint
BrowserYour own web pagesThe partner key plus the page's address, which must be registered.POST /dash/api/v1/beacons/collect
SignedYour server or app backendAn HMAC signature made with your signing secret.POST /dash/api/v1/beacons/collect
TokenYour own servers, if you prefer an API keyA Space API key that has been given the permission to send product events.POST /dash/api/v1/beacons
Never put the signing secret or a Space API key in a web page or an app. Anything shipped to a browser or a phone can be read by the person using it. Pages use the browser way; secrets stay on your server.

The request body

Every way takes the same JSON body: an object with an events list of 1 to 100 events, up to 256 KB in all.

FieldRequiredWhat to put
idYesA unique id you make up for this event, 6 to 64 characters. Send the same id again and it is ignored, so retries never double count.
nameYesOne of your registered event names, or a standard event type name. Not needed when you send event_id or event_type.
event_typeNoA standard event type, by name like listing_viewed or by its number. Use it instead of name, or with your own name to say which type it is.
event_idNoThe number of one of your registered events, shown under Manage events. Use it instead of name.
atYesWhen it happened, like 2026-10-10T14:03:22Z. No more than 5 minutes ahead or 14 days behind.
user_refNoYour own id for the person, like u_8f3a. Never an email address or phone number.
session_refNoYour own id for the visit, like s_41c2.
propsNoExtra details, only the properties listed for that event, with the type shown.
BodyJSON
{
  "events": [
    {
      "id": "evt-20261010-0001",
      "name": "listing_viewed",
      "at": "2026-10-10T14:03:22Z",
      "user_ref": "u_8f3a",
      "session_ref": "s_41c2",
      "props": { "category": "food", "count": 3 }
    }
  ]
}

Rules every event is checked against

Event types: standard names that add themselves

Bynefit keeps one list of standard event types for every partner, like listing_viewed, search_performed, signup_completed or order_completed. Each has a fixed number, a category (views, search, purchases and so on) and other spellings it also accepts, so page_view, pageView and Page View all count as page_viewed. The full list, with numbers and the details each type can carry, is under Manage events in the partner console at /dash/partner/events.

You send a name that is not registered yetWhat happens
Signed, or with an API keyIt is added to your events and accepted. A standard name is grouped under its type and gets that type's details; any other name is added as uncategorized with no details, until you or Bynefit give it a type.
From a web pageIt is refused with pending_approval and waits under Manage events. A manager or owner of your Space approves it there. Web pages never add events on their own, because anyone can copy a partner key.
Past a limitRefused with event_limit or auto_register_limit, and counted.

Once an event is registered you can also send it by number. event_id is the number of your registered event, shown under Manage events. event_type is a standard type, by name or number: on its own it picks your event of that type, and next to your own name it says which type that name is. name keeps working exactly as before. Bynefit stores the number either way, so totals stay right if a label changes.

Three ways to name the same eventJSON
{ "id": "evt-1", "name": "listing_viewed", "at": "2026-10-10T14:03:22Z" }
{ "id": "evt-2", "event_type": "listing_viewed", "at": "2026-10-10T14:03:22Z" }
{ "id": "evt-3", "name": "viewed_shop", "event_type": "listing_viewed", "at": "2026-10-10T14:03:22Z" }

Send from a web page

Your page's address must be registered. The browser sends it for you, and Bynefit only answers pages on your list. Add a new address in the console under Your websites. It works straight away.

const response = await fetch("https://bynefit.com/dash/api/v1/beacons/collect", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Partner-Key": "pbk_YOUR_PARTNER_KEY"
  },
  body: JSON.stringify({
    events: [{
      id: crypto.randomUUID(),
      name: "listing_viewed",
      at: new Date().toISOString(),
      props: { "category": "sample", "count": 1 }
    }]
  })
});

console.log(response.status, await response.json());

A browser that is closing the page can use navigator.sendBeacon. It cannot set headers, so the partner key goes in the address as ?key=, and the body is sent as plain text so the browser does not need a preflight. The page address is checked the same way.

JavaScriptsendBeacon
const body = JSON.stringify({
  events: [{
    id: crypto.randomUUID(),
    name: "listing_viewed",
    at: new Date().toISOString(),
    props: { "category": "sample", "count": 1 }
  }]
});

const queued = navigator.sendBeacon(
  "https://bynefit.com/dash/api/v1/beacons/collect?key=pbk_YOUR_PARTNER_KEY",
  new Blob([body], { type: "text/plain" })
);

console.log(queued ? "Queued" : "The browser refused it");

Some browsers print a cross-origin error in the console for a beacon that was in fact delivered. Check your totals, not the console.

Send from a server or app

Sign every request. The signature is the HMAC-SHA256 of the Unix time in seconds, a literal dot, and the exact body you send, keyed with your signing secret, written as lowercase hex.

  1. Take the time nowSeconds since 1970. It must be within 5 minutes of Bynefit's clock, so keep your server's clock synced.
  2. Write the bodySend exactly the characters you sign. Do not re-serialize between signing and sending.
  3. Join themThe time, a dot, then the body: 1760105000.{"events":[...]}.
  4. Sign itHMAC-SHA256 with the signing secret, lowercase hex.
  5. Send three headersX-Partner-Key, X-Beacon-Timestamp and X-Beacon-Signature, with Content-Type: application/json.
URL="https://bynefit.com/dash/api/v1/beacons/collect"
KEY="pbk_YOUR_PARTNER_KEY"
SECRET="YOUR_SIGNING_SECRET"

TS=$(date +%s)
ID="evt-$TS-$RANDOM"
AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)

BODY=$(cat <<EOF
{"events":[{"id":"$ID","name":"listing_viewed","at":"$AT","props":{"category":"sample","count":1}}]}
EOF
)

SIGNATURE=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -i -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-Partner-Key: $KEY" \
  -H "X-Beacon-Timestamp: $TS" \
  -H "X-Beacon-Signature: $SIGNATURE" \
  -d "$BODY"

Check your code against a known answer. With the secret YOUR_SIGNING_SECRET, the time 1760105000 and this body, the signature below is what Bynefit expects.

Worked exampletext
body       {"events":[{"id":"7b1c0e5e-3f2a-4c1d-9a7e-1d2f3a4b5c6d","name":"listing_viewed","at":"2026-10-10T14:03:22Z","props":{"category":"sample","count":1}}]}
to sign    1760105000.{"events":[{"id":"7b1c0e5e-3f2a-4c1d-9a7e-1d2f3a4b5c6d","name":"listing_viewed","at":"2026-10-10T14:03:22Z","props":{"category":"sample","count":1}}]}
signature  078742092221cdbb978d50900d652de4b4b6487ffc2572983526657aba9f0444

Send with an API key

A Space API key that holds the permission to send product events can post to the token endpoint instead. The key is made on /dash/integrations/api-tokens. This permission is sensitive: wildcards never grant it and only a manager or admin of the partner Space can hold it. On a partner Space it works as soon as the key is made, and Bynefit is told; other Spaces cannot use it. See Keys and review.

By approvalPOST
Request
Method and pathPOST /dash/api/v1/beacons
NeedsA Space API key from a partner Space, sent as a bearer token
Permissionwrite:beacons
curl -X POST https://bynefit.com/dash/api/v1/beacons \
  -H "Authorization: Bearer bapi_tk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"id":"evt-20261010-0001","name":"listing_viewed","at":"2026-10-10T14:03:22Z","user_ref":"u_8f3a","props":{"category":"food","count":3}}]}'

Token calls are not billed to your API credits and are not counted against the browser limits. They go through the same rate limit as every other API key.

What comes back

A successful request is always 202. An event that is refused is listed with its reason, and the status stays 202, so read the body.

StatusMeaningBodyWhat to do
202Received{"accepted":1,"duplicates":0,"rejected":[]}Done. Look for the event in your totals.
202Received, but some events were refused{"accepted":0,"duplicates":0,"rejected":[{"index":0,"id":"evt-1","reason":"unknown_event"}]}Read the reason on each refused event. An event name that is not registered is refused here, with a 202, not an error status.
401Key or signature not accepted{"error":"missing_or_invalid_key"} or {"error":"bad_signature"}Check the partner key, the signing secret, the exact string you signed and that your clock is within 5 minutes.
403Not allowed{"error":"origin_not_allowed"} or {"error":"partner_paused"}For a browser, your website address must be registered. A paused program accepts nothing.
413Too large{"error":"body_too_large"} or {"error":"batch_too_large"}Send fewer events per request, and keep a request under 256 KB.
422Body not understood{"error":"events_required"}Send {"events": [ ... ]} with at least one event.
429Slow down{"error":"rate_limited"} and a Retry-After headerWait the number of seconds in Retry-After, then send the same events again. Reused ids are ignored, so nothing counts twice.
202JSON
{
  "accepted": 1,
  "duplicates": 0,
  "rejected": [
    { "index": 1, "id": "evt-20261010-0002", "reason": "bad_property" }
  ]
}

Reasons an event is refused

ReasonMeaning
bad_eventEvent is not an object
bad_idMissing or malformed id
unknown_eventEvent is not registered for this partner
pending_approvalEvent name from a web page is not registered yet and is waiting for approval in the partner console
event_limitThis partner has reached its limit of registered events
auto_register_limitToo many new event names were added automatically; add the ones you want under Manage events in the console
temporarily_unavailableBusy adding this event name; send the same event again
bad_timeTimestamp missing, malformed, too old or in the future
bad_refuser_ref or session_ref is malformed
bad_propertyProperty is not registered for this event or has the wrong type
pii_suspectedValue looks like personal data
too_largeEvent is too large

Errors specific to the token endpoint

StatusBodyMeaning
401{"error":"missing_or_invalid_token"}No key, a wrong key, or a revoked one. token_revoked and token_expired are returned for those two cases.
403{"error":"insufficient_scope","required":"write:beacons"}The key does not hold the permission to send product events.
403{"error":"not_a_partner"}The key's Space is not enrolled as a partner.
405{"error":"method_not_allowed"}Use POST.

Test without saving

Add "test": true next to "events". The events are checked exactly as for a real request and the answer is the same 202 with "test": true, but nothing is stored or counted. Rate limits still apply.

BodyJSON
{ "test": true, "events": [ { "id": "test-0001", "name": "listing_viewed", "at": "2026-10-10T14:03:22Z" } ] }

The console's Send a test event button does the same with one made-up event. It does not exercise the signature or the website check, so send one signed or browser test as well.

Limits

LimitValueApplies to
Events in one request100Every way of sending
Size of one request256 KBEvery way of sending
Events per minute1,200Everything you send, added together
Events per hour50,000Everything you send, added together
Events per minute from one visitor120Web pages only
Requests per minute from one visitor60Web pages and anything unsigned
Clock difference on a signature5 minutesSigned requests
Age of an event5 minutes ahead, 14 days behindThe at field

If you are told to slow down, wait the number of seconds in Retry-After and send the same events again. Reused ids are ignored, so nothing counts twice.

Read your totals

By approvalGET
Request
Method and pathGET /dash/api/v1/beacons/summary?days=30
NeedsA Space API key from a partner Space that holds the permission to read event totals
Permissionread:beacons

days is 1 up to your retention period (never less than 90) and defaults to 30.

curl "https://bynefit.com/dash/api/v1/beacons/summary?days=7" \
  -H "Authorization: Bearer bapi_tk_YOUR_KEY"
200JSON
{
  "partner": { "code": "example", "status": "live" },
  "summary": {
    "days": 7,
    "accepted": 412,
    "users": 96,
    "events": [
      { "name": "listing_viewed", "count": 390, "last_seen": "2026-10-10 14:03:22",
        "event_id": 12, "type": "listing_viewed", "type_label": "Listing viewed",
        "category": "navigation", "category_label": "Views" }
    ],
    "today": { "accepted": 58, "rejected": 1, "top_reject": "bad_property" },
    "rejects": { "bad_property": 3 }
  },
  "funnel": { "spaces": 4, "bought": 1 },
  "credits": { "accrued_cents": 0, "paid_cents": 0, "orders": 0 }
}

Partner code on new Spaces

When your app creates a Space for someone, send partnerCode (and optionally partnerSource) in the create-Space request. The first partner to touch a Space keeps it. A paused or unknown code, your own Space, or a creator who is already a member of your Space is ignored. Bynefit staff switch this on for your program; until they do, no Space is attributed to you.

Your partner link, https://bynefit.com/via/your-code, does the same for people who sign up on the web: add ?s=poster to tell links apart. It remembers your code for 180 days. Anyone who manages the Space can see who referred it, the person who created it is emailed once, and an admin can detach it at any time, after which you stop seeing it.

Planned A QR code for your link, and a way to register a business from your own app, are planned.

Privacy and retention

Security notes