bettercollected is open source, onder de Apache License 2.0. De code, ook van alles op deze pagina, staat in de openbare repository. Deze pagina beschrijft bettercollected 4.1; de links naar de documentatie gaan naar release 4.1.1.

Wat er beschikbaar is

  • REST API: formulieren als JSON-documenten. Maak, controleer, wijzig, publiceer en verwijder ze, en lees de antwoorden.
  • MCP-server: dezelfde acties als tools voor een AI-client die jij kiest.
  • Python-SDK en CLI (opdrachtregeltool): een getypeerde client, en de opdracht bettercollected om formulieren als code te beheren.
  • TypeScript-SDK: een getypeerde client die alleen fetch nodig heeft.

Alle vier gebruiken dezelfde API-sleutels, dezelfde rechten en dezelfde publicatiecheck als de formulierbouwer. De SDK's staan op PyPI en npm, en werken met bettercollected 4.1 of nieuwer. Beide pakketten worden gebouwd en gepubliceerd door de CI van de openbare repository, met herkomstgegevens (provenance): npm audit signatures controleert het npm-pakket, en PyPI toont de attestaties van elk bestand onder Download files.

API-sleutels

Een beheerder van de werkruimte maakt sleutels in het dashboard, onder AI → MCP. Daar staat ook het MCP-adres van jouw omgeving.

  • Een sleutel werkt alleen in zijn eigen werkruimte. Hij begint met bc_, gevolgd door 64 tekens, en je ziet hem één keer: als je hem maakt. De server bewaart alleen een hash.
  • Elke sleutel heeft scopes: wat hij mag. Een sleutel doet nooit meer dan wat de maker op het moment van de aanroep zelf mag. Bij elke aanroep worden diens rechten opnieuw gecontroleerd.
  • Je trekt een sleutel in op hetzelfde tabblad. De lijst laat zien wanneer elke sleutel het laatst is gebruikt.

De scopes die een sleutel kan hebben:

  • forms:read: formulieren opvragen, lezen en controleren
  • forms:write: formulieren maken, wijzigen en publiceren
  • forms:delete: formulieren verwijderen, met hun antwoorden (forms:write mag dat niet)
  • responses:read: antwoorden opvragen en lezen, met de volledige antwoorden
  • deletion_requests:read: openstaande verwijderverzoeken opvragen (MCP)
  • deletion_requests:write: het verwijderverzoek van een invuller afhandelen (REST API)

Wie een sleutel met responses:read heeft, krijgt de volledige antwoorden, zonder dat er iets is weggelaten. Het dashboard vraagt de beheerder dat eerst te bevestigen. Wie bevestigde en wanneer, wordt bij de sleutel bewaard.

Auditlog. Elke aanroep met een sleutel, via de REST API of de MCP-server, komt in een auditlog op de server: welke sleutel, welke actie, of het lukte, en wanneer. Het log bewaart id's en aantallen, nooit de inhoud van formulieren of antwoorden. Je kunt het nog niet bekijken in het dashboard.

REST API

De API werkt met formulieren als documenten volgens form spec v1. De API-documentatie noemt elk endpoint, en het OpenAPI-document beschrijft ze volledig. De documentatie is in het Engels.

  • Basis-URL: https://<jouw-omgeving>/api/v1/public/v1
  • Authenticatie: Authorization: Bearer <API-sleutel>. Cookies worden nooit gelezen, dus een browsersessie werkt niet met de API.

De voorbeelden lezen de sleutel uit een omgevingsvariabele, zodat hij niet in je code staat. Haal hem uit je wachtwoordkluis of uit een .env-bestand dat je nooit in git zet, of typ hem in zonder dat hij zichtbaar is. Dan komt hij ook niet in je shellgeschiedenis:

export BETTERCOLLECTED_URL="https://<jouw-omgeving>"
read -rs BETTERCOLLECTED_API_KEY && export BETTERCOLLECTED_API_KEY   # plak de sleutel; je ziet hem niet
API="$BETTERCOLLECTED_URL/api/v1/public/v1"

Een formulier maken

Sla het formulier op als aanmelden.json. Het veld whyWeAsk is verplicht bij vragen naar een e-mailadres, een telefoonnummer of een identiteitsnummer: zonder dat veld keurt de publicatiecheck het formulier af.

{
  "specVersion": "1",
  "title": "Aanmelden voor de nieuwsbrief",
  "language": "nl",
  "pages": [
    {
      "id": "pagina-1",
      "fields": [
        {
          "id": "email",
          "type": "email",
          "title": "Je e-mailadres",
          "whyWeAsk": "Om je de nieuwsbrief te sturen.",
          "validations": { "required": true }
        }
      ]
    }
  ],
  "endings": [{ "title": "Bedankt", "message": "Je bent aangemeld." }],
  "trust": {
    "purpose": "Om je elke maand onze nieuwsbrief te sturen.",
    "retention": { "type": "days", "value": 365 }
  }
}

