# Bynefit API integration brief

Paste this into an AI agent that is building an integration. It is generated from the code the API runs on.

## Where things are

- Base URL: `https://bynefit.com/dash/api/v1`
- OpenAPI 3.1 spec: `https://bynefit.com/docs?format=openapi`
- Docs index for agents: `https://bynefit.com/docs?format=llms`
- Any docs page as Markdown: add `?format=md`, or send `Accept: text/markdown`
- Docs search over MCP: `https://bynefit.com/docs?format=mcp`

## Rules

1. Read the key from the environment variable `BYNEFIT_API_KEY`. Never print it, log it, commit it or put it in a URL.
2. Start with a test key: a `bapi_tk_test_` or `bapi_pk_test_` key answers like the real API with sample data (`X-Bynefit-Mode: test`, `"livemode": false`). An agent key's own test mode is different: it reads the owner's real data, read-only, on a smaller daily ceiling. Calls cost units: read `X-Bynefit-Units-Remaining`, and on `402 usage_exhausted` stop and tell your owner (the answer carries a `top_up_url`).
3. With a live key, make read calls only. Ask the person before any call that creates, changes or deletes data.
4. Send `Authorization: Bearer <key>` on every call. Partner calls are different: see the partner rows below.
5. Follow `next_cursor` until `has_more` is false. Never build a cursor yourself.
6. On `429`, wait for `Retry-After`. On `5xx`, retry with the same `Idempotency-Key` where the call accepts one.
7. Quote the `request_id` from any error when you report a problem.
8. If a response does not match the spec, stop and show the person the request and the response.

## Calls

| Call | What it does | Needs | Idempotency-Key |
| --- | --- | --- | --- |
| `GET /tasks` | List tasks, newest first | key with read:tasks | no |
| `GET /tasks/{id}` | Get one task | key with read:tasks | no |
| `GET /users` | List members of the Space | key with read:users | no |
| `GET /users/{id}` | Get one member | key with read:users | no |
| `GET /users/{id}/membership` | Get a member's membership in the Space | key with read:users | no |
| `GET /events` | List events from the event log, newest first | key with read:events | no |
| `GET /events/{id}` | Get one event with its data | key with read:events | no |
| `GET /forms` | List forms | key with read:forms | no |
| `GET /forms/{id}` | Get one form | key with read:forms | no |
| `GET /forms/{id}/submissions` | List submissions of a form | key with read:forms | no |
| `GET /forms/{id}/submissions/{submission_id}` | Get one submission | key with read:forms | no |
| `GET /payments` | List payments, newest first | key with read:payments | no |
| `GET /payments/{id}` | Get one payment | key with read:payments | no |
| `GET /teams/current` | Get the Space the key belongs to | any key | no |
| `GET /resources` | List the public community resources directory | key with read:resources:public | no |
| `GET /agent` | Describe the agent key making the call | any key | no |
| `POST /agent/access-requests` | Ask the key owner for one more permission | any key | yes |
| `GET /sites` | List the hosted sites in the Space | key with read:site-files | no |
| `GET /sites/{site_id}/files` | List the files of a site | key with read:site-files | no |
| `GET /sites/{site_id}/files/{path}` | Read one site file and its sha256 | key with read:site-files | no |
| `GET /sites/{site_id}/versions/{path}` | List the saved versions of a site file | key with read:site-files | no |
| `PUT /sites/{site_id}/files/{path}` | Create or replace a site file | key with write:site-files | yes |
| `DELETE /sites/{site_id}/files/{path}` | Move a site file to the trash | key with write:site-files | yes |
| `POST /forms` | Create a form | key with write:forms | yes |
| `PATCH /forms/{id}` | Change a form's settings | key with write:forms | no |
| `DELETE /forms/{id}` | Delete a form that has no submissions | key with write:forms | no |
| `POST /forms/{id}/fields` | Add a field to a form | key with write:forms | no |
| `PATCH /forms/{id}/fields/{field_id}` | Change a field of a form | key with write:forms | no |
| `DELETE /forms/{id}/fields/{field_id}` | Delete a field of a form | key with write:forms | no |
| `POST /forms/{id}/fields/{field_id}/move` | Move a field up or down | key with write:forms | no |
| `GET /engagement-types` | List the Space's engagement types | key with read:engagement-types | no |
| `GET /engagement-types/{type_key}` | Get one engagement type | key with read:engagement-types | no |
| `POST /engagement-types` | Create an engagement type | key with write:engagement-types | yes |
| `PATCH /engagement-types/{type_key}` | Change an engagement type | key with write:engagement-types | no |
| `DELETE /engagement-types/{type_key}` | Delete an engagement type no engagement uses | key with write:engagement-types | no |
| `POST /beacons` | Send a batch of events from your server | key with write:beacons | no |
| `GET /beacons/summary` | Read the event summary, funnel and credits | key with read:beacons | no |
| `POST /beacons/collect` | Send events from a browser or a signed server call | partner key (browser) or signature | no |
| `POST /partner/businesses` | Invite a business owner to set up a Space | partner key and signature | yes |
| `GET /partner/businesses/{business_ref}/promotions` | List the promotions you sent for a business | partner key and signature | no |
| `POST /partner/businesses/{business_ref}/promotions` | Send a promotion for a claimed business, held for the owner to approve | partner key and signature | yes |

## Errors

