Home›Membora›API
Membora · API

Membora API, as shipped in the download

Membora REST API & Webhooks (v2.0)

Membora ships a JSON REST API and signed webhooks so your club's other systems — a website membership form, a bank-feed reconciliation script, a mailing-list sync — can read the register and record attendance, using exactly the same rules the secretary uses.

Authentication

Create a key in Settings → API & Webhooks. A key belongs to a user and can do exactly what that user can do in the app. Send it on every call:

Authorization: Bearer apk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.

A key also carries a scope. A read-only key is refused every write at the door, in one place, rather than in each route — the route that forgets is exactly the one that would leak.

Endpoints

Method & pathWhat it does
GET /api/pingVerify a key
GET /api/membersMembers with their current subscription; ?q= searches name/email/phone
POST /api/membersCreate a member (name required; email, phone, join_date, notes)
GET /api/members/{id}One member + subscription, the voting verdict, payments, attendance, offices held
POST /api/members/{id}/paymentsRecord a payment (amount or amount_cents; optional due_date, method, paid)
GET /api/meetingsMeetings, each with its live quorum state
GET /api/meetings/{id}One meeting with its attendance and its motions, each motion tallied
POST /api/attendanceRecord attendance (member_id, meeting_id, optional state)
GET /api/arrearsOverdue subscriptions with the total, as of today
GET /api/openapi.jsonOpenAPI 3 spec (no auth)

Record attendance at a meeting

curl -X POST https://your-install/api/attendance \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"member_id": 12, "meeting_id": 3, "state": "present"}'

201 → {"attendance": {...}, "quorum": {"present": 7, "required": 8, "quorate": false}}
409 → {"error": "conflict", "detail": "That meeting was canceled."}

Note what the 201 gives you: the running quorum. That is the number a club actually wants back, and it is why the response is not just an echo of what you sent.

Standing is not checked here, deliberately. A member whose subscription has lapsed is recorded as present, because they were present, and they count toward quorum. Whether arrears cost them a vote is a separate question, answered at the ballot — see GET /api/members/{id}, which returns a voting object with allowed and a reason.

One row per member per meeting. The write upserts against a unique database index, so recording the same person twice updates the one row rather than adding a second. This is not tidiness: quorum is counted from these rows, and a duplicate would make a meeting look quorate when it was not.

The API is not a second implementation. Every attendance write runs the same record_attendance() the roll call, the meeting page and the MCP endpoint call, and every standing question runs the same Membership::votingGuard(). There is no second, weaker path.

Webhooks

Add receiver URLs in Settings → API & Webhooks. Events:

  • meeting.attendance — the member, the meeting, the state recorded, and the resulting quorum
  • motion.decided — the motion, its outcome, and the numbers it was decided on (present, quorum_required). Fired after the write, so a subscriber can never learn of an outcome that was rolled back.
  • attendance.refused — an attendance that could not be recorded (a canceled or full meeting), with the reason
  • payment.recorded — any payment lands

An endpoint saved before 1.0.3 under the old name meeting.attended receives meeting.attendance.

Each delivery is an HTTP POST with a JSON body and:

X-Membora-Event: meeting.attendance
X-Membora-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret>

Verify the signature before trusting a payload. Delivery is one attempt with a 5-second timeout — deliberately simple; the Recent deliveries log on the settings page shows every attempt and response code. Design receivers to be idempotent.

Every door fires the same events: a mark taken on the roll, one posted over the API and one made by an agent are indistinguishable to your receiver.

Why motion.decided carries the numbers

Because the decision is only worth as much as the meeting that took it. A receiver that files minutes, updates a public register or notifies members can state "carried, 9 of 8 present" without asking Membora a second question — and without re-implementing quorum, which is the rule most likely to be got wrong twice.

Limits, honestly

  • The API acts at user level; there is no separate scope system beyond the app's roles.
  • One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
  • List endpoints cap at 200 rows; this is a club register, not a data warehouse.
  • A subscription lapsing is computed on view — there is no scheduler, so there is no "subscription lapsed" webhook. GET /api/members reports the effective standing live.
  • Postal and proxy votes are not modeled. A vote is recorded against the member who cast it; a club that runs postal ballots will need to record the outcome as a motion and keep the ballot papers elsewhere.

MCP — the agent endpoint (new in 3.0)

Membora speaks MCP (Model Context Protocol) on one route, so an assistant can read the register and help keep the minutes without anyone building a bridge first.

POST /mcp
Authorization: Bearer apk_...          ← the SAME revocable key the REST API uses
Content-Type: application/json

Stateless streamable-HTTP: one JSON-RPC message per request, no session, no SSE. A GET gets 405 with Allow: POST, as the transport requires.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions about the register but never change it — the endpoint then lists only the six read tools and refuses the rest by name, so a careless prompt cannot record a vote. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http membora https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "membora",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Membora implements the protocol, not an integration with a particular vendor.