Maak er een concept van (met forms:write). De API stuurt de nieuwe formId en het opgeslagen document terug, met een ETag-header voor de versie.

curl -sS -X POST "$API/forms" \
  -H "Authorization: Bearer $BETTERCOLLECTED_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nieuwsbrief-aanmelden-v1" \
  --data @aanmelden.json

Met de Idempotency-Key kun je veilig opnieuw proberen: dezelfde sleutel met hetzelfde verzoek binnen 24 uur geeft de eerste reactie terug, in plaats van een tweede formulier te maken. Gebruik een nieuwe sleutel voor een ander document. Gebruik je dezelfde sleutel binnen 24 uur opnieuw voor iets anders, dan krijg je 422 idempotency_key_reused.

Wil je een document alleen controleren, zonder iets op te slaan? Stuur het dan naar POST /forms/validate.

Publiceren

FORM_ID="<formId uit de reactie hierboven>"
curl -sS -X POST "$API/forms/$FORM_ID/publish" \
  -H "Authorization: Bearer $BETTERCOLLECTED_API_KEY"

Bij publiceren voert de server de publicatiecheck uit, dezelfde als in de bouwer:

  • niets is vooraf gekozen;
  • geen verplichte vraag die niemand kan beantwoorden;
  • een reden bij elke vraag naar een e-mailadres, een telefoonnummer of een identiteitsnummer;
  • een einde voor elke sprong;
  • themakleuren die goed leesbaar zijn;
  • de werkruimte heeft een naam. Invullers zien die als degene die hun antwoorden ontvangt.

De bouwer controleert ook of elke vraag een titel heeft. Komt een formulier niet door de check, dan krijg je 422 form_not_publishable. De message noemt elk probleem, met een bericht over wat je moet aanpassen. Geef je werkruimte een naam in de instellingen voordat je publiceert: een werkruimte die nog "untitled" heet, telt als een werkruimte zonder naam.

Antwoorden lezen

Hiervoor is responses:read nodig. De lijst begint bij het nieuwste antwoord; size is hoogstens 100.

curl -sS "$API/forms/$FORM_ID/responses?page=1&size=20" \
  -H "Authorization: Bearer $BETTERCOLLECTED_API_KEY"

curl -sS "$API/forms/$FORM_ID/responses/<responseId>" \
  -H "Authorization: Bearer $BETTERCOLLECTED_API_KEY"

Het account waarmee iemand antwoordde (het e-mailadres waarmee diegene inlogde, of de identiteit) en de URL-parameters die een formulier vastlegde, komen nooit mee. De antwoorden op de vragen van het formulier wel, ook op een e-mailvraag zoals hierboven. Heeft de invuller gevraagd om een antwoord te verwijderen, dan staat erbij: "deletionRequest": {"status": "pending"}. Houd daar rekening mee in elke kopie die je bewaart.

Goed om te weten

  • Versies. GET /forms/{formId} stuurt een ETag mee. Stuur die terug als If-Match wanneer je een formulier vervangt (PUT) of bewerkt (POST .../ops). Is het formulier intussen gewijzigd, dan krijg je 412 en wordt er niets overschreven.
  • Bewerken. POST /forms/{formId}/ops voert bewerkingen uit, zoals een vraag toevoegen: alles of niets. De /dry-run-endpoints laten zien wat er zou veranderen en slaan niets op.
  • Fouten hebben altijd dezelfde vorm: een vaste code en een message. Is een document ongeldig, dan noemt errors elk probleem, met een JSON-pointer naar de plek in je verzoek.
  • Limieten. Er is een maximum aantal verzoeken per sleutel en per werkruimte. Daarboven krijg je 429 met een Retry-After-header.

MCP-server

De MCP-server geeft een AI-client dezelfde acties als tools. Hij gebruikt streamable HTTP en dezelfde sleutels en scopes als de REST API, en zijn aanroepen komen in hetzelfde auditlog.

  • Adres: https://<jouw-omgeving>/api/v1/mcp/. Het tabblad AI → MCP toont het zonder de laatste schuine streep; de server stuurt dat adres door naar het adres met de streep. Volgt je client geen doorverwijzingen, gebruik dan het adres met de streep.
  • Authenticatie: Authorization: Bearer <API-sleutel>

Tools: list_forms, get_form, get_form_spec, create_form, update_form, validate_form, dry_run_ops, publish_form, list_responses, get_response, list_deletion_requests en get_ai_profile.

Een MCP-client koppelen

Geef de sleutel alleen de scopes die de client nodig heeft. In Claude Code:

claude mcp add --transport http bettercollected "$BETTERCOLLECTED_URL/api/v1/mcp/" \
  --header "Authorization: Bearer $BETTERCOLLECTED_API_KEY"

Je shell vult de variabele in, dus de sleutel zelf komt in de eigen configuratie van Claude Code. Houd die configuratie privé, en voeg deze server nooit toe met --scope project: dan komt de sleutel in .mcp.json in je project, en zo makkelijk in git.