Every error has `error` (a stable code), `message`, `request_id` and `docs_url`. The same id is in `X-Request-Id`.

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | `missing_or_invalid_token` | The key is missing, malformed or unknown. |
| 401 | `token_revoked` | The key was revoked. Make a new one. |
| 401 | `token_expired` | The key passed its expiry date. Make a new one. |
| 401 | `missing_or_invalid_key` | The partner key is missing or not recognised. |
| 401 | `key_in_url` | A write endpoint does not take a key in the address. Send it as an Authorization Bearer header and revoke the key that was exposed. |
| 401 | `bad_signature` | The request signature did not match. Sign the exact body you send. |
| 403 | `insufficient_scope` | The key is valid but not scoped for this endpoint. The response names the missing scope in required. |
| 403 | `domain_not_authorized` | The request came from a web address this key is not allowed on. |
| 403 | `origin_not_allowed` | The request origin is not allowed for this key. |
| 403 | `not_a_partner` | The Space is not enrolled as a partner. |
| 403 | `partner_paused` | The partner is paused, so this call is refused until it is resumed. |
| 404 | `not_found` | There is nothing with that id, or the key cannot see it. |
| 404 | `not_enabled` | This feature is not switched on yet. |
| 404 | `unknown_resource` | There is no such resource in v1, or this key type cannot reach it. |
| 404 | `unknown_subresource` | There is no such sub-resource under that resource. |
| 405 | `method_not_allowed` | That HTTP method is not supported on this endpoint. The Allow header lists the ones that are. |
| 400 | `invalid_json` | The request body is not valid JSON. |
| 400 | `events_required` | The body needs a non-empty events list. |
| 400 | `invalid_id` | The id is not in the expected format for this resource. |
| 400 | `invalid_since` | since must be an ISO-8601 date-time. |
| 400 | `invalid_cursor` | The cursor is not one this endpoint issued. Start again without it. |
| 400 | `invalid_sort` | The sort value is not one this endpoint supports. |
| 400 | `invalid_offset` | offset must be a whole number of zero or more. |
| 400 | `invalid_paging` | The paging parameters are not valid together. |
| 400 | `invalid_assignee` | The assignee is not a member of this Space. |
| 413 | `batch_too_large` | The request carries more items than one call accepts. Split it. |
| 413 | `body_too_large` | The request body is larger than this endpoint accepts. |
| 400 | `invalid_idempotency_key` | Idempotency-Key must be 8 to 255 printable characters with no spaces. |
| 409 | `idempotency_key_reused` | That Idempotency-Key was already used with a different request. Use a new key for a new request. |
| 409 | `idempotency_in_progress` | The first request with this Idempotency-Key is still running. Retry after Retry-After. |
| 503 | `idempotency_unavailable` | Idempotency-Key cannot be honoured right now, so nothing was done. Retry later. |
| 402 | `usage_exhausted` | The Space has used all its agent units and has no pack credit, extra usage or automatic top up. Stop and tell the owner; top_up_url in the body is where they add units. |
| 429 | `rate_limited` | Too many requests in the trailing hour. Wait for Retry-After. |
| 429 | `resend_too_soon` | That invitation was sent recently. Wait for Retry-After. |
| 500 | `server_error` | Something failed on our side. Retry, and quote the request_id if it keeps happening. |

## Safe retries

Send `Idempotency-Key: <unique value, 8-255 printable characters>` on the calls marked yes above. Retrying the same request with the same key within 24 hours returns the first answer with `Idempotent-Replayed: true` and does the work once. The same key with a different body is `409 idempotency_key_reused`. Keys belong to your own key and are never shared across keys or Spaces. A `5xx` or `429` is not stored, so retry those with the same key.

## Limits

- Developer key: 60 calls per hour
- Free Space key: 1,000 calls per hour
- Paid Space key: 10,000 calls per hour
- Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.

## Permissions (scopes)

- `read:events`: Activity
- `read:tasks`: Tasks
- `read:users`: Member profiles
- `read:forms`: Forms and answers
- `read:payments`: Payments
- `read:*`: Everything readable
- `read:users:pii`: Member emails (by approval: ask the key owner)
- `embed:widget`: Website widget
- `write:resources`: Manage your directory (by approval: ask the key owner)
- `write:beacons`: Send product events (sensitive: named explicitly, never covered by a wildcard)
- `read:beacons`: Read event totals (sensitive: named explicitly, never covered by a wildcard)
- `read:resources:public`: read:resources:public
- `write:resources:public`: write:resources:public
- `read:site-files`: Read your site files (sensitive: named explicitly, never covered by a wildcard)
- `write:site-files`: Change your site files (sensitive: named explicitly, never covered by a wildcard)
- `write:forms`: Create and manage your forms (sensitive: named explicitly, never covered by a wildcard)
- `read:engagement-types`: Read your engagement types (sensitive: named explicitly, never covered by a wildcard)
- `write:engagement-types`: Create and manage your engagement types (sensitive: named explicitly, never covered by a wildcard)

A key only works for the permissions it was made with. To get one more, an agent key calls `POST /agent/access-requests` and the owner decides.

## Webhooks

Events reach your server signed: `X-Bynefit-Signature: sha256=<hex>` is HMAC-SHA256 of `<X-Bynefit-Timestamp>.<raw body>` with the endpoint secret. Verify it, and reject old timestamps. `X-Bynefit-Event-Id` is stable across retries and replays, so use it to de-duplicate. A replayed delivery carries `X-Bynefit-Replay: true`. Send a test event from the endpoint's page to learn the fields before anything real happens.
