On this page and tools
Hosted sites
Site host API
How a WordPress site, or your own connector, reports to Bynefit with a signed key: a connection test, visit counts and a daily health report. It is what the Bynefit Connect plugin uses, written down so you can build the same thing.
AvailableGet a site key
An admin of the Space opens the site in the dashboard, then Settings and WordPress (the older address /dash/sites/host-keys redirects there), and chooses Generate key for that site. The key is shown once, so paste it straight into the plugin or your connector's settings. The same screen lists each key's label, first characters, last use and whether it is active, and revokes one.
- A key looks like
bynli_sh_followed by 32 hex characters. - The key is the site's identity. There is no site id or slug in the request.
- There is no rotate button. Make a new key, put it in the plugin, then revoke the old one.
- Sites that Bynefit hosts for you get a key installed for them. You never see it and do not need one.
- Only sites that are WordPress managed by Bynefit, or connected from elsewhere, can have a key.
- A site key is not a site secret. Keys & secrets is for sites Bynefit hosts as files; a WordPress site keeps its own service keys on its server, outside any page.
Sign a request
Three headers on every call:
| Header | Value |
|---|---|
Authorization | Bearer bynli_sh_... |
X-Bynefit-Timestamp | The time now in Unix seconds, digits only. It must be within 5 minutes of Bynefit's clock. |
X-Bynefit-Signature | sha256= and the lowercase hex HMAC-SHA256 of the timestamp, a newline, and the exact body, keyed with the site key itself. |
X-Bynefit-Timestamp and X-Bynefit-Signature are the names to send.
The older X-Bynli-Timestamp and X-Bynli-Signature are still accepted, so an installed WordPress plugin keeps working.
The header name is not part of what is signed. Sending a header under both names with different values is refused with a 400.
For a GET the body is empty, so the signed text is the timestamp and a newline. The query string is not signed.
The key is used both to identify the site and as the signing key, so it must never leave your server.
KEY="bynli_sh_YOUR_SITE_KEY"
TS=$(date +%s)
BODY='{"kind":"heartbeat"}'
SIG="sha256=$(printf '%s\n%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$KEY" -hex | sed 's/^.* //')"
curl -i -X POST "https://bynefit.com/api/site-host/report" \
-H "Authorization: Bearer $KEY" \
-H "X-Bynefit-Timestamp: $TS" \
-H "X-Bynefit-Signature: $SIG" \
-H "Content-Type: application/json" \
-d "$BODY"const { createHmac } = require("crypto");
function sign(key, body = "", ts = String(Math.floor(Date.now() / 1000))) {
const signature = "sha256=" + createHmac("sha256", key).update(ts + "\n" + body).digest("hex");
return { ts, signature };
}
async function call(base, key, method, path, body = "") {
const { ts, signature } = sign(key, body);
const res = await fetch(base + path, {
method,
headers: {
Authorization: "Bearer " + key,
"X-Bynefit-Timestamp": ts,
"X-Bynefit-Signature": signature,
"Content-Type": "application/json"
},
body: body === "" ? undefined : body
});
return { status: res.status, body: await res.json() };
}
module.exports = { sign, call };
call("https://bynefit.com/api", process.env.BYNEFIT_SITE_KEY, "POST", "/site-host/report", JSON.stringify({ kind: "heartbeat" }))
.then(console.log);import os
import hashlib
import hmac
import json
import time
import urllib.request
def sign(key, body=b"", ts=None):
ts = str(ts if ts is not None else int(time.time()))
digest = hmac.new(key.encode(), ts.encode() + b"\n" + body, hashlib.sha256).hexdigest()
return ts, "sha256=" + digest
def call(base, key, method, path, body=b""):
ts, signature = sign(key, body)
request = urllib.request.Request(base + path, data=body or None, method=method, headers={
"Authorization": "Bearer " + key,
"X-Bynefit-Timestamp": ts,
"X-Bynefit-Signature": signature,
"Content-Type": "application/json",
})
with urllib.request.urlopen(request) as response:
return response.status, json.load(response)
print(call("https://bynefit.com/api", os.environ["BYNEFIT_SITE_KEY"], "POST", "/site-host/report", b'{"kind":"heartbeat"}'))function bynefit_site_sign(string $key, string $body = '', ?int $ts = null): array
{
$ts = $ts ?? time();
return [(string) $ts, 'sha256=' . hash_hmac('sha256', $ts . "\n" . $body, $key)];
}
function bynefit_site_call(string $base, string $key, string $method, string $path, string $body = ''): array
{
[$ts, $signature] = bynefit_site_sign($key, $body);
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $body === '' ? null : $body,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $key,
'X-Bynefit-Timestamp: ' . $ts,
'X-Bynefit-Signature: ' . $signature,
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
return [curl_getinfo($ch, CURLINFO_RESPONSE_CODE), json_decode((string) $response, true)];
}
print_r(bynefit_site_call("https://bynefit.com/api", getenv('BYNEFIT_SITE_KEY'), 'POST', '/site-host/report', '{"kind":"heartbeat"}'));Report: connection test, visits and daily health
| Request | |
|---|---|
| Method and path | POST /api/site-host/report |
| Needs | A site key with a signature. The body is JSON of up to 32 KB. |
The body is an object with a kind. Three kinds are accepted.
heartbeat
Checks that the key and signature work. Nothing is stored.
{ "ok": true, "site_id": 118, "slug": "riverside-aid", "kind": "heartbeat", "recorded": false }
visits
Tells Bynefit that your site got visitors, so the owners can be told. Send a count, not visitors: no IP addresses and no visitor details are accepted.
{ "kind": "visits", "count": 7 }
count is clamped to 0 to 1000. The answer is
{"ok": true, "site_id": 118, "kind": "visits", "recorded": true, "alerted": false}.
Bynefit groups visits into quiet and roll-up windows and notifies each admin of the Space the way they chose (instant, digest or off), when the site has visit alerts on.
daily
One health report a day. Sending it again for the same date replaces that day.
{
"kind": "daily",
"usage_date": "2026-10-10",
"storage_bytes": 52428800,
"bandwidth_bytes": 0,
"request_count": 0,
"meta": { "wp_version": "6.6", "php_version": "8.2", "plugin_version": "0.23.2" },
"insights": {
"updates": { "core": 0, "plugins": 2, "themes": 0 },
"posts": 48,
"pages": 12,
"comments_pending": 3,
"users_total": 5,
"admins": 1,
"https": true,
"debug": false
}
}
usage_dateis required, asYYYY-MM-DD. The three numbers are optional whole numbers, never below zero.metaandinsightsare free-form objects. Bynefit stores whatever you send, within the size cap, and shows the keys it knows: versions and update counts, content counts, comments waiting, users and admins, the active theme, and the https, debug, search and multisite flags.- Send counts, versions and flags only. Never send people's names, emails or content.
- The answer is
{"ok": true, "site_id": 118, "slug": "riverside-aid", "kind": "daily", "recorded": true}.
The first accepted report also attaches an unowned connected site to the person who made the key, which is what makes it show up in the Bynefit Sites app.
Errors
| Status | Body | Meaning |
|---|---|---|
401 | {"error":"unauthorized"} | No bearer key, a malformed key, an unknown one, or a revoked one. These look the same on purpose. |
400 | {"error":"bad_timestamp"} | The timestamp is not digits only. |
400 | {"error":"bad_signature_format"} | The signature does not start with sha256=. |
400 | {"error":"conflicting_headers"} | A signing header was sent under both its X-Bynefit- and X-Bynli- names with different values. |
401 | {"error":"signature_invalid"} | The signature does not match, or the time is more than 5 minutes off. |
400 | {"error":"body_too_large"}, invalid_json, bad_kind, bad_usage_date | The body is over 32 KB, is not JSON, has an unknown kind, or the daily date is not YYYY-MM-DD. |
405 | {"error":"method_not_allowed"} | Wrong method. |
429 | {"error":"rate_limited"} | Too many calls on this key. Wait the seconds in the Retry-After header. The body has no wait time. |
500 | {"error":"server_error"} | Retry later. |
If the signature check fails, check three things first: you signed the exact bytes you sent, you used a newline and not a dot between the time and the body, and your server clock is right.
Read the forms list
| Request | |
|---|---|
| Method and path | GET /api/site-host/forms |
| Needs | A site key with a signature and an empty body |
{ "ok": true, "team_id": 42, "forms": [ { "id": "frm_AbC123xYz", "title": "Contact us", "slug": "contact-us" } ] }
All of the Space's active forms, not only the ones for this site. At most 200.
Support tickets
A connected site can show and send Bynefit support tickets for its Space. Tickets belong to the Space, so every site key in the Space can read every ticket of that Space.
A person named in wp_user_email or wp_user_name is whatever your site says; Bynefit does not verify them.
A captured request can be replayed for up to five minutes, so keep these calls on your server over HTTPS.
| Call | What it does |
|---|---|
GET /api/site-host/tickets?status=open | Up to 50 tickets: open (default), in_progress, resolved or all. Returns each ticket's ref, subject, category, priority, status, reply count and a short preview, plus counts per status. |
GET /api/site-host/tickets/{ref} | One ticket with its replies. 404 not_found for a ref that is not in your Space. |
POST /api/site-host/tickets | Opens a ticket. subject 3 to 200 characters, body up to 5000, category one of technical, billing, general, account. Answers 200 with {"ok":true,"ticket_id":...,"ticket_ref":"...","priority":"...","created_at":"..."}. Errors: 400 subject_too_short, subject_too_long, empty_subject, empty_body, invalid_category or invalid_json; 413 body_too_large. |
POST /api/site-host/tickets/{ref}/reply | Adds a reply (body up to 5000 characters) and re-opens the ticket. |
POST /api/site-host/tickets/{ref}/resolve | Marks it resolved. An optional note is posted as a reply first. |
Plugin version and download
These two need no key.
| Request | |
|---|---|
| Method and path | GET /api/site-host/version |
| Needs | Nothing |
Returns the current Bynefit Connect release: name, slug, version, download_url, download_sha256,
requires, tested, requires_php, last_updated, homepage, description and changelog (HTML).
?changelog=full returns the whole history instead of the five newest versions.
Check the zip against download_sha256 before installing it. Cached for 10 minutes.
| Request | |
|---|---|
| Method and path | GET /api/site-host/download |
| Needs | Nothing |
Redirects (302) to the current zip. Both are limited per address; a 429 asks you to wait.
What is not part of this API
- Other calls the plugin makes are private and not documented.
- Reading anything back about visitors, members or payments. These endpoints take reports and tickets; they do not return your site's analytics.
- Planned A way to rotate a site key in one step.