Wil je de server met een project delen, zet hem dan in de .mcp.json van het project (het projectbestand van Claude Code), met een verwijzing naar de variabele in plaats van de sleutel:

{
  "mcpServers": {
    "bettercollected": {
      "type": "http",
      "url": "https://<jouw-omgeving>/api/v1/mcp/",
      "headers": { "Authorization": "Bearer ${BETTERCOLLECTED_API_KEY}" }
    }
  }
}

Claude Code vult ${BETTERCOLLECTED_API_KEY} in vanuit de omgeving. Andere clients hebben hun eigen bestand en vorm: kijk of jouw client ${…}-variabelen invult. Doet hij dat niet, bewaar de sleutel dan in de beveiligde opslag voor wachtwoorden en sleutels van je client, en zet nooit een bestand met een sleutel in git. Met responses:read krijgen de AI-client en de AI-aanbieder erachter de volledige antwoorden.

De AI-formulierassistent

bettercollected heeft een AI-formulierassistent. Die kan een formulier opzetten vanuit een beschrijving en wijzigingen voorstellen in de bouwer. Via MCP maken de tools create_form_with_ai, create_form_outline en build_form_from_outline (met fill_gaps) er gebruik van. De assistent staat uit totdat een beheerder van de werkruimte hem aanzet, voor één AI-aanbieder die op de omgeving is ingesteld. Tot dan weigert de server elk AI-verzoek (403 ai_not_enabled) en gaat er niets naar een AI-aanbieder. Op je eigen omgeving stel je zelf de aanbieder en de sleutel in.

Python-SDK en CLI

Een kleine getypeerde client (httpx en Pydantic) en de opdracht bettercollected. Zie de documentatie van de Python-SDK (in het Engels) en het pakket op PyPI.

Installeer hem vanaf PyPI (Python 3.10 of nieuwer). Je krijgt de bibliotheek en de opdracht bettercollected:

pip install bettercollected

# of in een uv-project
uv add bettercollected

# of alleen de opdracht, als uv-tool
uv tool install bettercollected

Maken, publiceren en antwoorden lezen:

import json
import os

from bettercollected import BetterCollected, BetterCollectedError

with open("aanmelden.json") as f:
    document = json.load(f)

with BetterCollected(os.environ["BETTERCOLLECTED_API_KEY"], os.environ["BETTERCOLLECTED_URL"]) as bc:
    created = bc.create_form(document, idempotency_key="nieuwsbrief-aanmelden-v1")
    try:
        bc.publish_form(created.form_id)
    except BetterCollectedError as error:
        print(error)  # de melding noemt wat je moet aanpassen, daarna de code

    for summary in bc.list_responses(created.form_id).items:
        response = bc.get_response(created.form_id, summary.response_id)
        print(response.submitted_at, response.answers)

Formulieren als code met de CLI. Die leest de sleutel alleen uit BETTERCOLLECTED_API_KEY, en de omgeving uit --url of BETTERCOLLECTED_URL:

bettercollected pull <formId> -o aanmelden.json   # zet het formulier in een bestand
bettercollected validate aanmelden.json           # controleer het
bettercollected diff aanmelden.json               # wat push zou veranderen
bettercollected push aanmelden.json               # vervang het formulier, als niemand het sinds de pull wijzigde

TypeScript-SDK

Een kleine getypeerde client die alleen fetch gebruikt (Node.js 22 of nieuwer; alleen ES-modules). Zie de documentatie van de TypeScript-SDK (in het Engels) en het pakket op npm.

Installeer hem vanaf npm:

npm install @bettercollected/sdk
import { readFile } from "node:fs/promises";
import { BetterCollected, BetterCollectedError } from "@bettercollected/sdk";

const bc = new BetterCollected({
  apiKey: process.env.BETTERCOLLECTED_API_KEY!,
  baseUrl: process.env.BETTERCOLLECTED_URL!,
});

const document = JSON.parse(await readFile("aanmelden.json", "utf8"));
const { formId } = await bc.createForm(document, { idempotencyKey: "nieuwsbrief-aanmelden-v1" });
try {
  await bc.publishForm(formId);
} catch (error) {
  // de melding noemt elk probleem en wat je moet aanpassen
  if (error instanceof BetterCollectedError) console.error(error.code, error.message);
}

const { items } = await bc.listResponses(formId);
for (const summary of items) {
  const response = await bc.getResponse(formId, summary.responseId);
  console.log(response.submittedAt, response.answers);
}

Zelf draaien

bettercollected draait ook op je eigen servers. De README van de repository legt uit hoe (in het Engels). De API, de MCP-server en de SDK's werken daar hetzelfde; gebruik je eigen adres als BETTERCOLLECTED_URL.

Vragen, of mis je iets? Open een issue of lees wat je nog meer kunt koppelen (in het Engels).