On this page and tools
Hosted sites
Hosted sites API
The public endpoints a hosted site's pages call: site status, page views, forms, events, resources and media. They need no key. The site's slug in the address is how Bynefit knows which site you mean.
AvailableConventions
| Topic | How it works |
|---|---|
| Base | https://bynefit.com/api/sites |
| Authentication | None. The slug (3 to 100 lowercase letters, numbers and hyphens) names the site. |
| Browsers | Every endpoint answers any origin (Access-Control-Allow-Origin: *) and never uses cookies, except secure requests, which answer only the site's own addresses. Send requests without credentials. |
| Errors | Most answer 200 with {"ok": false, "error": "..."}, so check ok, not just the status. The widget and events endpoints use real 404 and 500 codes. |
| Metering | Calls are counted per site per day for usage. They are not throttled, except where noted below. |
Which sites answer
Every endpoint decides the same way whether a site is open to the public. A live site is published, not suspended,
not archived or deleted or waiting for deletion, and belongs to a Space that is active and not being deleted.
Where an endpoint needs a live site, an unknown slug and a site that is not live get the identical answer (form submit also answers a malformed slug with Invalid site),
so nobody can learn which sites exist by asking.
| Endpoint | Needs | Otherwise |
|---|---|---|
| Site status | A site that is not archived or deleted. Suspended, unpublished and maintenance sites are reported as they are. | 200 Site not found |
| Page views | A site that is not archived or deleted | Still a GIF, nothing recorded |
| Form definition | The form's site live, or the Space active for a Space-wide form | 200 Form not found |
| Form submit | A live site, with the form active and belonging to it | 200 Site not available for a site that is not live; Form not found for an unknown or inactive form |
| Events | A live site, and the Space's public events setting on | 404 site_not_found |
| Ready-made blocks | A live site. The events block also needs public events on. | 404 Not found |
| Media library | A live site | 200 Site not available |
Only a Space that is switched off or being deleted stops serving its sites. Turning off public events hides the events endpoint and the events block, and nothing else.
Site status
| Request | |
|---|---|
| Method and path | GET /api/sites/runtime-status?slug=my-site |
| Needs | Nothing. Send slug, or host for a custom domain. |
curl "https://bynefit.com/api/sites/runtime-status?slug=my-site"const res = await fetch("https://bynefit.com/api/sites/runtime-status?slug=my-site");
const { ok, status } = await res.json();
if (ok && status.is_maintenance) console.log(status.maintenance_message);{
"ok": true,
"slug": "my-site",
"kind": "hosted",
"status": {
"is_suspended": false,
"is_maintenance": false,
"maintenance_message": null,
"published": true,
"skip_branding": false,
"badge_href": null
}
}
hostworks instead ofslugfor a verified custom domain. If both are sent,slugwins.- A site that is archived or deleted, or a slug that does not exist, answers
200with{"ok": false, "error": "Site not found"}. A suspended, unpublished or in-maintenance site is still reported, so the runtime can show its page. badge_hrefis the link the "Powered by Bynefit" badge should use. It isnullfor most sites. When the Space that owns the site is a Bynefit partner whose link is switched on, it is that partner's link onbynefit.com, and the badge should use it in place of the general link.- Cached for 30 seconds.
Page views
| Request | |
|---|---|
| Method and path | GET /api/sites/track?slug=my-site&path=/about |
| Needs | Nothing |
Always answers a 1 by 1 transparent GIF, whatever you send, so it can be fired as an image: new Image().src = ....
It stores the page path (without query string or fragment), the referring address, the browser's user agent and a hash of the visitor's IP address. No cookies and no raw IP address.
Views are recorded for any site that has not been archived or deleted, including one that is unpublished or suspended; an archived or deleted site records nothing. Visitors or sites that send too many views in a minute have the extra ones dropped silently.
The totals are shown to the Space owner in the dashboard; there is no endpoint to read them.
Form definition
| Request | |
|---|---|
| Method and path | GET /api/sites/forms/{form_id}/schema |
| Needs | Nothing. {form_id} looks like frm_AbC123xYz |
{
"ok": true,
"form": {
"id": "frm_AbC123xYz",
"name": "Contact us",
"team_name": "Riverside Aid",
"confirmation_message": null,
"fields": [
{ "id": 12, "label": "Email", "type": "email", "required": true, "placeholder": null, "help": null, "options": null },
{ "id": 13, "label": "Topic", "type": "dropdown", "required": false, "placeholder": null, "help": null, "options": ["Housing", "Food", "Other"] }
]
}
}
Field types: text, textarea, email, tel, number, date, dropdown, checkbox, radio, section_header, hidden.
Only active forms answer, and only while the form is open: a form tied to a site needs that site live, and a Space form needs an active Space. Anything else answers {"ok": false, "error": "Form not found"}, the same as an unknown form. A malformed id answers Invalid form id. Cached for 60 seconds.
Submit a form
| Request | |
|---|---|
| Method and path | POST /api/sites/{slug}/forms/{form_id}/submit |
| Needs | Nothing. The form must be active and belong to the site, and the site must be live: published, not suspended, not archived or deleted, and its Space active. |
Send a form-encoded body (application/x-www-form-urlencoded or multipart), not JSON.
A value is matched to a field by field_{id}, or by the field's label lowercased with anything that is not a letter or number turned into _
(so Full name is full_name). Checkbox groups can repeat the name (topic[]); the values are joined with a comma.
Names that match no field are ignored.
curl -X POST https://bynefit.com/api/sites/my-site/forms/frm_AbC123xYz/submit \
-d "full_name=Sam Rivera" \
-d "email=sam@example.com" \
-d "message=Do you have evening hours?"const res = await fetch("https://bynefit.com/api/sites/my-site/forms/frm_AbC123xYz/submit", {
method: "POST",
headers: { Accept: "application/json" },
body: new URLSearchParams({
full_name: "Sam Rivera",
email: "sam@example.com",
message: "Do you have evening hours?"
})
});
const result = await res.json();
if (!result.ok) console.error(result.error, result.missing);$ch = curl_init("https://bynefit.com/api/sites/my-site/forms/frm_AbC123xYz/submit");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'full_name' => 'Sam Rivera',
'email' => 'sam@example.com',
'message' => 'Do you have evening hours?',
]),
]);
$result = json_decode(curl_exec($ch), true);{ "ok": true, "message": "Thanks! We received your submission." }
The message is the form's own confirmation message when it has one. Nothing else comes back: no submission id and no echo of what was sent.
Forms that ask for an email code
The schema answer carries auth_mode. When it is email_otp, a submit without a confirmed email answers 403 with "code": "email_code_required".
The Bynefit form module handles this for you: it shows a short first step, sends the code, and unlocks the form with the confirmed address already in the first email field.
A form that asks people to sign in (bynli_login) answers 403 with "code": "auth_required" and the address of its Bynefit form page instead.
| Request | |
|---|---|
| Method and path | POST /api/sites/{slug}/forms/{form_id}/otp |
| Needs | Nothing. The form must be an email-code form on a live site. |
Form-encoded body with action=send and email: a six digit code is emailed and {"ok": true, "challenge": "..."} comes back. Codes are limited to one a minute per address and form, 5 an hour per address and 200 an hour per form, and each caller address is throttled; over a limit answers 429.
Then action=verify, challenge and code: {"ok": true, "ticket": "...", "email": "..."}. Five wrong tries lock a code.
Send the ticket as _bynli_otp with the submit. It is a bearer token: good for an hour, for that form only, for any number of submissions. The submission is stored under the confirmed address, and the form's first email field is replaced by it. An unknown or non-email-code form answers 404 or 400, and an expired ticket answers 403 with email_code_required so the form asks again.
| Status | Body | Meaning |
|---|---|---|
200 | {"ok":false,"error":"Please fill in: Email","missing":["Email"]} | A required field is empty. missing lists the labels. |
200 | {"ok":false,"error":"Invalid site"} | The slug in the address is malformed. |
200 | {"ok":false,"error":"Site not available"} | The site is unknown, unpublished, suspended, archived or deleted, or its Space is inactive or being deleted. All of these answer the same. |
200 | {"ok":false,"error":"Form not found"} | The form is unknown or not active. A malformed id answers Invalid form. A form that belongs to another Space or is not enabled for this site answers Form does not belong to this site or Form not enabled on this site. |
403 | {"ok":false,"code":"email_code_required","error":"Confirm your email with the code we send you before submitting this form."} | The form is an email-code form and the submit carried no valid _bynli_otp ticket. |
403 | {"ok":false,"code":"auth_required","form_url":"..."} | The form needs people to sign in, which only works from its Bynefit form page. |
429 | {"ok":false,"error":"Too many submissions. Please try again in a minute."} | More than 5 submissions to the same form from one address in a minute. |
500 | {"ok":false,"error":"Server error. Please try again."} | Something failed on our side. Retry. |
What a submission sets off
- The answers are saved with the form. A lead form also creates a contact.
- The
form.submittedwebhook fires. It carries the form's id and name, never the answers. See Webhooks. - Unless it is a registration form, your Space's leaders get a follow-up task and a notification. This is capped at 20 a hour per Space; past the cap the answers are still saved.
- If the person gave an email address (
email,email_addressor_respondent_email), they get your autoresponder if the form has one, or a short receipt. A malformed address is ignored and the submission still goes through. - A hidden field named
_bynli_hpis a bot trap. If it is filled in, the answer is{"ok": true}and nothing is saved.
The server only checks that required fields are filled. It does not check that an email looks like an email, that a number is a number, or that a dropdown value is one of the options, so validate in the page as well.
Events
| Request | |
|---|---|
| Method and path | GET /api/sites/{slug}/events?limit=5&scope=upcoming |
| Needs | Nothing |
limit is 1 to 50 (default 5). scope is upcoming (default) or past. Only a live site answers, and only when the Space has turned on public events. Otherwise the answer is the same 404 {"ok": false, "error": "site_not_found"} as for an unknown or malformed slug, so the answer never reveals which sites exist. Cancelled and draft events are left out. upcoming covers today up to two years ahead and past reaches back two years, newest first.
curl "https://bynefit.com/api/sites/my-site/events?limit=3&scope=upcoming"const res = await fetch("https://bynefit.com/api/sites/my-site/events?limit=3&scope=upcoming");
const { events, count } = await res.json();
for (const e of events) console.log(e.event_date_iso, e.title);$data = json_decode(file_get_contents("https://bynefit.com/api/sites/my-site/events?limit=3&scope=upcoming"), true);
foreach ($data['events'] as $e) {
echo $e['event_date_iso'], ' ', $e['title'], "\n";
}{
"ok": true,
"team": { "slug": "riverside-aid", "name": "Riverside Aid" },
"scope": "upcoming",
"count": 1,
"events": [
{
"id": 481,
"title": "Free tax help night",
"description": "Bring your documents.",
"event_date": "2026-11-04 18:00:00",
"event_date_iso": "2026-11-04T18:00:00+00:00",
"location": "Community hall",
"tickets_enabled": true,
"ticket_price": 0.0,
"ticket_name": "General",
"tickets_sold": 12,
"tickets_limit": 40
}
]
}
Errors use real codes: 404 site_not_found for an unknown, malformed or gated site (one answer for all, so it never reveals which sites exist), and 500 server_error. At most 50 events come back.
Cached for 60 seconds. Sold counts and limits come with every event listed. There are no attendee lists or buyers in the answer. Ticket and detail links on a hosted site go to the event's page on Bynefit.
Ready-made blocks
| Request | |
|---|---|
| Method and path | GET /api/sites/widget/{slug}/{kind} |
| Needs | Nothing. {kind} is events or resources. |
Returns finished HTML in a JSON field, for dropping into a page. Any other kind, or any site that is not live, answers 404 with {"ok": false, "error": "Not found"}, the same body for each, and so does the events block when the Space has not turned on public events. Cached for 60 seconds.
| Kind | Options | Answer |
|---|---|---|
events | scope (upcoming or past), limit 1 to 50, default 5 | {"ok":true,"enabled":true,"html":"...","count":N}. Needs a live site and public events turned on, or it answers 404. Cancelled and draft events are left out, within the same two-year window as the events endpoint. count is the number of upcoming events, not the number shown. |
resources | category (letters, numbers, spaces, _ and -, up to 40), limit 1 to 50, default 12 | {"ok":true,"html":"...","count":N}. The Space's most-upvoted approved resources, filtered by category. |
The HTML is escaped on our side. Set it with innerHTML only for these two endpoints, never for text from anywhere else.
Media library
| Request | |
|---|---|
| Method and path | GET /api/sites/{slug}/media |
| Needs | Nothing. The site must be live. |
{
"ok": true,
"site": "my-site",
"count": 1,
"media": [
{
"id": 7,
"filename": "team.jpg",
"url": "https://my-site.site.bynefit.com/media/team.jpg",
"mime_type": "image/jpeg",
"size_bytes": 184233,
"uploaded_at": "2026-10-08 09:12:44"
}
]
}
Newest first, with no paging, so a big library is a big answer. Not cached. An unknown, malformed or not-live site answers {"ok": false, "error": "Site not available"}.
Secure requests
| Request | |
|---|---|
| Method and path | GET /api/sites/my-site/proxy/weather |
| Needs | Nothing from the page. The site owner names the request, its service and its methods in the dashboard, and the call must come from a page on the site. |
The one endpoint here that is not open to every origin. It forwards a page's call to the service the site owner chose and adds the owner's secret on the server, so a page can use a paid or private API without holding its key. It accepts only the methods the owner allowed, answers only the site's own addresses, and has per-visitor, per-minute and per-day limits. The full contract, limits and error codes are in Keeping keys safe on a hosted site.
What is not here
- No way to read form answers, visitor totals, members, payments, donations or ticket buyers from a page.
- Donations and tickets are links to Bynefit's own pages, not API calls.
- Managing a site (publishing, uploading media, editing the document) is done in the Bynefit Sites app and the dashboard, not through a public API.
- Planned A key-based API for a site's shop, products and orders.