{
  "openapi": "3.1.0",
  "info": {
    "title": "Elpia",
    "version": "1",
    "summary": "Activities become Drops, Drops become documented donations.",
    "description": "Bodies and answers are described in prose here; see /developers for authentication, limits and house rules."
  },
  "servers": [
    {
      "url": "https://elpia.org/"
    }
  ],
  "tags": [
    {
      "name": "public"
    },
    {
      "name": "auth"
    },
    {
      "name": "me"
    },
    {
      "name": "client"
    },
    {
      "name": "link"
    },
    {
      "name": "admin"
    },
    {
      "name": "ops"
    }
  ],
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "better-auth.session_token"
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "A session token for a person, or the API key of an app or device."
      }
    }
  },
  "paths": {
    "/api/auth/sign-up/email": {
      "post": {
        "summary": "Creates an account and sends the confirmation e-mail.",
        "tags": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "description": "`email`, `password` (at least 10 characters), `name`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the new user and a session",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/sign-in/email": {
      "post": {
        "summary": "Signs in with an e-mail address.",
        "tags": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "description": "`email`, `password`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "a session; the token is also in the `set-auth-token` header for callers without cookies",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/sign-in/username": {
      "post": {
        "summary": "Signs in with a username instead of an e-mail address.",
        "tags": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "description": "`username`, `password`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "a session",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/sign-out": {
      "post": {
        "summary": "Ends the current session.",
        "tags": [
          "auth"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ success: true }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/update-user": {
      "post": {
        "summary": "Changes the profile: display name, username and whether Deeds may show the name.",
        "tags": [
          "auth"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "any of `name`, `username`, `defaultVisibility`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the updated user",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/request-password-reset": {
      "post": {
        "summary": "Sends a password reset link. Answers the same way whether or not the address exists.",
        "tags": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "description": "`email`, `redirectTo`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`{ status: true }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/reset-password": {
      "post": {
        "summary": "Sets a new password and ends all other sessions of that account.",
        "tags": [
          "auth"
        ],
        "security": [],
        "requestBody": {
          "description": "`token` from the e-mail link, `newPassword`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`{ status: true }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/api/auth/verify-email": {
      "get": {
        "summary": "Confirms an e-mail address from the link in the message.",
        "tags": [
          "auth"
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "the token from the e-mail link",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "a redirect back to the website",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/rules": {
      "get": {
        "summary": "The earning rules that are active right now.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "`{ rules: [...] }` with reward, unit, daily cap, the minimum trust level and `audience`; invited-only rules add `invitedCount` (never names)",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/changelog": {
      "get": {
        "summary": "Every rule version with its reason, plus every budget deposit. Nothing is ever changed in place.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "`{ ruleVersions: [...], budgetDeposits: [...] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/budget": {
      "get": {
        "summary": "Where the money behind the Drops is: deposited, not yet issued, in wallets, reserved, donated.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "amounts in milli-units as decimal strings",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/catalog": {
      "get": {
        "summary": "The redemption catalog by category, with current prices and pool state.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "`{ categories: [...], providers: [...] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/catalog/{slug}": {
      "get": {
        "summary": "One project with its complete price history.",
        "tags": [
          "public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "project slug, e.g. `ecologi-tree`",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the project, its provider, pool and `priceHistory`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/deeds": {
      "get": {
        "summary": "All settled payouts. Contributors who chose anonymity have `name: null`.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "`{ deeds: [...] }` newest first",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/deeds/{id}": {
      "get": {
        "summary": "One Deed with its proof: the snapshot frozen at payout time, the ledger posting and a fresh check of the receipt hash.",
        "tags": [
          "public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "payout id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the Deed plus `receiptCheck`: `matches`, `differs` or `missing`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/payouts/{id}/receipt": {
      "get": {
        "summary": "The receipt file of a payout, so anyone can hash it themselves.",
        "tags": [
          "public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "payout id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the file as it was uploaded",
            "content": {
              "application/pdf, image/png, image/jpeg or image/webp": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/impact": {
      "get": {
        "summary": "The headline totals: donated and queued Drops, Deeds, contributors.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "one object with four numbers",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/stats": {
      "get": {
        "summary": "What the Deeds financed per project, with CO₂e grouped by period, impact type and how well the figure is backed. Living units are counted from the day they were paid for, so the totals grow on their own.",
        "tags": [
          "public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "`{ since, deeds, contributors, financed: [...], co2e: { groups, dropsWithoutFactor } }`; each group carries `accumulated` and `perYear`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/map": {
      "get": {
        "summary": "Activity per country for the world map. Never finer than a country.",
        "tags": [
          "public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "window of 1 to 90 days, 7 by default",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ days, countries: [{ country, activities, people }] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/public/footprint": {
      "get": {
        "summary": "The server's estimated CO₂e and water use as reported by the hosting provider, the website's traffic, and what the Deeds dealt with in the same days. They stand side by side; nothing is offset or subtracted.",
        "tags": [
          "public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "`day`, `week`, `month` (default) or `year`: a rolling window ending on the last day the provider has figures for",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ period, since, window: { from, to, days, bucket }, server: { co2eGrams, waterLitres, daysWithData }, visits: { bytes, responses, daysWithData, since }, buckets: [{ start, days, co2eGrams, waterLitres, bytes, uptake }], deeds: [{ period, impactType, basis, units, amount, sourceUrls }] }`; `window` and `server` are null before the first figures arrive, `visits` before the website reported traffic; `uptake` is the estimated daily uptake of living units, never subtracted from anything",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "summary": "The signed-in person with their Drops and Sparks balance.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ user, balances }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/activities": {
      "get": {
        "summary": "Your own reported activities and what they earned.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ activities: [...] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      },
      "post": {
        "summary": "Reports an activity yourself. Trust level `self_reported`, so most rules will not pay for it.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`source`, `type`, `quantity`, `unit`, `startedAt`, `endedAt`, optional `externalId` and `zone`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the activity and the reward decision",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/stats": {
      "get": {
        "summary": "Your own share: what your Drops financed and the CO₂e estimate behind it, counted from the day each Deed was paid.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "the same shape as `/v1/public/stats`, limited to your redemptions",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/invitations": {
      "get": {
        "summary": "The rules you are invited to. Rules with audience `invited` pay only invited people; being invited also lets you confirm your device's link code.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ invitations: [{ ruleId, key, title, source, translations, createdAt }] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/redemptions": {
      "get": {
        "summary": "Your redemptions with their status.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ redemptions: [...] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      },
      "post": {
        "summary": "Turns Drops into a queued donation; the Drops are reserved at today's price.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`projectId` plus `quantity` (unit projects) or `drops` (free amount), optional `visibility`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the redemption",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/redemptions/{id}/cancel": {
      "post": {
        "summary": "Takes a redemption back while it is still queued and returns the Drops.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "redemption id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "the cancelled redemption",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/clients": {
      "get": {
        "summary": "Your connected apps and devices.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ clients: [...] }` without key hashes",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      },
      "post": {
        "summary": "Creates an API key for an app. The key is shown once and only its hash is stored. Only admins may do this for now (403 `clients_owner_only` otherwise); a key made by anyone else counts as a third-party app.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`name`, optional `kind` (`first_party` for admins), optional `signingPublicKey` (Ed25519, base64)",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the client and `apiKey`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/clients/{id}": {
      "delete": {
        "summary": "Revokes a device: its key stops working immediately.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "client id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "nothing"
          }
        }
      }
    },
    "/v1/me/link/{code}": {
      "get": {
        "summary": "Shows which app and device are waiting behind a link code.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "the code shown on the device, e.g. `K7QM-3XRA`",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "app and device name",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/link/{code}/approve": {
      "post": {
        "summary": "Approves a device. It may then report activities for you, nothing else. Only admins may do this for now (403 `clients_owner_only`).",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "the code shown on the device",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "nothing"
          }
        }
      }
    },
    "/v1/me/link/{code}/deny": {
      "post": {
        "summary": "Refuses a link request.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "the code shown on the device",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "nothing"
          }
        }
      }
    },
    "/v1/me/export": {
      "get": {
        "summary": "Your data as one JSON file (GDPR art. 15 and 20). Contains no password hash, token or key hash.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "a download with profile, devices, activities, rewards, redemptions and postings",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/me/delete": {
      "post": {
        "summary": "Deletes your account by anonymising it: the ledger and the Deeds stay, everything that points at you goes. Queued redemptions are donated anonymously, the rest of the Drops goes back to the budget.",
        "tags": [
          "me"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`password`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "nothing"
          }
        }
      }
    },
    "/v1/client/activities": {
      "post": {
        "summary": "Reports one activity. `externalId` makes it idempotent, so a retry never pays twice. With an Ed25519 signature in `X-Elpia-Signature` the activity counts as `verified`, otherwise as `device`.",
        "tags": [
          "client"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`source`, `type`, `quantity`, `unit`, `startedAt`, `endedAt`, `externalId`, optional `zone`, `energyWhMeasured`, `energyWhEstimated`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the activity and the reward decision",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/client/me": {
      "get": {
        "summary": "What this key reported and earned (last 24 hours and in total), the latest reward decision and the account's balances. Nothing about other apps.",
        "tags": [
          "client"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ client, last24h, total, lastRewardStatus, balances }`, amounts in milli units",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Revokes this key, e.g. when someone logs out on the device. It stops working immediately and cannot be restored.",
        "tags": [
          "client"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "204": {
            "description": "nothing"
          }
        }
      }
    },
    "/v1/link/start": {
      "post": {
        "summary": "Starts pairing and returns a code the person types into the website.",
        "tags": [
          "link"
        ],
        "security": [],
        "requestBody": {
          "description": "`app`, `deviceName`, optional `signingPublicKey`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "`{ code, pollToken, expiresIn, interval }`; the code is valid for 10 minutes",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/link/poll": {
      "post": {
        "summary": "Asks whether the person approved. The API key comes back exactly once, on the first approved answer.",
        "tags": [
          "link"
        ],
        "security": [],
        "requestBody": {
          "description": "`pollToken`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`pending`, `denied`, `expired`, or `approved` with `apiKey`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/audit": {
      "get": {
        "summary": "Checks the books: balanced postings, no unexpected negative balances, redemptions matching payouts, receipts matching their hash.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "the report; 409 instead of 200 when there are findings",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/rules": {
      "get": {
        "summary": "All rule versions, including the disabled ones.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ versions: [...] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      },
      "post": {
        "summary": "Creates a rule with its first version.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`key` and a `version` object with reward, unit, caps, minimum trust and a public `reason`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the rule and its version",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/rules/{id}/versions": {
      "post": {
        "summary": "Adds a new version of a rule. The old one stays readable; nothing is ever rewritten.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "rule id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "the new version with a public `reason`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the new version",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/rules/{id}/invitations": {
      "get": {
        "summary": "Who is invited to a rule that only pays invited people.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "rule id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ invitations: [{ userId, username, email, createdAt }] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      },
      "post": {
        "summary": "Invites a person to a rule. Inviting someone twice changes nothing; an unknown person gives 404 `user_not_found`.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "rule id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "`user`: username or e-mail address",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the invited person",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/rules/{id}/invitations/{userId}": {
      "delete": {
        "summary": "Removes an invitation. New activities stop earning; rewards already booked stay.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "rule id (UUID)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "the invited person's id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "nothing"
          }
        }
      }
    },
    "/v1/admin/budget/deposits": {
      "post": {
        "summary": "Pays money into the budget. Drops can only be issued from it.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`amountCents`, optional `memo`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the ledger transaction",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/catalog": {
      "get": {
        "summary": "The catalog including inactive providers and projects.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "like the public catalog, but complete",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/categories": {
      "post": {
        "summary": "Creates a category.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`slug`, `name`, optional `description`, `translations`, `sortOrder`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the category",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/categories/{id}": {
      "patch": {
        "summary": "Changes a category. Only the fields that are sent change.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "category id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "any subset of the fields",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the category",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/providers": {
      "post": {
        "summary": "Creates a provider with its minimum payment and fee note.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`slug`, `name`, `website`, optional `minPayout`, `feeNote`, `translations`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the provider",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/providers/{id}": {
      "patch": {
        "summary": "Changes a provider.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "provider id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "any subset of the fields",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the provider",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/projects": {
      "post": {
        "summary": "Creates a project. `impactType` is required, so no card is ever ambiguous about what the money does.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`slug`, `providerId`, `categoryId`, `title`, `kind`, `impactType`, optional `unitName`, `co2e`, `translations`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the project",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/projects/{id}": {
      "patch": {
        "summary": "Changes a project. `kind` is fixed because redemptions depend on it.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "project id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "any subset of the fields; `co2e: null` removes the CO₂e figure",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "the project",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/projects/{id}/prices": {
      "post": {
        "summary": "Adds a price. Prices are append-only and queued redemptions keep the price they were made at.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "project id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "`price`, `currency`, `fxToEur`, public `reason`",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the new price version",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/queue": {
      "get": {
        "summary": "The pools waiting for a payout and whether they have reached the provider's minimum.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "`{ pools: [...] }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/budget/forecast": {
      "get": {
        "summary": "What paying out today would cost, how much the owner would have to add, and how long the budget lasts at the pace of the last 30 days.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "balances, pools and the runway",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/payouts": {
      "post": {
        "summary": "Records a real payment with its receipt and turns the settled redemptions into a Deed. Payouts cannot be changed afterwards.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`projectId`, `redemptionIds`, `amountPaid`, `currency`, `paidAt`, optional `comment`, and the `receipt` file",
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the payout with its snapshot and receipt hash",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/admin/payouts/{id}/reviews": {
      "post": {
        "summary": "Records what the provider delivered after the payment. Append-only: a correction never hides the earlier entry.",
        "tags": [
          "admin"
        ],
        "security": [
          {
            "sessionCookie": []
          },
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "payout id (UUID)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "`status` (`confirmed`, `needs_clarification`, `corrected`), `note`, `evidenceUrl` (required for `confirmed`)",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "the review entry",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Checks the process and its database. Answers 503 when the database is unreachable, so a broken instance is taken out of rotation.",
        "tags": [
          "ops"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "`{ status, database }`",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/v1/ops/traffic": {
      "post": {
        "summary": "The website reports how many compressed bytes and responses it sent per UTC day since its last report; the API adds them up. Only totals: no address, cookie or page, so no visit can be told apart.",
        "tags": [
          "ops"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "`{ days: [{ day, bytes, responses }] }`; days older than 30 days or in the future are skipped",
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`{ stored }`, the number of days added",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    }
  }
}