API Reference openapi.yaml

jamiio API

The shared spine behind the resident app and the trades app — communities, membership, jobs, governance and funds. Twenty-six operations, all of them testable, described by an OpenAPI 3.1 document that is generated from and validated against the running service.

Overview

Everything the two apps disagree about lives here. Whether a trade is verified, what state a job is in, how a community voted, what has been given toward a campaign — facts a second person has to be able to check.

Everything else stays in the client: region packs, copy, currency formatting, every derived dataset. They are identical for everybody, and fetching them would buy nothing but latency and a way to fail.

Quickstart

The service is up and needs no key to check:

curl https://jamiio.net/api/health

Anything that belongs to somebody needs a caller. The simplest is a device key — any stable string you generate and keep:

curl https://jamiio.net/api/profile \
  -H 'x-jamiio-key: 8f2c1a9e-4d7b-4c02-9a11-6e3f0b8d2c55'

Write to it the same way:

curl -X PUT https://jamiio.net/api/profile \
  -H 'x-jamiio-key: 8f2c1a9e-4d7b-4c02-9a11-6e3f0b8d2c55' \
  -H 'content-type: application/json' \
  -d '{"name":"Wanjiru Kamau","unit":"House 14"}'

Base URLs

EnvironmentURL
Productionhttps://jamiio.net/api
Localhttp://localhost:3000/apivercel dev

Functions and database are both in fra1, co-located so a query does not cross an ocean to answer.

Identity

Every row that belongs to somebody keys on a single string. There are two forms, and which one you get is decided by the request, not by you:

FormWhenReaches
user:<id>A verified sessionFollows the person across devices
device:<key>No sessionThat one device, and it says so

Signing in adopts device-keyed records onto the account rather than copying them — POST /profile and POST /providers, each once only. There is no merge and no duplicate: the rows change owner.

A request with no session and no device key is rejected, rather than treated as an anonymous caller. It used to degrade to a shared empty identity, which meant two such requests could read each other's records. If you get 401 Missing device key, that is this.

Authentication

Device key — ownership, not authentication

Send x-jamiio-key with any stable string. It proves nothing about who you are; it establishes that this caller owns the records it created. Treat endpoints secured by this alone as open. It is honest about what it is, which is more than most such schemes.

Bearer token — verified

Send Authorization: Bearer <jwt>. The signature is verified server-side on every request. A client can claim any user id it likes; it cannot forge a signature. An expired or tampered token does not 401 — it falls back to the device key, because the app works signed out.

curl https://jamiio.net/api/memberships \
  -H 'Authorization: Bearer eyJhbGciOi…' \
  -H 'x-jamiio-key: 8f2c1a9e-…'

Send both. The token wins where it is valid, and the key catches the rest.

Privacy

This is enforced, not promised. There is no endpoint, role, or query parameter that returns another person's private fields.

Anyone in the communityOnly the person themselves
Display name — Wanjiru K.
House or lot — House 14
Phone, email, exact coordinates,
contributions, votes, anything financial

The projection is applied server-side in one function, so a new endpoint cannot quietly disagree with it. The public shape has no field for a phone number at all — a field that does not exist cannot be forgotten about in a select *.

The service still uses what it hides. A quorum is counted without publishing who voted, and a campaign total is exact without naming who gave what. Using data and exposing it are different things, and most systems conflate them because exposing it is easier.

Money

Amounts are integers in minor units — cents, not currency. 2500 is twenty-five of whatever the community spends, where that currency has two decimal places. No floating point touches anybody's money.

Totals are counted from the contributions on every read, never stored. A number people trust with their own money must not be able to drift, and a stored count cannot be recounted.

Nothing is charged through this API. Money moves the way the community already moves it; these endpoints record that it did, so a committee can account for it.

Rate limits

Fixed windows of one minute, per caller — verified user, else device key, else IP.

ClassPer minute
/health600
Reads120
Writes30
Votes20

Every response carries the IETF headers, so you never have to guess:

RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 41

Over the limit returns 429 with Retry-After. The limiter fails open: if its own storage is unreachable, requests are allowed rather than refused. Losing the counter should not take the service down with it.

Errors

Every failure is the same shape, so one handler covers all of them:

{ "error": "That invitation has already been used, or has expired" }
StatusMeans
400The request is missing something or malformed.
401No caller could be established. Send a device key.
403Established, but not yours.
404No such record.
409Real, but not in a state that allows this — a spent invite, a closed vote.
429Rate limited. Retry-After says how long.
405Wrong method for that path.

Messages are written to be shown to a person. They are not stable identifiers — branch on the status code, not the string.

All endpoints

GET/healthIs it up, and can it reach the database
GET/communitiesFind one, or read one
POST/communitiesStart one — joins you as admin
PUT/communitiesAmend one you administer
GET/membershipsWhere you belong
POST/membershipsJoin
DEL/membershipsLeave — you can be invited back
GET/invitesPreview one, or list the ones you sent
POST/invitesInvite one named person, once
PATCH/invitesRedeem or revoke
GET/profileYour own — nobody else's
PUT/profileSave it
PATCH/profileWhere you left off
POST/profileAdopt a device's profile on sign-in
GET/providersTrades covering a community
PUT/providersCreate or amend a trade account
POST/providersAdopt one on sign-in
GET/jobsYour requests, or a trade's inbox
POST/jobsRaise one
PATCH/jobsMove it on, with the history kept
GET/proposalsThe notice board
POST/proposalsPropose something
PATCH/proposalsMove, postpone, cancel or publish
POST/votesCast or change a vote
GET/contributionsWhat a campaign raised, and what it buys
POST/contributionsGive toward it
PUT/contributionsSet the target and the plan