The tools

ToolWrites?What it does
list_membersnoThe register with each member's effective standing and whether their vote would be recorded — the ballot's own verdict, not a second opinion.
member_detailnoOne member: subscription, attendance record, offices held, how they voted, payments, arrears.
list_meetingsnoMeetings with their live quorum state — present against required, and whether the meeting may decide anything.
meeting_detailnoOne meeting: who was present, who sent apologies, its motions and how each vote fell.
subscriptions_lapsingnoSubscriptions ending within N days — the members about to lose their vote. Life and honorary rates never appear.
subscription_reportnoCollected in a month, outstanding arrears, meetings held, motions decided. Integer cents.
record_attendanceYESMarks a member present, apologies or expected. Answers with the running quorum.
record_voteYESRecords how a member voted on an open motion.

A read-only key sees only the six read tools. They are not merely refused — they are absent from tools/list, so the model never proposes them in the first place. Ask for one by name anyway and the server answers with a sentence explaining that the key is read-only.

What the endpoint refuses, and why

  • record_attendance never refuses on standing. A member in arrears is recorded as present, because they were, and they count toward quorum. If this ever started refusing, the register would have stopped being a record of fact.
  • record_attendance cannot inflate quorum. It calls the same record_attendance() the roll call, the meeting page and the REST API call, upserting against a unique index — so an agent and a clerk working the same roll cannot create two rows for one person.
  • **record_vote refuses a member whose subscription has lapsed, been suspended or resigned.** That is the constitution's rule, applied at the ballot rather than at the door, and the refusal names the reason and the lapse date so the agent can relay it.
  • record_vote refuses a decided motion. Re-voting one rewrites history instead of correcting it; the correction is a new motion, which is also how a committee would do it.
  • A motion cannot be carried at an inquorate meeting. The refusal states the numbers — "4 present, 5 required". Withdrawal is always allowed.
  • Arguments must be single values. Anything nested is rejected — the same guard Api::body() applies to the REST API.
  • Unknown arguments are refused by name. An agent that typos stauts is told so, rather than silently getting unfiltered results.
  • Out-of-enum values are refused, with the valid options listed.
  • Roles apply exactly as they do in the browser. A key belonging to a viewer is refused record_attendance — a different answer for the same role would make the agent path a privilege hole.

Every MCP write lands in the audit trail beside the browser and API writes, naming the key's user as the actor and recording via: mcp.

Example

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"record_attendance",
                 "arguments":{"member_id":42,"meeting_id":3,"state":"present"}}}'

→ {"ok":true,"member":"Alicia Moreno","meeting":"Annual General Meeting",
   "state":"present","quorum":{"present":8,"required":8,"quorate":true}}

Connect it to Zapier, Make or n8n

Membora talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.

Before you start

  • In Membora, open Settings → API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
  • Use your install's full address, for example https://membora.yourcompany.com.

When something happens in Membora: send it to another tool

  1. In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
  2. Copy that URL into Membora under Settings → API & Webhooks, and choose the event or events below.
  3. Make the event happen once in Membora, so the other tool receives a sample and you can map its fields.
EventFires when
meeting.attendanceThe member, the meeting, the state recorded, and the resulting quorum
motion.decidedThe motion, its outcome and the numbers it was decided on, sent after the vote is saved
attendance.refusedAn attendance that could not be recorded (a canceled or full meeting), with the reason
payment.recordedAny payment lands

Every delivery carries X-Membora-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.

When something happens elsewhere: act in Membora

  • Zapier: "Webhooks by Zapier", action POST (or Custom Request).
  • Make: module HTTP → Make a request.
  • n8n: the HTTP Request node.

Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.

The endpoints that change something:

  • POST /api/members: Create a member (name required; email, phone, join_date, notes)
  • POST /api/members/{id}/payments: Record a payment (amount or amount_cents; optional due_date, method, paid)
  • POST /api/attendance: Record attendance (member_id, meeting_id, optional state)

Example recipes

  • meeting.attendance → a row in Google Sheets: webhook trigger on meeting.attendance (The member, the meeting, the state recorded, and the resulting quorum), then the Google Sheets "Create row" action with the fields you want.
  • motion.decided → a message in Slack or Teams: webhook trigger on motion.decided (The motion, its outcome, and the numbers it was decided on (, ). Fired after the write, so a subscriber can never learn of an outcome…), then the Slack or Microsoft Teams "Send message" action.
  • A new form entry or row elsewhere → Membora: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling POST /api/members (Create a member (name required; email, phone, join_date, notes)).

These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Membora's own automated tests check its API.

← Back to Membora · Manual · Quickstart · Test run