{
  "openapi": "3.1.0",
  "info": {
    "title": "crew — the team ledger",
    "version": "3.1.0",
    "description": "One team's record of who is on it, what each agreed to, and what each delivered. Every row is scoped to the ledger's account; a teammate is a principal named on a row, never an account of their own.\n\n## This surface writes, and it now reads the ledger\n\n`GET /_/api/v1/ledger` answers the ledger this session holds, as data: who is on it, what each agreed to, what was delivered, and the counts the page prints. It is drawn by the same function the page at `/crew/ledger` draws with, so a machine and a person read the same rows. The session's organisation names the ledger; a session with no organisation has none and is told so. The six writes below are unchanged. The channel pages — `/crew/`, `/crew/rooms`, `/crew/jobs` and `/crew/upwork` — remain pages and are not published as operations: a machine that needs the client rooms as data is still asking for an operation this app does not serve."
  },
  "servers": [
    {
      "url": "/crew",
      "description": "zpe-crew on zpe.vc"
    }
  ],
  "paths": {
    "/_/api/v1/ledger": {
      "get": {
        "operationId": "getLedger",
        "summary": "The ledger this session holds, as data: who is on it, what each agreed to, and what was delivered.",
        "tags": [
          "ledger"
        ],
        "responses": {
          "200": {
            "description": "The ledger the session's organisation names. `team` is the roster the page draws (a principal, their name, whether it is you, how many pacts name them, how many items they claimed, and the minute of their last event); `pacts` is every agreement in this ledger with the terms, the counterparty's principal, the due minute and the stake; `items` and `events` are the counts the page prints. A member's read is narrowed to their own pacts and items, by the same rule the page uses, not by a filter here.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ledger",
                    "team",
                    "pacts",
                    "items",
                    "events"
                  ],
                  "properties": {
                    "ledger": {
                      "type": "object",
                      "required": [
                        "account",
                        "tenant",
                        "role"
                      ],
                      "properties": {
                        "account": {
                          "type": "string"
                        },
                        "tenant": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string"
                        }
                      }
                    },
                    "team": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pacts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "items": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "events": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "page": {
                      "type": "object",
                      "required": [
                        "number",
                        "of"
                      ],
                      "properties": {
                        "number": {
                          "type": "integer"
                        },
                        "of": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session. The ledger is read by a person the door names; a key names an account and this read is the account holder's own, so it asks for the cookie the page asks for.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The door named no organisation on this session, or this principal holds no seat in the ledger that organisation names. Nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door did not answer, so nobody could be named and nothing was read. A retry costs nothing.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/pacts/{id}/accept": {
      "post": {
        "operationId": "postPactsByIdAccept",
        "summary": "Send the person who accepted to the contract layer's page for this pact.",
        "tags": [
          "pacts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "the pact to accept, as `pact.id` spells it",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "303": {
            "description": "The contract layer's page for the same pact id. The signature is the service's to record — its version, its evidence and its tier — so this route writes nothing and answers the address a person signs at."
          },
          "400": {
            "description": "The request could not be read",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing or does not open this account",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "This account may not do that",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/_/api/v1/clients": {
      "post": {
        "operationId": "postClients",
        "summary": "Store a client the agency pull found, or update the one already here.",
        "tags": [
          "ingest"
        ],
        "responses": {
          "200": {
            "description": "A client the ledger already held: its name, contract reference, milestone and `synced_at` are rewritten, and `created_at` stays the minute the first sighting wrote. `conversationId` is written only when the body names it (null detaches); absent leaves the attached conversation alone.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "client"
                  ],
                  "properties": {
                    "client": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A client this ledger did not hold. The agency's own id is the key, so the row a second pull sends lands on this one rather than beside it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "client"
                  ],
                  "properties": {
                    "client": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body names no id, no name, or no contract reference — the three `clients.store()` refuses rather than fills in.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key. The ingest is a machine call on the ledger's account; a browser session names a person, and a person is not a client's pull.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key's account does not hold this ledger's owner seat. The agency clause licenses this material to the account that authenticated, and a key that could write into another account is the redistribution D11 refuses.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/rooms": {
      "post": {
        "operationId": "postRooms",
        "summary": "Pour one pull's rooms: the client's half into chat, his own into the ledger. A re-post writes nothing.",
        "tags": [
          "ingest"
        ],
        "responses": {
          "200": {
            "description": "Every room the body named, with the conversation each one is about and the count written and skipped per half.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rooms"
                  ],
                  "properties": {
                    "rooms": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not one object with a `rooms` array, or a room or a line failed its own check: the refusal names the part (`room`, `ref`, `side`, `body`, `at`, `duplicate`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key. The pull is a machine call, and a browser session names a person.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key's account does not hold this ledger's owner seat, so the rooms of another ledger are not its to write.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Chat refused one room's client half (no key, a refused grant, too long, or no answer). The owner's half still landed, and the body carries every room's counts so a retry costs nothing.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/clients/{id}/messages": {
      "post": {
        "operationId": "postClientsByIdMessages",
        "summary": "Remember what a client said, with the day its words stop being served.",
        "tags": [
          "ingest"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "the client the words are about, as `client.id` spells it",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "The post's own retry handle. The unique index on (account_id, idempotency_key) is what makes a second post free: a repeat answers 200 with the first call's row and writes nothing. Absent, every post appends, which is the behaviour a caller with no key had before the column existed.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "That `Idempotency-Key` was already here: nothing was written, and the row is the one the first call answered with — a repeat key whose words moved is still not a second line.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The body as it was written: the minute it was said, and the minute its words stop being served.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "message"
                  ],
                  "properties": {
                    "message": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No client in this ledger holds that id, the body has no words, the day it names is outside `(the minute it was said, thirty days after it]`, or a body with no day of its own names no open item (`item`, `unknown-item`, `closed`) — each one the module's own refusal word.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key. A body is written for the ledger's account, and a cookie names a person.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key's account does not hold this ledger's owner seat, so there is no client list here for the words to hang on.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "The item the words name was confirmed by the other side, so there is no open delivery for them to live on.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/events": {
      "post": {
        "operationId": "postEvents",
        "summary": "Push one line of the local ledger's history, under the key that makes a second push free.",
        "tags": [
          "ingest"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "The push's own key, and the same fact as the body's `idempotencyKey`. The header wins when both are present. The unique index on (account_id, idempotency_key) is what makes the second push free.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "That key was already here: nothing was written, and the row keeps the fields the first push sent — a repeat key whose fields moved is still not a second line of history.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event"
                  ],
                  "properties": {
                    "event": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A line this ledger did not hold. The caller's `idempotencyKey` is the local row's own uid, so a second push of the same row lands here rather than beside it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event"
                  ],
                  "properties": {
                    "event": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A body this ingest refuses: no `idempotencyKey`, no kind, no subject, no artifact, an `at` that is not whole epoch milliseconds, a minute before 1970, or one more than five minutes ahead of this app's clock.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key. The push is a machine call on the ledger's account, and a cookie names a person rather than a collector.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key's account does not hold this ledger's owner seat, so there is no history here for the push to write into.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/upwork": {
      "post": {
        "operationId": "postUpwork",
        "summary": "Replace one ledger's Upwork snapshot with the document the mini read. `?account=` names the ledger; absent, the key's own. A ledger the key does not own needs the door's `can_write` for that ledger's channel, granted by its owner.",
        "tags": [
          "ingest"
        ],
        "parameters": [
          {
            "name": "account",
            "in": "query",
            "description": "The ledger the snapshot is written into, when it is not the key's own account. The door is asked for `can_write` on `crew-channel:<account>`; nothing is granted by this parameter.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ledger held a snapshot and this replaced it. `updated_at` is this post's minute — a re-push moves the page's stamp and nothing else — and nothing appends.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "channel"
                  ],
                  "properties": {
                    "channel": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The ledger held no snapshot and now holds this one: the body as the mini sent it, and the minute crew received it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "channel"
                  ],
                  "properties": {
                    "channel": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not one JSON object, or it carries no `proposals` array: the envelope is the one thing this route judges, and the content is the producer's.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key. The mirror is written by the machine on his desk, and a cookie names a person.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key's account does not hold this ledger's owner seat, and the ledger's owner has not granted it `can_write` on this ledger's channel. The door's `why` is in the sentence; nothing was written.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The body is over 256 KB. A proposal list past that is not the channel.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The ledger named by `?account=` could not be checked at the door (no `DOOR_INTERNAL_TOKEN`, or the door did not answer). Nothing was written.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/channel-events": {
      "post": {
        "operationId": "postChannelEvents",
        "summary": "Keep a page of the channel's events in this ledger, keyed by the channel's own id. A re-post of a page writes nothing and says so.",
        "tags": [
          "ingest"
        ],
        "parameters": [
          {
            "name": "account",
            "in": "query",
            "description": "The ledger the events are kept in, when it is not the key's own. The door is asked for `can_write` on `crew-channel:<account>`; nothing is granted by this parameter.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page was poured. `written` rows are new, `replayed` events were already here (a retry writes nothing), and `cursor` is the highest channel id in the page — the `since` to send next, or null for an empty page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "written",
                    "replayed",
                    "cursor"
                  ],
                  "properties": {
                    "written": {
                      "type": "integer"
                    },
                    "replayed": {
                      "type": "integer"
                    },
                    "cursor": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not one JSON object with an `events` array, or an event in it was refused: `event`, `id`, `kind`, `subject_ref`, `at` or `payload`. Nothing was written, in this ledger or any other.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No key. The mirror is written by the machine on his desk, and a cookie names a person.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The key's account does not hold this ledger's owner seat, and the ledger's owner has not granted it `can_write` on this ledger's channel. Nothing was written.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The body carries more than 200 events. That is the channel's own page size, so a larger body is a consumer that joined pages rather than one that read one.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The ledger named by `?account=` could not be checked at the door, or the store refused the write. The sentence names which event could not be written; a retry costs nothing.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key minted on the account's settings page at zpe.vc/home/settings. Send it as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem+json: `type`, `title`, `status`, `code` and `instance`, plus `detail` when a person should read a sentence.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "instance"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "bad-request",
              "unauthenticated",
              "malformed",
              "unknown",
              "mismatch",
              "revoked",
              "expired",
              "another-account",
              "scope-refused",
              "grant-refused",
              "forbidden",
              "not-found",
              "method-not-allowed",
              "conflict",
              "gone",
              "payload-too-large",
              "unsupported-media-type",
              "unprocessable",
              "rate-limited",
              "payment-required",
              "not-open",
              "internal",
              "unavailable"
            ]
          },
          "instance": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-surface": {
    "app": "zpe-crew",
    "base": "/_/api/v1",
    "document": "/_/api/v1/openapi.json",
    "generated_by": "zpe-kit machineDocument"
  },
  "x-zpe-routes": 8,
  "x-zpe-documented": 8,
  "x-zpe-internal": []
}
