{"openapi":"3.1.0","info":{"title":"Verigrant core-api","description":"The internal HTTP service behind Verigrant: users and sessions, the custodied profile, the scoped and revocable grants that delegate access to it, the verification layer that records which of a profile's claims somebody else has checked, a BYO-key vault, an agent that drafts from a profile using the profile owner's own key, and — on the other side of the market — the employer organizations a user may belong to, where belonging confers a rank over a roster and never access to anybody's record.","license":{"name":"MIT"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local development"}],"paths":{"/admin/audit":{"get":{"tags":["admin"],"summary":"`GET /admin/audit` — the custody trail, newest first.","description":"This is the same table `GET /me/grants/{id}/activity` and `GET /me/export`\nread, seen from the other side: those are scoped to one person by\nconstruction, and this is the operator's view across the service. It is the\nroute that makes the trail *usable* — a log nobody can query is a log that\nexists to be pointed at rather than read.\n\nWhat travels is the row as it was written, `detail` included, and that is\nsafe for the reason the writers are held to: nothing in this service ever\nputs a disclosed *value* in an audit entry. [`crate::regulated`] records that\na regulated field was shared and never what it said; the failed-login row\ncarries the address that was attempted and never the password; no handler\nanywhere writes key material into one. The trail is a record of acts, so the\nroute that reads it discloses acts.","operationId":"audit_log","parameters":[{"name":"user_id","in":"query","description":"Only entries about this account. Absent, every account — including the\nentries that name none.","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"action","in":"query","description":"Only entries with this exact action, e.g. `auth.login_failed`. Matched\nliterally rather than by prefix: an operator who wants a family of\nactions gets a page per action, which is the honest shape for a filter\nbacked by an index on the whole value.","required":false,"schema":{"type":"string"},"example":"auth.login_failed"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of the trail, newest first: at most `limit`, default 50, maximum 200","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}}}}},"400":{"description":"`user_id` is not a UUID, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/billing":{"get":{"tags":["admin"],"summary":"`GET /admin/billing` — per-organization usage, for whoever runs the\ndeployment.","description":"The operator's counterpart to `GET /orgs/{id}/billing`, and it stays inside\nthe line `/admin` has always held: it reports **organizations and counts**,\nand not one field of one person's record. An institution's usage is a fact\nabout a company, which is exactly the kind of fact an administrator is\nentitled to.\n\nOrdered by unbilled usage, descending, because the question an operator opens\nthis with is \"who owes the most and has not been invoiced?\". Organizations\nwith no billing row appear, on the free plan, because an operator deciding\nwho to move onto a paid plan needs to see the ones nobody has priced yet.","operationId":"admin_summary","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"Every organization with its plan, its status and what it has run up, heaviest unbilled usage first. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrgBillingSummary"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/jobs":{"get":{"tags":["admin"],"summary":"`GET /admin/jobs` — the queue, newest first.","description":"The question `SELECT * FROM job_queue WHERE status = 'failed'` has always\nbeen the answer to, asked this time through a guard and answered in a shape\nthat leaves the payload behind. `?status=failed` is what an operator opens\nafter `GET /admin/metrics` has told them there are failures; `?kind=` narrows\nto one sort of work, which is what a vendor outage looks like from here.\n\n**Newest first**, unlike [`crate::review::queue`] and like every other\ncollection in this service. The two are different in kind: a review queue is\nwork to be got through in order, and this is a log to be read — the failure\nsomebody is about to ask about is the one that just happened.\n\nPaged like every other collection, and deliberately with no index of its own.\n`job_queue_status_run_after_idx` serves the `?status=` filter, the sort is\nover a table the retention sweep keeps bounded, and an index on a queue table\nis paid for by every insert and every claim — which is the wrong trade for a\npage an operator opens when something is wrong.","operationId":"list_jobs","parameters":[{"name":"status","in":"query","description":"One of [`JOB_STATUSES`]. Absent, every status — which is the whole queue\nand its history rather than the thing an operator came here for.","required":false,"schema":{"type":"string"},"example":"failed"},{"name":"kind","in":"query","description":"One exact kind, e.g. `mail.send`. Matched literally rather than by\nprefix, for the reason `GET /admin/audit` matches an action literally:\nthe column is free text and a prefix filter would be a promise about a\nnaming convention the schema does not enforce.","required":false,"schema":{"type":"string"},"example":"mail.send"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of the queue, newest first: at most `limit`, default 50, maximum 200. Never a payload's contents — see `QueuedJob`.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/QueuedJob"}}}}},"400":{"description":"`status` is outside its set, `kind` is too long, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/jobs/{id}/cancel":{"post":{"tags":["admin"],"summary":"`POST /admin/jobs/{id}/cancel` — stop a job that should not run.","description":"The other half of what an operator needs and the one the schema had no room\nfor: a job whose payload names a row somebody has since erased, a sweep\nchain that has to be broken, a kind that was enqueued by a typo and will\notherwise sit `queued` forever. Until\n`20260905000012_job_cancelled_status.sql` the only way to say it was a\n`DELETE`, which destroys the evidence that anybody ever decided anything.\n\nFrom `queued` or `failed`. Not from `running` — that row belongs to a worker\nand cancelling it here would leave the handler running against a row that\nsays it is finished, which is exactly the two-writers problem `AND status =\n'running'` exists to prevent; the honest way to stop a running job is to let\nits deadline cut it off and cancel it after. Not from `done`, which already\nhappened, and not from `cancelled`, which already is: all three are a `409`.\n\n`last_error` is left exactly as it was. On a failed row it is why somebody\ncancelled, and clearing it would delete the reason at the moment of acting on\nit.","operationId":"cancel_job","parameters":[{"name":"id","in":"path","description":"The job's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The job, now `cancelled`. Terminal: no worker will claim it and nothing moves it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The job is `running`, `done` or already `cancelled`. A running job belongs to its worker; the other two are conclusions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/jobs/{id}/requeue":{"post":{"tags":["admin"],"summary":"`POST /admin/jobs/{id}/requeue` — give a failed job its whole budget back.","description":"**`attempts` is reset to zero, not left where it was**, and that follows from\nhow [`Worker::claim`] counts: it increments at claim time and\n[`Job::is_final_attempt`] compares the result with `max_attempts`. A failed\njob has normally spent all of them, so re-arming without resetting would hand\nit one claim that is final before it starts — any failure at all, however\ntransient, terminal on the spot, and an `attempts` above `max_attempts` in\nthe row afterwards. That is not what an operator means. They mean \"the vendor\nis back, try this properly\", and properly is what a job enqueued today would\nget: `max_attempts` tries with the backoff between them.\n\nThe spent attempts are not thereby denied. The trail says the job was\nrequeued and by whom, and this route is the only thing in the service that\nmoves a row out of `failed`.\n\n`run_after` becomes now, `last_error` is cleared — it describes an attempt\nthis row is no longer on — and the worker is woken the way [`enqueue`] wakes\nit. That last part is not decoration: the notify trigger is `AFTER INSERT`\nonly, deliberately, so an `UPDATE` that re-arms a row would otherwise wait\nfor the next poll. `pg_notify` on the request's own transaction inherits the\nproperty the trigger has — Postgres delivers it at commit and never on a\nrollback — so a requeue that is rolled back cannot wake a worker to look for\nit.\n\nOnly from `failed`. A `queued` row is already going to run, a `running` one\nbelongs to a worker, and `done` and `cancelled` are conclusions: all four are\na `409` rather than a silent no-op, because \"requeue\" is an instruction and a\nroute that answered `200` without following it would be lying to somebody\nwatching a stuck queue.","operationId":"requeue_job","parameters":[{"name":"id","in":"path","description":"The job's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The job, queued again with a fresh budget of attempts and no recorded error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedJob"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The job is not `failed`. Only a failed job can be put back on the queue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/metrics":{"get":{"tags":["admin"],"summary":"`GET /admin/metrics` — the counters, in one snapshot.","description":"The route that most needs the widening [`require_admin`] performs: nearly\nevery table counted here is under a row-level policy keyed on one user, so\nunder any other identity this statement would truthfully answer zero. It is\nthe honest shape of \"how big is the service\" and there is no way to ask it as\none person.","operationId":"metrics","responses":{"200":{"description":"The counters, taken in one statement so they agree with each other, and beside them two things that are reported rather than counted: what the hourly `documents.reconcile` sweep last found on disk, and what the machine's five-minute watchdog last found about the deployment around this process. `documents` is null in all three of its fields until that sweep has run once, and `watch` is null where nothing is watching at all.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Metrics"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/metrics/trends":{"get":{"tags":["admin"],"summary":"`GET /admin/metrics/trends` — the same service over the last thirty days.","description":"The counters at `GET /admin/metrics` say how big this deployment is and\ncannot say whether it is growing, which is the first question anybody opening\nan operator console has. This is that, and deliberately the smallest version\nof it: two numbers a day for a month, computed on read from rows that already\nexist.\n\nOne statement, and `generate_series` is what makes it dense — the calendar is\ngenerated and the counts are joined onto it, so a day with no signups is a\nrow saying zero rather than a row that is not there. Doing it the other way\nround and leaving the client to fill the gaps would be a chart that lies by\nomission on every quiet deployment, which is every deployment before launch.\n\nBehind [`AdminUser`] like the rest of this module, and it needs the widening\nas much as [`metrics`] does: `users` and `application` are both under\nrow-level policies keyed on one person, so under any other identity this\nstatement answers thirty rows of zeroes rather than refusing, which is the\nfailure mode a guard has to prevent rather than one an operator would notice.","operationId":"trends","responses":{"200":{"description":"Signups and applications per day for the last 30 days, oldest first, one row per day including the days on which nothing happened. Computed on read with `date_trunc` over columns the service already writes: there is no tracking behind it and no table of its own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Trends"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/orgs/{id}/billing":{"patch":{"tags":["admin"],"summary":"`PATCH /admin/orgs/{id}/billing` — put an institution on a plan.","description":"The one route in this service that decides what somebody pays, and therefore\nthe one that most needs a trail: it writes an `admin.billing.changed` entry\nnaming the organization and every field it moved, from and to. A price change\nnobody can reconstruct later is a price change that becomes an argument.\n\nAn upsert rather than an update, because an organization has no billing row\nuntil somebody prices it — see `20260905000006_billing.sql`. An id with no\norganization behind it is a `404` from the foreign key rather than a row\npointing at nothing.\n\n# Moving an institution onto a plan\n\nA plan is four numbers and not one word, so naming a *different* plan fills\nin that plan's [`list_price`] for whichever priced fields the request left\nout. An operator who moves an institution onto `platform` and says nothing\nelse gets 20000 a month, 500 a pull and 40 included, which is what `platform`\nmeans; one who names a figure beside the plan gets the figure they named,\nwhich is what a negotiated rate means. A request that does not change the\nplan touches no price it did not name, so a suspension cannot quietly reset a\nterm somebody agreed six months ago.\n\n`included_free_pulls` moves with the plan, which it did not when the\nallowance only ever refused things. It does now because the allowance is part\nof what a plan *costs*: the included pulls are deducted from every invoice\nuntil they are used up, so a `platform` institution left on ten would be\npaying for thirty pulls its plan includes. The number is still counted over\nthe institution's whole history rather than refilled — see [`free_pulls`] —\nso an institution moved up to `platform` after using five free ones has\nthirty-five left, not forty.\n\n# A paying account is a vetted account\n\nMoving an institution onto a paid plan is refused with a `409` unless its\nrelying-party registration says `registered` — that is, unless an\nadministrator has vetted it through `POST /admin/relying-parties/{id}/verify`.\nVetting is free and this route is the only place the two meet; see [`vetted`],\nwhich is where the direction of that dependency is argued.\n\nIt deliberately does **not** call the payment gateway. Creating a customer is\nthe invoicing sweep's business, on a job that can be retried, and a\nsynchronous route that reached a third-party service would turn an operator's\nprice change into something that fails when somebody else's API is down.","operationId":"admin_set_billing","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetBillingRequest"}}},"required":true},"responses":{"200":{"description":"The organization's billing as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Billing"}}}},"400":{"description":"`id` is not a UUID, the body named nothing, `plan` or `status` is outside its set, or one of `unit_price_cents`, `monthly_fee_cents`, `annual_fee_cents` and `included_free_pulls` is negative","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This organization is not a verified institution, so it cannot be moved onto a paying plan. Vetting is free; verify it at `POST /admin/relying-parties/{id}/verify` first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/orgs/{id}/billing/charge":{"post":{"tags":["admin"],"summary":"`POST /admin/orgs/{id}/billing/charge` — draw one institution's bill and\ncharge it, now.","description":"The `billing.invoice` sweep for a single organization, on demand: what it does\nper institution — draft the unbilled meter into an invoice, then charge the\ncard on file — is what this does for the one named in the path. It is for the\ntwo things a monthly pass is bad at. An operator settling an account today\nrather than in three weeks, and anybody who needs to see the charge path work\nend to end without billing every customer on the deployment to find out.\n\n# It reuses the sweep rather than repeating it\n\nBoth halves are the sweep's own functions. The draft is [`draw_one`], which is\nthe only place in this service that turns a meter into an amount: the free\ntier's ten included pulls and then nothing, `per_pull`'s ten and then the\nplan's price, `platform`'s monthly fee with forty included and a lower price\nafter them, and `contract`'s flat annual charged nowhere all live in that one\nstatement and in [`Billing::unit_price`] and [`Billing::monthly_fee`] beside\nit. The charge is [`present`], which posts the same PaymentIntent under the\nsame `Idempotency-Key: <invoice id>` — so an operator who presses this twice,\nor presses it the hour before a sweep, charges a card once.\n\nWhat this route deliberately does **not** do is chase this institution's\n*other* unpaid bills. That is the sweep's second pass, and it is a different\nquestion — \"what does anybody still owe?\" rather than \"what has this\ninstitution run up since we last drew?\" — so the answer here is about the one\ninvoice this run drafted and nothing else. An operator who wants last month's\nrefusal retried has the sweep for it.\n\n# What it answers\n\n* `402 card_required` — no card on file. First, before anything is drawn,\n  because the route's whole purpose is the charge: drafting a bill this\n  deployment has no way to present would leave a `draft` invoice behind as the\n  only trace of a request that failed. It is the same refusal\n  [`require_may_take_in`] gives before the first pull-through, so an\n  institution that reached this state got here without ever taking a candidate\n  in — or had its card removed at the gateway's end since.\n* `200` with `outcome: \"nothing_unbilled\"` — nothing is waiting to be billed.\n  A no-op said out loud rather than a `204` or a `409`: an operator pressing\n  this on an account that was swept an hour ago has done nothing wrong, and\n  \"there is nothing here\" is the answer, not an error.\n* `200` with the invoice and one of the four remaining outcomes otherwise.\n\nA gateway that refuses is a `200` carrying `not_charged` and not a `502`, and\nthat is the one decision here worth arguing. The invoice **exists** either\nway: it was drafted, the meter was stamped with it, and the rows are on it. An\nerror status would throw that away — the body of an [`AppError`] has nowhere\nto put an invoice — and leave the operator to go and find out by hand what\ntheir own request had just created. The refusal is in `detail` and on\n`invoice.last_error`, where the next pass will read it too.\n\nAudited as `admin.billing.charged` against the administrator who asked, on\nevery outcome including the no-ops, because \"who ran billing on this account,\nand when?\" is exactly the question that comes up afterwards. The trail entry\nnames the organization, the invoice, what it came to and how it went — and no\nfield of anybody's record, the line every `/admin` route holds. The charge\nitself writes its own entries with no user at all, through [`settle`] and\n[`record_failure`]: a gateway settling a charge is not something a person did.","operationId":"admin_charge_org","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"What the run did: the invoice it drafted, if it drafted one, and how the charge went. `outcome` is `nothing_unbilled`, `nothing_to_charge`, `presented`, `paid` or `not_charged` — including where the gateway refused, which is a recorded outcome rather than a failed request, because the invoice exists either way.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChargeRun"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"`card_required`: this organization has no card on file, so there is nothing to charge. Nothing was drawn.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`gateway_not_configured`: this deployment has no payment gateway, so a bill can be drawn but not presented. Nothing was drawn.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/relying-parties":{"get":{"tags":["admin"],"summary":"`GET /admin/relying-parties` — the institutions waiting to be vetted, oldest\nfirst.","description":"The same shape [`crate::review::queue`] has, for the same reasons, and the\narguments are worth restating rather than cross-referencing because this\nqueue decides something different:\n\n* **Oldest first, whatever the filter.** This is work rather than a feed, and\n  a queue served newest first is a queue whose tail is never served.\n* **`?status=pending` is the queue proper.** Without it the same route is the\n  history of what was decided, which is what an operator wants when they are\n  asking what happened rather than what is outstanding.\n* **Paged like every other collection**, and `relying_party_status_created_at_idx`\n  is exactly this statement's shape.\n\nWhat it discloses is an *institution*, which is a company: its trading name\nand handle, the register entry it claims, the domain it publishes at, and the\nmember of staff it has named as answerable. That is the one person in it, and\nnaming them is the point of the control. No field of anybody's career record\nis here, so the line `crate::admin` draws holds.","operationId":"review_queue","parameters":[{"name":"status","in":"query","description":"`pending`, `registered` or `suspended`. Absent, every status, which is\nthe review history rather than the queue.","required":false,"schema":{"type":"string"},"example":"pending"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of registrations, oldest first: at most `limit`, default 50, maximum 200","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Registration"}}}}},"400":{"description":"`status` is outside its set, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/relying-parties/{id}/reject":{"post":{"tags":["admin"],"summary":"`POST /admin/relying-parties/{id}/reject` — the administrator says no.","description":"`suspended`, which is the one value the gate treats as shut, and\n`rejected_at` beside it so that the institution's own screen can tell a\nrefusal from a withdrawal. A registration that was never verified keeps\n`registered_at IS NULL` for good, which is the durable difference between an\ninstitution that was never let in and one that was let in and later shut out\n— and it is why refusing an institution that is currently `registered`\nstamps only `suspended_at`.\n\nUnlike [`verify`] this **always writes and always audits**, including on a\nsecond refusal. The difference is that a reason is content: the second call\ncarries a sentence the first did not, so it is a second decision rather than\na retry.\n\nNothing is deleted. The keys stay, the log keeps its leaves, and the\nregistration keeps the fields it was refused over, because an institution\nthat fixes its filing and comes back should be looked at rather than made to\ntype it all again.","operationId":"reject","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectRegistrationRequest"}}},"required":true},"responses":{"200":{"description":"The registration, now `suspended`, carrying the reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Registration"}}}},"400":{"description":"`id` is not a UUID, or `reason` is missing, blank or too long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No organization here has registered as a relying party","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/relying-parties/{id}/verify":{"post":{"tags":["admin"],"summary":"`POST /admin/relying-parties/{id}/verify` — the administrator says this\ninstitution is who it says it is.","description":"The one act that opens the gate. Until it happens the registration is\n`pending`, [`live_registration`] refuses it, and the institution can do\neverything except read somebody's record.\n\n**There is no fee attached to this and no plan involved.** Verification is\nfree, and what it unlocks is the ability to be *charged*: `PATCH\n/admin/orgs/{id}/billing` refuses to move an unverified institution onto a\npaying plan. A paying account is a vetted account.\n\nIdempotent in the shape [`crate::review::approve`] is: verifying an already\nverified registration is a `200` that writes nothing and appends nothing to\nthe trail, so a retried request is not a second entry in a custody record.\nVerifying one that was refused is allowed and audited as the correction it\nis, for the reason that route gives: a record that cannot record a reversal\nkeeps saying something everybody involved has stopped believing.\n\n# The applicants who were waiting\n\nVerification is now the moment an institution becomes able to receive a\ndisclosure, which is a moment that used to belong to\n[`register_key`] alone. An employer may register, publish its keys and take\napplications while it is `pending`; each of those applications mints a grant\nnaming nobody, because [`has_live_sealing_key`] answers `false` for an\nunverified institution, and a grant naming nobody cannot be sealed under.\n\nSo this route calls [`crate::grants::bind_waiting_audiences`] on exactly the\npredicate [`register_key`] uses. Without it an institution that did\neverything in the natural order — register, publish keys, get verified —\nwould leave every applicant who arrived in between holding a delegation that\ncould never be sealed, and nothing in the service would ever come back for\nthem.","operationId":"verify","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRegistrationRequest"}}},"required":true},"responses":{"200":{"description":"The registration, now `registered`. The institution may read from the next request, and the delegations minted for it while it was unverified now name it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Registration"}}}},"400":{"description":"`id` is not a UUID, or `note` is too long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No organization here has registered as a relying party","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/users":{"get":{"tags":["admin"],"summary":"`GET /admin/users` — the account directory, searched and paged.","description":"Newest first, because the question an operator most often has about a list of\naccounts is about a recent one. Every filter is optional and they compose:\n`?q=example.com&status=suspended` is \"which of these people did we suspend\".\n\nThis is not [`crate::users::get_user`] with the guard changed. That route is\nthe *directory* — one row, a display name, no address — and it is what one\nordinary account may learn about another. This one carries the email address,\nthe status and the role, and it exists because an operator answering a\nsupport ticket has only the address to search by.","operationId":"list_users","parameters":[{"name":"q","in":"query","description":"A substring of the email address or the display name, matched\ncase-insensitively. Absent, every account.","required":false,"schema":{"type":"string"},"example":"ada@"},{"name":"status","in":"query","description":"`active` or `suspended`. Absent, both.","required":false,"schema":{"type":"string"},"example":"suspended"},{"name":"role","in":"query","description":"`user` or `admin`. Absent, both.","required":false,"schema":{"type":"string"},"example":"admin"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of accounts, newest first: at most `limit`, default 50, maximum 200","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminUserSummary"}}}}},"400":{"description":"`status` or `role` is outside its set, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/users/{id}":{"get":{"tags":["admin"],"summary":"`GET /admin/users/{id}` — one account, and the shape of what hangs off it.","description":"The counts are read in the same statement as the row, as scalar subqueries,\nso the answer is one round trip and one snapshot rather than eight reads that\ncould disagree with each other.","operationId":"get_user","parameters":[{"name":"id","in":"path","description":"The account's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The account and its counters. Never a field of the person's record — see the `admin` tag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDetail"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/users/{id}/reactivate":{"post":{"tags":["admin"],"summary":"`POST /admin/users/{id}/reactivate` — let a suspended account act again.","description":"The inverse of [`suspend`] in every respect but one: the sessions do not come\nback. They were revoked, revocation is a fact rather than a state, and the\nperson logs in again — which is the correct amount of ceremony for an account\nthat was suspended.\n\nReactivating an account that is already active is a `200` that writes and\naudits nothing, for the same reason as above.","operationId":"reactivate","parameters":[{"name":"id","in":"path","description":"The account's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The account, active again. It can log in; the sessions revoked by the suspension stay revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserSummary"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/users/{id}/suspend":{"post":{"tags":["admin"],"summary":"`POST /admin/users/{id}/suspend` — stop an account acting, at once.","description":"Two writes and two trails, on the request's one transaction:\n\n* `users.status` becomes `suspended`. That alone is already binding on the\n  very next request from that account, because [`crate::auth`] joins on\n  `status = 'active'` when it resolves a session and `POST /auth/login`\n  checks it before it will mint one.\n* **Every live session is revoked.** Belt and braces on the line above, and\n  not only that: without it, [`reactivate`] would silently hand back every\n  token the account held when it was suspended. A suspension is meant to end\n  the account's ability to act, and an ability that comes back on its own is\n  not ended.\n\nSuspending an account that is already suspended is a `200` that writes\nnothing and audits nothing — the same shape [`crate::pipeline::set_stage`]\ntakes, and for the same reason: a no-op that appends to a custody trail turns\na retried request into evidence of an act that did not happen.\n\n**An administrator may not suspend an administrator, themselves included.**\nThat is a `409`, and the argument is in this module's documentation: who\nadministers a deployment is declared by its environment and settled at boot,\nso removing one is an operator act — and a console that could do it is a\nconsole where one stolen admin session locks out everybody who could undo it.","operationId":"suspend","parameters":[{"name":"id","in":"path","description":"The account's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The account, now suspended. Its live sessions are revoked and it can no longer log in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserSummary"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The target is a platform administrator. Administrators are declared by the deployment's environment, not removed from a console.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/verifications":{"get":{"tags":["admin"],"summary":"`GET /admin/verifications` — the checks waiting to be decided, oldest first.","description":"**Oldest first, whatever the filter**, and that is the one ordering decision\nworth arguing about. Every other collection in this service is newest first,\nbecause every other collection is something a person is *reading*. This one\nis work: the check that has been waiting longest is the one somebody has been\nwaiting longest on, and a queue served newest-first is a queue whose tail\nnever gets served at all.\n\n`?status=pending` is the queue proper. Without it the same route is the\nreview history, which is what an operator wants when they are asking what was\ndecided rather than what is outstanding.\n\nPaged like every other collection — see [`crate::paging`] — and the index\n`verifications_status_created_at_idx` is exactly this statement's shape, so a\ndeep page is a range scan rather than a sort of the table.","operationId":"queue","parameters":[{"name":"status","in":"query","description":"`pending`, `verified`, `failed` or `expired`. Absent, every status —\nwhich is the review history rather than the queue.","required":false,"schema":{"type":"string"},"example":"pending"},{"name":"kind","in":"query","description":"`identity`, `employment`, `education` or `income`. Absent, every kind.","required":false,"schema":{"type":"string"},"example":"identity"},{"name":"user_id","in":"query","description":"Only the checks belonging to one account. Absent, every account.\n\nThe question behind it is a support ticket — \"this person says their\nidentity check has been sitting there for a week\" — and without it the\nonly way to answer is to page through the whole queue looking for a\n`user_id`. It takes the same id `GET /admin/users/{id}` does, which is\nwhere an operator gets it: nothing here searches for a person by name.","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of checks, oldest first: at most `limit`, default 50, maximum 200","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/QueuedVerification"}}}}},"400":{"description":"`status` or `kind` is outside its set, `user_id` is not a UUID, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/verifications/{id}/approve":{"post":{"tags":["admin"],"summary":"`POST /admin/verifications/{id}/approve` — the operator says the check\npassed.","description":"Four things happen, on the request's one transaction, and either all of them\nland or none of them do:\n\n* `status` becomes `verified` and `verified_at` is set by the same statement,\n  so the row can never carry a verification timestamp and a status that\n  disagrees with it — the invariant `POST /me/verifications/{id}/complete`\n  holds, held here too.\n* `reviewed_by` and `reviewed_at` record who decided and when, and\n  `review_reason` is cleared: a verified check carrying the sentence\n  explaining why it was once rejected would be a row that argues with itself.\n* A credential is minted from the row as it now stands, through\n  [`crate::attest::issue`].\n* Both trails are written and the subject is notified.\n\n**Approving an already-verified check is a `200` that writes nothing** — the\nsame idempotence [`crate::admin::suspend`] has, so that a retried request is\nnot a second entry in a custody trail. It still returns a credential, because\na credential is a statement of the standing fact rather than a one-time\nartifact, and answering a retry with a hole where the token was would send a\nclient looking for a bug.\n\n**Approving a check that was rejected is allowed**, and audited as the\ncorrection it is. [`crate::webhooks::apply_verification`] makes the same\nallowance for a vendor that changes its mind, on the same grounds: a record\nthat cannot record a reversal is a record that keeps saying something\neverybody involved has stopped believing.","operationId":"approve","parameters":[{"name":"id","in":"path","description":"The verification's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The check, now `verified`, and a freshly signed credential for it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovedVerification"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such verification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/admin/verifications/{id}/reject":{"post":{"tags":["admin"],"summary":"`POST /admin/verifications/{id}/reject` — the operator says the check did not\npass.","description":"`failed`, not `expired`: the two are distinct in this schema on purpose — a\ncheck that failed says something about the claim and one that expired says\nonly that it is old — and an operator looking at evidence and refusing it is\nmaking the first kind of statement. A sweep that retires stale checks is what\nwrites the second, and it is not this route.\n\n`verified_at` is cleared by the same statement, so a rejected row cannot keep\nthe timestamp from an approval it has since lost.\n\nUnlike [`approve`], this **always writes and always audits**, including on a\nsecond rejection of an already-failed check. The difference is that a reason\nis content: the second call carries a sentence the first one did not, so it\nis a second decision rather than a retry of the first.","operationId":"reject","parameters":[{"name":"id","in":"path","description":"The verification's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectVerificationRequest"}}},"required":true},"responses":{"200":{"description":"The check, now `failed`, carrying the reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueuedVerification"}}}},"400":{"description":"`id` is not a UUID, or `reason` is missing, blank or too long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The session is not a platform administrator's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such verification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/agent/grants":{"post":{"tags":["vera"],"summary":"`POST /agent/grants` — the auto-issuance.","description":"Section 4.4, step by step, with step 7 — the enforcement that is independent\nof anything the agent asserts — as the body of this handler. Everything the\nagent could lie about is counted here; everything it cannot lie about is\narithmetic it has already done in its own process.","operationId":"issue","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueRequest"}}},"required":true},"responses":{"201":{"description":"The grant, its token — shown once, to the agent that must deliver it — and the manifest of what was sealed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Issued"}}}},"400":{"description":"The bundle or the statement does not check out","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable Vera ID, a request signature that does not verify, or one that has already been presented — a signed agent request is good once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The capability is revoked, outside its window, out of issuances, or asked for more than its preset allows — each with a reason, and each written to the owner's trail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such pending application of this agent's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/agent/inbox":{"get":{"tags":["inbound"],"summary":"`GET /agent/inbox` — the envelopes of the account this agent acts for.","description":"Behind the same two credentials every other `/agent/*` route takes: the auto\nmode Vera ID, and an Ed25519 signature over this request under the key the\nowner's mandate authorises. Nothing about the subject comes from the request,\nwhich is the property the whole surface rests on: the account is the\ncapability row's, and there is no parameter that could name another.\n\nRefused where the capability's preset does not carry [`KEPT_SCOPE`]. An agent\nthat was never given the scope a kept item lives in could not open one, and\nserving it a list of what its owner has been sent would be a disclosure even\nthough it is a useless one. That is the argument\n[`crate::capability::records`] already makes about ciphertext, applied to the\none thing here that is not ciphertext.","operationId":"agent_inbox","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of envelopes, newest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentInboxItem"}}}}},"401":{"description":"No usable Vera ID, or a request signature that does not verify","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The capability is revoked, outside its window, or was not given the scope a kept item lives in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/agent/mcp":{"post":{"tags":["vera"],"summary":"`POST /agent/mcp` — one JSON-RPC message in, one response out.","description":"The body is taken as raw bytes rather than through a `Json` extractor for the\nreason [`crate::capability::issue`] takes its own that way: the signature\ncovers the exact bytes that arrived, and a body axum has already deserialised\nis a body this handler can no longer hash.","operationId":"handle","requestBody":{"content":{"application/json":{"schema":{}}},"required":true},"responses":{"200":{"description":"The JSON-RPC response, whatever the outcome of the call inside it","content":{"application/json":{"schema":{}}}},"202":{"description":"A notification, which gets no reply"},"400":{"description":"A body that is not JSON at all","content":{"application/json":{"schema":{}}}},"401":{"description":"No usable Vera ID, a request signature that does not verify, or one already presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The capability is revoked, outside its window, or lacks the scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/agent/mode":{"get":{"tags":["agent"],"summary":"`GET /agent/mode` — which provider arrangement this deployment runs.","description":"Unauthenticated, and that is the point of it. The Agent tab and the model-key\ncard both have to say something true about whose credential pays for a draft,\nand until this route existed they could only hedge: the web app's copy said\n\"where this deployment drafts with each user's own key…\" because no route\nreported the mode and guessing wrongly would have told somebody to go and buy\nan API key they did not need. It is a property of the deployment rather than\nof an account — the same answer for everyone, including a visitor with no\nsession yet — so requiring a credential to read it would only mean the first\nscreen after sign-up rendered before it knew what to say.\n\nNothing here is anybody's data and nothing here is a secret: the mode is\nalready visible in the `provider` field of every draft this service returns,\nand the cap is a published limit. What is deliberately *not* here is the\nplatform's key, the model, or any figure about a person.","operationId":"mode","responses":{"200":{"description":"The deployment's drafting mode and, in `hosted` mode, the per-account daily allowance. No credential is required: the answer is the same for every caller and the front end needs it before there is a session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModeResponse"}}}}}}},"/agent/pending":{"get":{"tags":["vera"],"summary":"`GET /agent/pending` — what this agent has applied for and may now issue\nagainst.","operationId":"pending","responses":{"200":{"description":"The unanswered applications this agent made.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentPending"}}}}},"401":{"description":"No usable Vera ID, a request signature that does not verify, or one that has already been presented — a signed agent request is good once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The capability is revoked or outside its window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/agent/provisional":{"post":{"tags":["vera"],"summary":"`POST /agent/provisional` — create an account for somebody who has not signed\nup, holding a record only they will ever open.","description":"# Two credentials, like every other write on this surface\n\nIt goes through [`crate::capability::authenticate_agent`], so it takes the\nauto mode Vera ID **and** an Ed25519 signature over the request under the key\nthe owner's mandate authorises. A leaked id alone creates nothing, and the\nsignature is spent once. An unauthenticated caller cannot reach this route at\nall: it is in the Vera group, where the credential is neither a session nor a\ngrant.\n\nIt is worth being plain about what the mandate does and does not say here.\nThe mandate the user signed is about *their* record, and this route is about\nsomebody else's — so what the capability is being used for is not \"may this\nagent disclose my history\" but \"is this a real agent somebody vouched for\".\nThat is the credential the founder handoff asks this to reuse, and the bound\nit buys is real rather than complete: every provisional account names the\ncapability that made it, the trail carries the act, revoking the capability\ntakes its unclaimed drafts with it by foreign key, and the link expires.\n\n# What the service checks, and what it cannot\n\nIt checks the shapes — the wrap is 48 bytes, the nonce 24, the digest 32, the\nverifier 32 before it is hashed — and that the address is not already\nregistered and the id not already in use. It cannot check that the wrap\nactually contains a DMK, or that the digest is the digest of the secret the\nagent is about to hand over, and it says so rather than implying otherwise:\nan agent that sends a digest of the wrong string produces a link that finds no\nrow, and the person discovers it. That is the same class of thing\n`PUT /me/keyring` cannot check about a re-wrap, and it has the same answer —\nthe client is where it is enforced, in `web::client::claim`, which derives all\nthree values from one secret in one function.","operationId":"create_provisional","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProvisionalRequest"}}},"required":true},"responses":{"201":{"description":"The account exists, unclaimed and holding nothing but a wrap this service cannot open. The answer carries the claim link's prefix and never the claim secret, which this service has not seen.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisionalCreated"}}}},"400":{"description":"A byte string that is not base64 or not the length the construction that produced it emits, an unusable address, or a window outside the bound","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable Vera ID, a request signature that does not verify, or one that has already been presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The capability is revoked or outside its window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The address is already registered, or the minted id is already in use — two different remedies, so two different messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/agent/provisional/{user_id}/records/{id}":{"put":{"tags":["vera"],"summary":"`PUT /agent/provisional/{user_id}/records/{id}` — seal a record into an\naccount whose owner has not arrived.","description":"The point of the whole feature: the person opens their claim link and finds a\nfinished record rather than an empty form. The agent holds the DMK it minted,\nso it seals exactly as any client seals, and what arrives here is the same\nfour opaque blobs and the same labels `PUT /me/records/{id}` takes.\n\n# Three checks before a byte is written\n\n* **The agent's two credentials**, through the same\n  [`crate::capability::authenticate_agent`] gate every other `/agent/*` route\n  runs, with the signature spent once.\n* **This agent's own account.** The claim slot names the capability that\n  created it, and a write from any other capability is a `404` — not a `403`,\n  because a refusal that distinguished \"not yours\" from \"no such account\"\n  would let one agent walk ids to discover which of them are other agents'\n  provisional accounts.\n* **Still unclaimed.** The instant the person claims it, this route stops\n  working, and it stops working by the same `404`: the claim deletes the slot.\n  An agent that could keep writing into a claimed account would be writing\n  into somebody's record without their key and without their knowing, which is\n  exactly what the claim is supposed to end.\n\nEverything after that is [`crate::encrypted::write_record`] unchanged, so the\nscope, the padding class, the ciphertext length and the write epoch are held\nto the same rules as an owner's own write. An agent writing for somebody who\nis not there to notice is the last caller that should be held to a weaker one.","operationId":"put_provisional_record","parameters":[{"name":"user_id","in":"path","description":"The provisional account's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"id","in":"path","description":"The record id, minted by the agent","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"If-Match","in":"header","description":"The record version being replaced. Absent means create.","required":false,"schema":{"type":["string","null"]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutRecordRequest"}}},"required":true},"responses":{"200":{"description":"Replaced. The row as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EncryptedRecord"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EncryptedRecord"}}}},"400":{"description":"An unknown scope, an unusable padding class, or a wrap or nonce of the wrong length","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable Vera ID, a request signature that does not verify, or one that has already been presented","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No unclaimed account of that id created by this capability — which is also the answer once the person has claimed it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The id is already in use, the version has moved, or `write_epoch` is not the scope's current epoch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/agent/records":{"get":{"tags":["vera"],"summary":"`GET /agent/records` — the ciphertext this agent's preset covers.","description":"Ciphertext and wrapped keys, exactly as the owner's own client receives them.\nThe wrap opens under the scope root key the agent was sealed at pairing and\nunder nothing else, so a scope outside the preset is not a permission this\nroute refuses — it is 48 bytes the agent cannot open. It is refused anyway,\nbecause serving ciphertext nobody asked to share is a disclosure even when it\nis a useless one.","operationId":"records","parameters":[{"name":"scope","in":"query","description":"A comma-separated list of scopes, all of which must be in the preset.\nAbsent means every scope the preset carries.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The ciphertext rows of the requested scopes.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentRecord"}}}}},"400":{"description":"A scope outside this capability's preset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable Vera ID, a request signature that does not verify, or one that has already been presented — a signed agent request is good once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The capability is revoked or outside its window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"agent_signature":[],"vera_id":[]}]}},"/auth/account-reset":{"post":{"tags":["auth"],"summary":"`POST /auth/account-reset` — mail the confirmation link, and say nothing\nabout the address.","description":"**`202` in every case**, on exactly the argument [`crate::auth::forgot`]\nmakes: this is a route a stranger can reach with any address they like, so\nany difference between an address that names an account and one that does\nnot is an account-existence oracle. An address that names nothing is\nanswered after the same work, and nothing is written and nothing is sent.\n\n**Nothing is scheduled by this route.** It mints a token and posts a letter.\nThe account is untouched until somebody opens that letter and confirms, which\nis what lets the message say, truthfully, that ignoring it is enough.\n\n**It does not care whether the account is encrypted**, and that is a change\nof shape from `POST /auth/forgot`, which sends one of two messages depending\non whether a keyring exists. Here the two would be the same message: an\naccount with no keyring whose owner has lost the password has\n`POST /auth/forgot` to go to and is told so by the copy, and one with a\nkeyring has this. Branching would buy nothing and would be a second place\nfor the two paths to drift.\n\n**Verification is deliberately not required**, for the reason `forgot` does\nnot require it: a person who cannot sign in is exactly the person who may\nnever have got round to confirming their address, and refusing them the one\nroute back would make an administrative preference into a permanent lockout\nof the address itself.","operationId":"request","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestResetRequest"}}},"required":true},"responses":{"202":{"description":"Taken. The same answer whether or not that address names an account, so this reply is not an oracle for whether it does. Nothing is scheduled and nothing is destroyed until the mailed link is opened and confirmed."},"400":{"description":"No email in the body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`mail_not_configured`: this deployment has no outbound mail, so there is nothing to send and saying `202` would be a lie","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/account-reset/confirm":{"post":{"tags":["auth"],"summary":"`POST /auth/account-reset/confirm` — spend the link, schedule the\ndestruction, and tell the account's own address.","description":"**Still nothing is destroyed.** What this writes is one `account_reset` row\nwith a `due_at` [`DELAY_HOURS`] out. Every session the account holds goes on\nworking, the record is untouched, and — this is the part that matters — the\nowner can sign in and cancel for the whole of the window.\n\n**A second confirmation does not move the deadline.** The insert is\n`ON CONFLICT DO NOTHING` against the partial unique index, so a caller who\nopens two links, or who is racing themselves across two tabs, gets the\ndeadline that is already standing rather than a fresh one. A window that\ncould be restarted would be a window that could be *shortened* by whoever\ncontrolled when it started, which is the same person this delay exists to\nslow down.\n\n**A refused confirmation does not burn the link.** Every refusal here is a\n4xx, `crate::db::unit_of_work` rolls a 4xx back, and the token is spent by a\nstatement inside that transaction — so somebody who mistypes the\nconfirmation opens the same link again rather than having to ask for a new\none. That is the same arrangement `crate::auth::reset` makes for the same\nreason.\n\n**Mail has to be working.** The notice to the account's own address is the\nentire protection for the person who did not ask, so a deployment that\ncannot send one has no business scheduling a destruction. It is checked\nfirst, before the token is looked at.","operationId":"confirm","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmResetRequest"}}},"required":true},"responses":{"200":{"description":"Scheduled. Nothing has been destroyed; the account is untouched until `due_at`, and anybody who can sign in may cancel until then.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledReset"}}}},"400":{"description":"`invalid_token` — unknown, expired or already spent, deliberately one answer for all three — or `confirmation_required`, meaning the typed confirmation was not the phrase the screen asked for. Nothing was written, and the link is still good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`reset_already_scheduled`: this account already has a destruction pending, and the deadline in the message is the one that stands. Asking twice does not bring it forward.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`mail_not_configured`: this deployment cannot send the notice that is the whole protection here, so it will not schedule anything","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/claim":{"get":{"tags":["auth"],"summary":"`GET /auth/claim` — the claim slot's parameters, to whoever can name the\ndigest of a claim secret.","description":"Unauthenticated, because the person following the link has no account and no\nsession; throttled with the rest of the unauthenticated surface, because it is\na read a stranger can reach and each one costs a lookup.\n\n**Serving the wrap to an unauthenticated caller is safe on the same argument\nthe recovery challenge rests on**, and it is worth restating rather than\nassuming: what is served is a master key sealed under a key derived from 256\nbits of uniform entropy that exist only in a link. There is no dictionary to\nrun against it. The caller had to name SHA-256 of that same secret to get this\nfar, so the only party this answers is one that already holds the thing the\nanswer is useless without.\n\n# One refusal, and it says nothing\n\nA digest that names no row, a link that has expired, and a link that has\nalready been claimed are **one `404`**. They have to be: the difference\nbetween the second and the third is whether somebody got there first, which is\na fact about a person, and the difference between the first and either is\nwhether a link ever existed. The screen says one sentence, and\n[`crate::claim`]'s own design makes the third case indistinguishable anyway —\na claimed slot is deleted rather than marked.","operationId":"claim_slot","parameters":[{"name":"claim_sha256","in":"query","description":"SHA-256 of the claim secret, base64. Standard base64 rather than\nURL-safe, because it is the same encoding every other byte string on this\nservice's boundary uses and a second spelling would be a second thing to\nget right; a client sends it URL-encoded like any other query value.","required":true,"schema":{"type":"string"},"example":"base64 of 32 bytes"}],"responses":{"200":{"description":"The claim slot: an account id and a wrap that opens under the secret in the link. Nothing about the person, and nothing about the record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimSlot"}}}},"400":{"description":"`claim_sha256` is not base64 of 32 bytes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live claim slot of that digest. Unknown, expired and already claimed are one answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this caller has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["auth"],"summary":"`POST /auth/claim` — set a password, take ownership, and destroy the claim\nslot.","description":"**The claim is the signup.** Everything a `POST /users` plus a\n`PUT /me/keyring` would have produced is produced here, out of a master key\nthat already exists: the password slot, a recovery key the person is shown\nonce, the published `box_pub` and `sign_pub`, and the `auth_verifier` that is\nfrom this moment the account's only credential. What is *not* produced is a\nnew master key, which is the entire point — the record the agent sealed opens\nafterwards, because it is the same DMK.\n\n# One transaction, and what is in it\n\nThe keyring, the move to `active`, the deletion of the claim slot, the\nsession and the trail. All of it or none of it. Every pair of those has a\nfailure mode worth refusing to have: a keyring with no state change is an\naccount that holds keys and cannot log in; a state change with no keyring is\nan account that can log in and can open nothing; a surviving claim slot is a\nsecond way into an account that now has an owner.\n\n# Why the slot is deleted rather than marked spent\n\nBecause \"single use\" should be a fact about the database and not about a\nhandler's `WHERE`. After this there is no row here that the agent's secret\nopens, so a link that leaks a year later is inert against the live system\nrather than merely refused. A second claim then gets the same `404` an unknown\nlink gets, which is also the least revealing answer. The fact of the claim is\nin `audit_log`, where the history of an account belongs.","operationId":"claim","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimRequest"}}},"required":true},"responses":{"201":{"description":"The account is the claimant's: an ordinary version 2 account with their password behind it. The answer carries a session and the account row, and the claim slot no longer exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Claimed"}}}},"400":{"description":"A byte string that is not base64 or not the length the construction that produced it emits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`claim_auth` did not match the stored verifier. Not distinguished from a link that has expired or been claimed, which are 404s, because the two say different things about what exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No live claim slot of that digest: unknown, expired and already claimed are one answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The account already holds a keyring, which a provisional account cannot — nothing was written","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled with the rest of the unauthenticated surface","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/forgot":{"post":{"tags":["auth"],"summary":"`POST /auth/forgot` — one answer, whatever the address names.","description":"**`202` in every case**, and that is the whole shape of this route. An\naddress that names nothing, one that names a version 1 account and one that\nnames a version 2 account are indistinguishable to the caller, because any\ndifference between them is an account existence oracle — and this is a route\na stranger can reach with any address they like.\n\nWhat differs is what is *sent*, and each of the three is right for the account\nit goes to:\n\n* **Nothing at all**, for an address with no active account.\n* **A reset link**, for a version 1 account: one that authenticates against\n  `users.password_hash` and has no keyring. Spending it at [`reset`] sets a\n  new hash.\n* **The recovery note** in [`crate::mail::encrypted_account`], for a version 2\n  account. Nothing is minted and nothing is reset, because there is nothing\n  here to reset: the password never reaches this service and the record is\n  sealed under a key derived from it in the browser. A link that set a server\n  side password would produce an account somebody could sign in to and a\n  record nobody could open, which is worse than no reset at all. The recovery\n  key is the way back, and the message says so and links to the screen that\n  takes one.\n\nVerification is deliberately *not* required. A person who cannot sign in is\nexactly the person who may never have got round to confirming their address,\nand refusing them the one route back would be this service making an\nadministrative preference into a lockout.","operationId":"forgot","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}},"required":true},"responses":{"202":{"description":"Taken. The same answer whether or not that address names an account, so this reply is not an oracle for whether it does."},"400":{"description":"No email in the body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`mail_not_configured`: this deployment has no outbound mail, so there is nothing to send and saying `202` would be a lie","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/kdf-params":{"post":{"tags":["auth"],"summary":"`POST /auth/kdf-params` — the salt and parameters a v2 login needs, signed,\nwith a deterministic decoy for an address that has no keyring.","description":"This is the route that makes a v2 account portable between browsers. Without\nit the only way to learn an account's KDF salt was `GET /me/keyring`, which\nneeds the session that the salt is needed to obtain.\n\n**It is not an oracle.** An address with no keyring — one that names nothing,\nand equally one that names a version 1 account — is answered with values\nderived by HMAC from a per-deployment secret and the address itself. The\nanswer is the same on every call, so probing twice learns nothing, and it is\nthe same shape as a real one, so comparing two answers learns nothing either.\nWhat follows is a login that fails on the credential, which is what a wrong\npassword does.\n\n**It is signed.** See the module documentation: an unsigned answer lets\nanything between the client and this service order a v2 client to send its\nraw password.\n\n**And it expires.** `expires_at` is [`KDF_PARAMS_TTL_MINUTES`] out and is\ninside the signature, so this answer is a statement about now rather than a\npermanent artifact. Without it, a signature captured once stays valid for\never and whatever can replay it can pin a client to parameters the account\nhas since moved off — which is the same downgrade the signature exists to\nstop, arriving by patience instead of by forgery.","operationId":"kdf_params","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}},"required":true},"responses":{"200":{"description":"The parameters to stretch with, signed by the transparency log's publication key. An address with no keyring is answered with a deterministic decoy of the same shape, so this reply says nothing about whether the account exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KdfParamsResponse"}}}},"400":{"description":"No email in the body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/login":{"post":{"tags":["auth"],"summary":"`POST /auth/login` — verify the password, mint a session, audit it.","description":"An unknown email, a wrong password and a non-active account are\nindistinguishable to the caller: same 401 body, and the same argon2 work is\ndone in every case so the three do not differ in timing either. The audit\ntrail records which it was; the response does not.\n\nTwo throttles stand in front of this route, and they answer different\nquestions. The per-address limiter in [`crate::ratelimit`] is layered outside\nthe handler and asks whether this *caller* is hammering, so a flood is\nrefused before any of the work below is reached. The per-account backoff is\nthe handler's own and asks whether this *account* is being guessed at, which\nis the question an address-keyed counter cannot answer once the guessing is\nspread over many addresses. The administrator signs in through this same\nroute, so the second question is worth asking.\n\n# The account backoff slows a guess and never blocks the owner\n\nIt used to block. Ten refused sign-ins for an address inside fifteen minutes\nand the route stopped answering that address at all — before the\nverification, so a throttled attempt cost nothing and recorded nothing.\n\nThat is a denial of service with a published recipe, and the recipe needs no\nresources: the counter is keyed on the email alone, so anyone who knows an\naddress can hold it at `429` forever by guessing one wrong password every\nninety seconds from wherever they like. The address of an administrator is\nnot a secret. Neither is anybody else's. The owner, holding the correct\npassword, was refused for exactly as long as a stranger cared to keep typing\n— and because a throttled attempt recorded no failure, the attack was cheap\nto sustain and invisible in the trail beyond the ten rows that started it.\n\nSo the policy is now: **the credential is always checked, and a correct one\nalways wins.** The failure count never stands between the account's owner and\ntheir own account. What it does instead is make a *wrong* credential slow:\npast [`ACCOUNT_THROTTLE_FAILURES`] the refusal is held for\n[`backoff`]'s span — doubling with the count, capped at\n[`ACCOUNT_BACKOFF_MAX`] — and then answered `429` with a `Retry-After`, and\nthe attempt is recorded whether it was slowed or not, so guessing raises its\nown price instead of resetting it.\n\nWhat that preserves and what it costs, said plainly. Preserved: guessing one\naccount is rate-limited, because a guess cannot be repeated on a connection\nuntil its refusal has been answered, and the per-address limiter still caps\nhow fast one address may ask. Cost: a distributed attacker holding many\nconnections open gets more guesses per hour than the hard block allowed. That\nis the right way round. A password on this service has been through\n`web::client::password::assess` and is not falling to a few thousand guesses;\nan administrator who cannot sign in during an incident is a real outage,\ncaused by a stranger, on demand.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"A new session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Refused. An unknown email, a wrong password and a disabled account are one answer on purpose — the response is not an oracle for which it was.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled, and `Retry-After` says for how long. Either `rate_limited`, meaning this address has spent its allowance, or `account_throttled`, meaning the credential was wrong *and* ten or more sign-ins for the email attempted have already been refused in the last fifteen minutes, so this refusal was held back before it was answered. A correct credential is never answered this way, however many failures precede it. Neither answer depends on whether that email names an account, so neither is more an oracle than the 401 is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/logout":{"post":{"tags":["auth"],"summary":"`POST /auth/logout` — revoke the session this request authenticated with.","description":"The guard on `revoked_at` makes a replayed logout a no-op rather than a way\nto backdate an existing revocation.","operationId":"logout","responses":{"204":{"description":"The session is revoked, effective on the next request"},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/auth/me":{"get":{"tags":["auth"],"summary":"`GET /auth/me` — the public fields of whoever holds the token.","operationId":"me","responses":{"200":{"description":"The session's own user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUser"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/auth/recover":{"post":{"tags":["auth"],"summary":"`POST /auth/recover` — a signature over the challenge, in exchange for an\nordinary session.","description":"The proof is Ed25519 under `user_keyring.sign_pub`. That key is derived from\nthe DMK, and the only way to reach the DMK without the password is the\nrecovery slot, so a valid signature is a proof of possession of the recovery\nkey — **without this service ever seeing it and without decrypting\nanything.** It works for every account enrolled since the keyring existed,\nbecause `sign_pub` has been on the row from the first day.\n\nEvery failure is the same `401`: a challenge that never existed, one that has\nexpired, one already spent, an address that does not match the challenge, an\naccount with no keyring, a malformed signature and a wrong one are one\nanswer, because any distinction between them is a distinction about whose\naccount it is.\n\nThe session this mints is an ordinary one in every respect but one: the\nchallenge row keeps its id, and `PUT /me/keyring` reads that row to learn\nthat this session proved the recovery key. That is what lets the person who\nhas just recovered actually change the password that lost them the account —\nsee [`crate::encrypted::put_keyring`].","operationId":"recover","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoverRequest"}}},"required":true},"responses":{"200":{"description":"A new session, exactly as `POST /auth/login` returns one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Refused. Every reason — unknown challenge, expired, already spent, wrong address, wrong signature — is this one answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/recovery-challenge":{"post":{"tags":["auth"],"summary":"`POST /auth/recovery-challenge` — 32 bytes to sign, and the wrap slot that\nmakes signing them possible.","description":"The first half of section 7's reset flow, and the half that had never been\nbuilt: the recovery slot has been stored on every v2 keyring since the\nkeyring existed, behind a session the person recovering cannot get.\n\nSame decoy discipline as [`kdf_params`], and it has to be: an address with no\nkeyring is answered with a slot derived from the per-deployment secret, so a\ncaller cannot tell an account that has never enrolled from one that has. The\nchallenge is real random bytes either way and the row is written either way,\nso the two answers cost the same and look the same. Nothing can be recovered\nwith a decoy: there is no `sign_pub` for [`recover`] to check against.","operationId":"recovery_challenge","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}},"required":true},"responses":{"200":{"description":"A challenge and the recovery wrap slot. An address with no keyring is answered with a deterministic decoy slot of the same shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoveryChallengeResponse"}}}},"400":{"description":"No email in the body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/resend-verification":{"post":{"tags":["auth"],"summary":"`POST /auth/resend-verification` — another link, at most one per ten minutes.","description":"Behind a session rather than taking an address, and that is the difference\nbetween this and an open relay: the address mailed is the one on the caller's\nown account, so nobody can point this service at a stranger's mailbox. The\nthrottle is the second half of the same argument, and [`RESEND_INTERVAL_MINUTES`]\nsays what it is measured against.\n\nAn account whose address is already proved is answered `409 already_verified`\nrather than sent a link it has no use for. The app hides the button in that\nstate, so this is the answer to a stale tab rather than to a normal one, and\nit is worth saying plainly instead of pretending a message went out.","operationId":"resend_verification","responses":{"202":{"description":"A new link is on its way"},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`already_verified`: this address has been confirmed, so there is nothing to send","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`verification_throttled`: one message per ten minutes, and the message says how long is left","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`mail_not_configured`: this deployment has no outbound mail, so nothing can be sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/auth/reset":{"post":{"tags":["auth"],"summary":"`POST /auth/reset` — spend the link, write a new hash, and end every session.","description":"Three things happen and they are one act, on the request's own transaction:\nthe token is spent, `users.password_hash` becomes a fresh argon2id hash of\nthe new password, and every live session this account holds is revoked.\n\n**The revocation is not housekeeping.** The ordinary reason somebody resets a\npassword is that they have lost it; the other reason is that somebody else\nhas it, and in that case the attacker is holding a session that would\notherwise outlive the reset by up to thirty days. A reset that left it\nstanding would be a reset that did not do the one thing the person came here\nfor. Sessions are rows, so the revocation takes effect on the very next\nrequest each of them makes, including the one the person is reading this on —\nthey sign in again with the password they have just chosen.\n\n**A version 2 account is refused by name**, and it is belt and braces:\n[`forgot`] never mints a reset token for one. If a keyring appeared between\nthe two calls — an account that enrolled in encryption while a reset link sat\nin its mailbox — writing a server side password would leave an account\nsomebody could sign in to and a record nobody could open.\n\n**Spending the link does not verify the address.** It would be a defensible\nreading of the same proof, and it is deliberately not taken: what is proved\nhere is possession of a mailbox at some point in the last day, and the route\nwhose job is to record that fact is [`verify_email`]. One fact, one route.","operationId":"reset","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetRequest"}}},"required":true},"responses":{"204":{"description":"The password is set and every session for this account is over"},"400":{"description":"The password is too short, or `invalid_token`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`encrypted_account`: this account has a keyring, so there is no server side password to set. The recovery key is the way back.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/totp/verify":{"post":{"tags":["auth"],"summary":"`POST /auth/totp/verify` — prove the factor for *this session*.","description":"The one call that stands between an administrator's session and the console.\nIt stamps `sessions.admin_verified_at`, which\n[`crate::admin::require_admin`] reads and refuses on when it is null or\nolder than twelve hours.\n\nUnder `/auth` rather than `/me`, deliberately: it is about the credential the\nrequest arrived on rather than about the account, and it is the only route in\nthis module whose effect is scoped to one session. The session is the one\nmaking the request and cannot be named in the body — a route that took a\nsession id would be a route for verifying somebody else's.\n\nA recovery code is accepted and is spent when it is: the array shrinks by\none, in the same transaction that writes the timestamp, so a captured\nrecovery code is worth exactly one verification even if it is replayed\nimmediately.\n\n`204`, because there is nothing to say. The caller already knows which\nsession it used, and the only observable outcome is that `/admin` now\nanswers.","operationId":"verify","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeRequest"}}},"required":true},"responses":{"204":{"description":"This session has proved the second factor, for twelve hours"},"400":{"description":"The code is neither six digits nor a recovery code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`totp_invalid`: the code does not match","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"This account has no enabled second factor, so there is nothing to verify. Enrol at `POST /me/totp/enrol` first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`totp_throttled`: five failed verifications in five minutes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/auth/verify-email":{"post":{"tags":["auth"],"summary":"`POST /auth/verify-email` — spend the link, and mark the address proved.","description":"Unauthenticated, and it has to be: the link arrives in a mail client and is\nopened in whatever browser that client hands it to, which is very often not\nthe browser the account was created in. The token is the whole credential,\nwhich is what 32 bytes of CSPRNG output, a day of life and a single use are\nfor.\n\nOne statement does both halves, so there is no window in which a token is\nspent and the account is not verified. `COALESCE` on the timestamp is\nbelt and braces over the single use: whatever happens, the moment recorded is\nthe *first* time this address was proved.","operationId":"verify_email","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailRequest"}}},"required":true},"responses":{"200":{"description":"The address is now confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifiedEmail"}}}},"400":{"description":"`invalid_token`: unknown, expired or already spent — deliberately one answer for all three","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this address has spent the unauthenticated allowance it shares with `POST /auth/login`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/credentials/pubkey":{"get":{"tags":["verifications"],"summary":"`GET /credentials/pubkey` — the platform's ed25519 public key.","description":"Unauthenticated, and not merely as a convenience: this is the key that makes\n[`verify`] optional. A party holding a credential can check it with any\ned25519 library against this key, on a machine that cannot reach us, and get\nan answer that does not depend on trusting the service that issued the\ntoken. An endpoint that would only tell you a signature was good if you asked\nit nicely would be no better than the claim it was checking.\n\nIt reads no row and holds no secret. The private half never leaves\n[`crate::grants::PlatformKey::sign_bytes`], and nothing on this route can\nreach it: the only thing published here is the half already written into\n`grants.public_key` on every grant this service has ever minted.\n\nOne key, so one object rather than a key set. `grants.public_key` is per row\nprecisely so that rotation is a value change rather than a schema change; the\nday this service holds two, this becomes a list and the field above becomes\nthe current one.","operationId":"pubkey","responses":{"200":{"description":"The platform's ed25519 verifying key, and what it verifies. Public, stable, and safe to cache.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformPublicKey"}}}}}}},"/credentials/verify":{"post":{"tags":["verifications"],"summary":"`POST /credentials/verify` — check a signed verification attestation.","description":"Unauthenticated, like `POST /grants/verify` and for the same reason: the\ncredential under examination is the entire input, the answer does not depend\non who is asking, and the third parties this exists for hold no account here.\n\nUnlike that endpoint, this one reads no row. There is nothing to read — the\nsignature and the clock are the whole of the verdict — which is exactly what\nmakes the same check possible offline, from the platform's public key, by\nsomebody who cannot reach this service at all.\n\n- 200 `valid: true` — signed by us and unexpired; read `status` for the claim\n- 401 `malformed` — not this format, or not `cred.v1`\n- 401 `invalid_signature` — tampered with, or never ours\n- 401 `expired` — genuine, and past the check's own expiry","operationId":"verify","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCredentialRequest"}}},"required":true},"responses":{"200":{"description":"Signed by the platform key and not expired. `valid` speaks for the signature; `status` speaks for the claim.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialVerified"}}}},"401":{"description":"`malformed`, `invalid_signature`, or `expired`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialRefused"}}}}}}},"/grants":{"get":{"tags":["grants"],"summary":"`GET /grants` — the caller's own grants, newest first. Also served at\n`GET /me/grants`, the name the rest of the owner-facing surface uses.","description":"Revoked and expired grants stay in the list: this is the view a user audits\ntheir delegations from, and a disappearing row would hide history. Each one\ncarries `status` — the same verdict [`authorize`] reaches, from the same\nfunction — along with `last_used_at` and `use_count`, so the list answers\n\"which of these is somebody actually using\" and not only \"which of these\nexist\".","operationId":"list","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The caller's grants, newest first. Revoked and expired ones stay in the list: this is the view a user audits their delegations from, and a disappearing row would hide history. Each carries `status` (`active`, `expired` or `revoked` — the same verdict `POST /grants/verify` reaches, from the same rule), `use_count` and `last_used_at`, which is `null` for a grant nobody has presented yet, the `audience` it was issued to where it names one, and `bundle_kinds` — which stages have actually been sealed under it, so an owner's client can tell a delegation the institution can open from one it cannot. No tokens and no sealed keys. One page: at most `limit`, default 50, maximum 200. Also served at `GET /me/grants`.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GrantSummary"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["grants"],"summary":"`POST /grants` — create a grant for the caller and mint its token. Also\nserved at `POST /me/grants`.","description":"The row and the audit entry share a transaction, so the custody trail cannot\nrecord a grant that does not exist or miss one that does. The token is\nminted from the committed row's id and handed back exactly once.\n\nThe new row's `use_count` is zero and its `last_used_at` is null: minting a\ndelegation is not using it.","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGrantRequest"}}},"required":true},"responses":{"201":{"description":"The grant, and its token. This is the only time the token exists on our side — store it now or create another grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGrantResponse"}}}},"400":{"description":"Empty, oversized or unusable `scopes`, or an `expires_at` that is not in the future","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/grants/verify":{"post":{"tags":["grants"],"summary":"`POST /grants/verify` — the unauthenticated endpoint third-party agents call.","description":"No bearer token is required, and none would help: the grant token *is* the\ncredential under examination, and the answer is the same whoever asks.\n\nThe signature is checked first, then the row. That second step is the point\nof the endpoint — a revoked grant still carries a perfectly good signature\nforever, so anything that trusted the token alone would never learn it had\nbeen withdrawn.\n\nAnswers are `{\"valid\": ...}` rather than the usual error envelope, and carry\na `reason` a caller can branch on:\n\n- 200 `valid: true` — signed, unexpired, present, not revoked\n- 401 `malformed` / `invalid_signature` — not a token we issued\n- 401 `expired` — past `exp`, or past the row's `expires_at`\n- 403 `revoked` — genuine and unexpired, but withdrawn by its owner\n- 401 `unknown_grant` — signed by us, but no such row (the user was deleted)\n- 503 `unavailable` — the row could not be read, so there is no verdict","operationId":"verify","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequest"}}},"required":true},"responses":{"200":{"description":"Signed by us, unexpired, present, and not revoked. **Not the same as redeemable**: `relying_party_required` is always `true` in this body, because reading a record also needs the credential of a registered institution, and `audience` names the one institution this delegation was issued to when it names one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantVerified"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired` or `unknown_grant`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantRefused"}}}},"403":{"description":"`revoked` — a genuine, unexpired grant its owner withdrew. A valid signature never stops being valid, which is why this endpoint reads the row as well.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantRefused"}}}},"503":{"description":"`unavailable`: the row could not be read. \"I don't know\" is never answered as \"yes\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantRefused"}}}}}}},"/grants/{id}":{"delete":{"tags":["grants"],"summary":"`DELETE /grants/{id}` — revoke a grant the caller owns. Also served at\n`DELETE /me/grants/{id}`.","description":"Idempotent by way of `COALESCE`: revoking twice returns 204 both times and\nkeeps the original timestamp, so a retry cannot backdate the revocation.\n\nThe row stays, with everything it recorded: revoking a delegation ends what\nit may do, not what it did. `GET /me/grants/{id}/activity` answers for a\nrevoked grant exactly as it does for a live one.\nA grant belonging to someone else is a 404, not a 403 — ownership is not\nsomething a stranger gets to probe for.","operationId":"revoke","parameters":[{"name":"id","in":"path","description":"The grant's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Revoked, effective on the holder's next request. Idempotent — revoking twice keeps the original timestamp."},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such grant, or it belongs to somebody else — ownership is not something a stranger gets to probe for","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/health":{"get":{"tags":["system"],"summary":"Readiness probe: HTTP 200 with the body `ok` once Postgres answers,\nHTTP 503 if it errors or does not answer within [`HEALTH_TIMEOUT`].","operationId":"health","responses":{"200":{"description":"Postgres answered","content":{"text/plain":{"schema":{"type":"string"},"example":"ok"}}},"503":{"description":"Postgres errored or did not answer in time","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/inbound/{user_id}":{"get":{"tags":["inbound"],"summary":"`GET /inbound/{user_id}` — where to send, and what to seal to.","description":"Behind the relying-party gate, like the push itself. A public version of this\nwould be a directory of every account's public key, queryable by anybody who\ncould guess an id, and the key is the thing a sender needs in order to be\nable to send at all. That gate is the one difference between this and\n`GET /rp/{slug}/keys`, whose shape it otherwise mirrors: an institution\npublishes its keys to the world because a stranger has to be able to check\nwhat it signed, and a person's key is served to a vetted correspondent\nbecause being able to seal to somebody is being able to learn they exist.\n\n**This is the route a sender polls.** Everything it answers is a fact about\nthe account that a sender needs *before* it does any work: the key, the\ngeneration of that key, a fingerprint it can log, the catalogue this\ndeployment tags, and the exact byte length it will accept. A sender that\ncaches the key should cache [`InboundAddress::key_epoch`] beside it and\nre-read this route when it is about to send, which costs one round trip and\nis the difference between a push that lands and a `409`.\n\nA blocked sender is refused here as well as at the push. It could learn the\nsame fact one request later by trying, and a sender that is told before it\nhas sealed a document is a sender that has not wasted the work.","operationId":"address","parameters":[{"name":"user_id","in":"path","description":"The Verigrant ID to resolve","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The key to seal to, which generation of it this is, and what this deployment accepts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundAddress"}}}},"403":{"description":"`relying_party_required` or `sender_blocked`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"`no_such_inbox`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"relying_party":[]}]},"post":{"tags":["inbound"],"summary":"`POST /inbound/{user_id}` — a vetted institution sends a sealed document to a\nperson's record.","description":"The order below is the order the checks have to happen in, and each step is\nload-bearing:\n\n1. **The gate.** [`relying_party::require_relying_party`] establishes the\n   sender, which is the same function and the same registration status the\n   read side requires. It leaves the transaction narrowed to the sender's\n   organization, which is what admits the `INSERT` at the end through the\n   `inbound_item_push` policy — see the migration for why that is a better\n   position than widening to the system for the write.\n2. **The envelope's shape**, which is pure and touches no row.\n3. **The recipient**, under a widening that is given back before any refusal\n   is rendered.\n4. **The signature**, rebuilt from the values checked above rather than from\n   anything the body asserts about itself.\n5. **The write**, then the owner's notice and the owner's trail.\n\nWhat this handler never does is look inside `payload`. It cannot: the key is\nderived from a password this service never receives, and `core-api` does not\nlink the crate that knows the construction. See [`crate::encrypted`].","operationId":"push","parameters":[{"name":"user_id","in":"path","description":"The Verigrant ID being pushed to","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushRequest"}}},"required":true},"responses":{"201":{"description":"Stored. The envelope is in the clear and the payload is not","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushReceipt"}}}},"400":{"description":"An unknown schema, a missing version, an unusable padding class, a payload that is not exactly `byte_class + 48` bytes, a `payload_hash` that is not SHA-256 of those bytes, a `sent_at` in the future, a signing key that is not registered and live, or a provenance signature that does not verify over this envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`relying_party_required` — the caller is not a registered, verified institution, which is the same gate reading requires — or `sender_blocked`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"`no_such_inbox`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`stale_recipient_key`: sealed to a key, or to a key generation, that is not the recipient's current one. The message names the generation the account is on. Resolve the address again and re-seal; this service holds no key that could re-seal it here, so a payload stored against a superseded generation would be one nobody could ever open","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"relying_party":[]}]}},"/me/access-requests":{"get":{"tags":["access"],"summary":"`GET /me/access-requests` — what has been asked of the caller.","description":"Pending first, then newest first: the question this route is opened to answer\nis \"what needs me\", the same reason `GET /me/notifications` puts unread\nfirst. Decided requests stay in the list because the record of having agreed\nis worth as much as the agreement.","operationId":"list_mine","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of the requests made of the caller: pending first, then newest first. At most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccessRequest"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/access-requests/{id}/approve":{"post":{"tags":["access"],"summary":"`POST /me/access-requests/{id}/approve` — say yes, and mint the delegation.","description":"One transaction: the grant, the decision on the request, the application\nre-pointed at the new delegation, the superseded one revoked, the notice back\nto the recruiter who asked, and the caller's own audit entry. Either all of\nit happened or none did — an approval that minted a grant nothing pointed at\nwould be access the employer could not use and the seeker could not find.\n\nApproving twice is a `404`, not a second grant: the second call finds no\npending request, which is the same answer an id that never existed gets.\n\n# And then the client seals\n\nThe grant is a permission, and under [`crate::encrypted`] the records it\npermits are ciphertext this service cannot open — so approving is half a\ndisclosure until the seeker's own client seals the record keys to the\ninstitution at `POST /me/grants/{id}/bundle`. This response carries what that\ntakes: the grant, the institution's slug, the scopes, and whether the\ninstitution can receive a seal at all. See [`Delegated`], and note that the\nsealing happens in the client and nowhere else — there is no route here that\nwould take a record key, which is the property the whole architecture rests\non.\n\nWhere the institution is a registered relying party with a live X25519 key,\nthe minted grant **names it**, so the bundle can be sealed to it and\n`GET /orgs/{id}/applications/{application_id}/records` serves it back to\nthem. Where it is not, the grant is minted unbound exactly as before and\n`relying_party_registered` is `false`.","operationId":"approve","parameters":[{"name":"id","in":"path","description":"The access request's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The request, now `approved` and naming the grant it minted, with `delegated` saying what to seal and to whom. The grant is an ordinary one: it is in `GET /me/grants` and `DELETE /me/grants/{id}` withdraws it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovedRequest"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such pending request of the caller's — including one already decided, and one belonging to somebody else","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/access-requests/{id}/decline":{"post":{"tags":["access"],"summary":"`POST /me/access-requests/{id}/decline` — say no.","description":"Nothing is minted and nothing is explained: the request is closed, the\ncompany is told the answer and not the reason, and the seeker owes neither.\nThe company may ask again later — the unique index is partial on `pending` —\nwhich is the honest consequence of not having an \"and never ask again\" that\nthis service could enforce against a determined asker anyway.","operationId":"decline","parameters":[{"name":"id","in":"path","description":"The access request's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The request, now `declined`. Nothing was delegated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequest"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such pending request of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/account":{"delete":{"tags":["portability"],"summary":"`DELETE /me/account` — destroy the account and everything held under it.","description":"Irreversible, and there is no soft-delete flag hiding behind it: the rows are\ngone, and so are the sessions and grants that reached them.\n\nThe order matters and is worth stating:\n\n1. **The credential is verified first**, outside any transaction, because\n   argon2 is deliberately slow and a lock held across it would be a lock held\n   for a tenth of a second. Which credential is checked is decided by which\n   one arrived, and against which stored hash by whether the account has a\n   keyring — the same rule, and the same [`auth::verify_secret`], as\n   `POST /auth/login`. A failed confirmation is recorded in the trail — an\n   attempt to destroy somebody's record is exactly the event a trail is for —\n   and answered `403`, not `401`: the session was perfectly good, the\n   confirmation was not.\n2. **The census is taken inside the transaction**, before anything is\n   deleted. It is the receipt, and it is taken under the same lock as the\n   delete so it cannot describe a state that never quite existed. The lock\n   is taken `NOWAIT`, so a second erasure arriving while this one is running\n   is refused with a `409` on the spot instead of queueing behind it — see\n   the comment on the statement for the deadlock that queueing produces.\n3. **The trail is deleted explicitly**, because `audit_log` does not cascade\n   — and neither would it reach the failed-login rows that carry this\n   address with no user id.\n4. **The user row goes**, and every table in the inventory with it, by\n   `ON DELETE CASCADE`.\n5. **The census is taken again**, and every count must now be zero. If one is\n   not, the transaction is rolled back and the caller gets a `500` with their\n   account intact. A cascade that silently stopped short is the one failure\n   this route must never report as success, and checking is three\n   milliseconds of counting on an operation that happens once in an account's\n   life.\n6. **The retained record is written**, then the transaction commits.\n7. **The document files are removed** from disk, after the commit. Postgres\n   cannot cascade to a filesystem; a file that outlives its row is an orphan\n   the operator hears about, and is not a reason to tell somebody their\n   erasure failed after it has committed.\n\nSteps 2 to 6 are [`destroy`], which is shared with the one other thing that\ndestroys an account: [`crate::reset`]'s sweep, three days after somebody who\nhad lost both their password and their recovery key confirmed by mail that\nthey wanted to start over. What this route holds on to is the part that is\nits own — the credential, the receipt, and the `owner_request` reason on the\nretained record.","operationId":"erase","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EraseAccountRequest"}}},"required":true},"responses":{"200":{"description":"The account and everything held under it are gone, along with every session and grant that reached them. The receipt says how many rows went from each table; a retained record of the erasure — carrying no name and no address — is kept in `erasure_log`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErasureReceipt"}}}},"400":{"description":"Neither `password` nor `auth_key` in the body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The credential did not match — including a `password` sent for an encrypted account, or an `auth_key` sent for one that has no keyring. The session was fine; this is the confirmation failing, and it is recorded in the trail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`erasure_in_progress`: another request is already erasing this account, or writing to it. Nothing was destroyed by this one, and the account is either gone or about to be — a second attempt will say which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/account-reset":{"get":{"tags":["portability"],"summary":"`GET /me/account-reset` — whether a destruction is pending for the caller.","description":"`null` rather than a 404 for the ordinary case, matching `GET /me/keyring`\nand the regulated islands: \"there is nothing scheduled\" is an answer, not a\nmissing resource, and it is the answer almost every call gets.\n\nThis is the route that makes the delay visible inside the product rather\nthan only in a mailbox. An owner who never reads the notice still sees the\nbanner the moment they open the application, which is the other half of\n\"email notice at both ends\".","operationId":"pending","responses":{"200":{"description":"The pending destruction, or `null` if none is scheduled","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PendingReset"}]}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/account-reset/cancel":{"post":{"tags":["portability"],"summary":"`POST /me/account-reset/cancel` — stop it.","description":"**A session is the proof, and it is the right proof.** See this module's\ndocumentation at length: whoever asked for the reset proved control of the\nmailbox, so the mailbox cannot be what cancels it. A session is reachable\ntwo ways on an encrypted account — the password through\n`POST /auth/login`'s `auth_key` arm, and the recovery key through\n`POST /auth/recover` — which is exactly the set of people the delay is\nprotecting.\n\nWhich of the two it was is recorded in the trail rather than merely\naccepted, because \"somebody tried to destroy my account and I stopped it\nwith my recovery key\" is a different event, to the person reading their own\nhistory afterwards, from stopping it with a password they still had.\n\nThe `UPDATE` is guarded on `cancelled_at IS NULL`, so a second cancellation\nis a `404` rather than a way to backdate the first — the same guard\n`POST /auth/logout` puts on a revocation and for the same reason.","operationId":"cancel","responses":{"204":{"description":"Cancelled. Nothing was destroyed and the account is exactly as it was."},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"`no_reset_pending`: nothing is scheduled for this account, or it has already been cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/addresses":{"post":{"tags":["contact"],"summary":"`POST /me/addresses` — record an address.","operationId":"create_address","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAddressRequest"}}},"required":true},"responses":{"201":{"description":"The address was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressEntry"}}}},"400":{"description":"An unrecognised type, an oversized field, or a `to_date` before its `from_date`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/addresses/{id}":{"delete":{"tags":["contact"],"summary":"`DELETE /me/addresses/{id}`.","operationId":"delete_address","parameters":[{"name":"id","in":"path","description":"The address's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The address is gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["contact"],"summary":"`PATCH /me/addresses/{id}` — change one of the caller's addresses.","operationId":"patch_address","parameters":[{"name":"id","in":"path","description":"The address's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAddressRequest"}}},"required":true},"responses":{"200":{"description":"The address, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/agent/draft-application":{"post":{"tags":["agent"],"summary":"`POST /me/agent/draft-application` — draft an application from the caller's\nown profile.","description":"Session-authenticated and self-only, like everything under `/me`: the\nprofile drafted from, and the key spent in live mode, both belong to the\nsession's user, and neither is addressable from the request.\n\n- 400 — a missing or empty `job_title`, `company` or `job_description`\n- 401 — no session\n- 409 `no_llm_key` — live mode, and the caller has stored no key\n- 429 `draft_quota` — hosted mode, and this account's daily allowance is\n  spent. The message names the moment it comes back, and\n  `GET /me/agent/quota` answers the same instant as a timestamp.\n- 502 `provider_*` — the provider refused, failed, or answered with nonsense\n- 200 — `{\"draft\", \"provider\", \"model\"?}`","operationId":"draft_application","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftRequest"}}},"required":true},"responses":{"200":{"description":"The draft, and what produced it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftResponse"}}}},"400":{"description":"A missing, empty or oversized `job_title`, `company` or `job_description`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`no_llm_key`: this process is in live mode and the caller has stored no key. Deliberately not a silent downgrade to the mock — a user who asked for a real draft would otherwise get a canned one and no sign that their key was never used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`draft_quota`: this process is in hosted mode — the platform pays for the model — and the caller has spent today's allowance. The message names the instant it resets, and `GET /me/agent/quota` answers the same instant as a timestamp beside the count. Storing an API key of one's own is not metered at all.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`provider_*`: the provider refused, failed, or answered with nonsense","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/agent/quota":{"get":{"tags":["agent"],"summary":"`GET /me/agent/quota` — today's hosted use, and the allowance.","description":"Session-authenticated and self-only like everything under `/me`: the figures\nare the session user's, and there is no user id in the request to aim it\nsomewhere else.\n\nAnswered in every mode, not only in `hosted`. A client should be able to draw\nthis without first asking which mode the deployment is in, and the honest\nanswer where nothing is metered is a `daily_cap` of `null` beside a count of\nwhatever this account has actually used — which is zero on a deployment that\nhas never been hosted and is deliberately *not* zeroed on one that has, since\nthe rows are a record of platform spending and not a display.","operationId":"quota","responses":{"200":{"description":"Today's hosted drafts, the tokens they consumed, the daily allowance and the instant it resets. `daily_cap` and `remaining` are `null` on a deployment that meters nothing — every mode but `hosted`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quota"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/agent/run":{"post":{"tags":["agent"],"summary":"`POST /me/agent/run` — ask the agent to work the caller's top matches.","description":"Takes no body, for the reason `POST /postings/{id}/apply` takes none:\neverything a run needs is already on file. Which vacancies to work is the\nmatching engine's answer, what to say is the profile's, and what the screening\nquestions resolve to is the record's and the bank's.\n\nEnqueues rather than runs. The work is several provider calls and a read of\nthe whole record, which is exactly the shape of thing this service puts on the\nqueue — and a second press while the first run is still in flight enqueues\nnothing, because two runs would draft the same three applications twice and,\nin live mode, charge for both.","operationId":"start","requestBody":{"description":"Optional. A request with no body is a run that drafts and submits nothing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartRunRequest"}}},"required":true},"responses":{"202":{"description":"The run is queued. `already_queued` is `true` when the caller already had one in flight and this request enqueued nothing, and `submit_grant_id` names the delegation an authorised run may submit under. What the run produces arrives at `GET /me/agent/suggestions`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunAccepted"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/agent/suggestions":{"get":{"tags":["agent"],"summary":"`GET /me/agent/suggestions` — what the agent has drafted for the caller.","description":"Newest first. Only suggestions about open postings appear: the join says so\nand `posting_public_read` says it again to Postgres, so a withdrawn vacancy\ndrops out of the list rather than sitting in it as a proposal nobody can act\non.","operationId":"list","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The caller's suggestions, newest first. Each carries the draft, the screening questions the engine answered and where each answer came from, and the gaps it could not — which are the only questions worth putting to a person. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Suggestion"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/agent/suggestions/{id}/answers":{"post":{"tags":["agent"],"summary":"`POST /me/agent/suggestions/{id}/answers` — answer this suggestion's gaps,\nand teach the service.","description":"Two effects, and the second is the one that compounds. The answers are stored\nin the answer bank through [`crate::answers::store_answer`] — the same write\n`PUT /me/answers` performs, under the same fingerprint — so the next posting\nanywhere that asks the same question resolves it without asking again. And\nthis suggestion is then re-resolved against the posting's questions and\nrewritten, so the answer the user just gave appears in `filled_answers` in the\nresponse to the request that gave it.\n\nAnswering a question the suggestion did not ask is not an error. The bank is\nthe user's, a question is a question, and refusing to learn something because\nit was volunteered a moment early would be a strange thing for this endpoint\nto do. What the response reports is this posting's questions, resolved afresh.","operationId":"answer_gaps","parameters":[{"name":"id","in":"path","description":"The suggestion's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FillGapsRequest"}}},"required":true},"responses":{"200":{"description":"The suggestion, re-resolved. Every answer sent is now in the answer bank, so the next posting to ask the same question resolves it from there — and `gaps` here is what is still genuinely unknown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Suggestion"}}}},"400":{"description":"No answers, more than the batch limit, or an answer the bank refuses: an empty question, no value or several, a value that does not match the type the question is on file as, or a document that is not the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such suggestion of the caller's — or the posting it is about is no longer open","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/agent/suggestions/{id}/submit":{"post":{"tags":["agent"],"summary":"`POST /me/agent/suggestions/{id}/submit` — send what the agent drafted.","description":"The suggestion's own draft and its own answers, submitted as they stand:\n**what is sent is what the person read**. Re-resolving the screening\nquestions here would be defensible and is deliberately not done — the whole\npoint of showing somebody a suggestion is that they can agree to *that*, and\nan application that quietly answered differently from the preview would make\nthe preview worthless.\n\nGaps travel with it. A question the service could not answer is submitted\nunanswered, exactly as `POST /postings/{id}/apply` submits it, because what\nis stored on an application is what was sent. A client that would rather fill\nthem in first has `POST /me/agent/suggestions/{id}/answers` and can call this\nafterwards; the service does not refuse on the user's behalf.\n\nSubmitting twice is a `409`, from the suggestion's own status and from the\n`application` unique index underneath it — the same refusal, arriving from\ntwo places, for the same reason a second `POST /postings/{id}/apply` is\nrefused.","operationId":"submit_suggestion","parameters":[{"name":"id","in":"path","description":"The suggestion's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitSuggestionRequest"}}},"required":true},"responses":{"201":{"description":"The application, and the delegation it issued to the employer. The suggestion is now `submitted` and names the application it became.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationSubmitted"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`submission_not_authorized`: no active grant carrying `applications:submit`, and the request did not ask for one. Nothing was written and the suggestion is still there to submit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such suggestion of the caller's, or the posting it is about is no longer open","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This suggestion has already been submitted, or the caller has applied to the posting by another route","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/answers":{"get":{"tags":["answers"],"summary":"`GET /me/answers` — everything the caller has answered, most recently\nwritten first.","operationId":"list_answers","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The answers the caller has stored, each with the question it answers and the fingerprint it is filed under. Most recently written first. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StoredAnswer"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["answers"],"summary":"`PUT /me/answers` — store or replace the caller's answer to a question.","description":"Upserts twice: the question by fingerprint, then the answer by\n`(user_id, question_id)`. Answering the same question again replaces the\nstored answer rather than appending — the bank holds what the user says\n*now*, and a history of what they used to say is a different feature that no\nform asks for.","operationId":"put_answer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreAnswerRequest"}}},"required":true},"responses":{"200":{"description":"The stored answer, with the question and the fingerprint it was filed under. Answering the same question twice replaces the answer and returns the same `id`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredAnswer"}}}},"400":{"description":"The question was empty or has nothing to identify it by; a `value_bool`, `value_text`, `value_num`, `value_date` or `value_multi` was sent, which is a field value and is not stored here; `answer_type` is missing or disagrees with the type this question is already recorded as; `record_id` does not name one of the caller's own live records in scope `answers:read` of type `screening_answer`; or `value_doc_id` is not one of the caller's documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/answers/lookup":{"get":{"tags":["answers"],"summary":"`GET /me/answers/lookup?question=…` — the reuse path.","description":"Hand it the question a form is asking. If this user has answered the same\nquestion before — at another employer, through another ATS, spelled with\ndifferent punctuation — the stored answer comes back and the form can be\nfilled without asking again. If they have not, it is a `404`: a miss is the\nordinary case in a form-filling loop, not an error in the request.\n\nThe read has one side effect, and it is deliberate: a hit stamps\n`last_used_at`. That column exists to answer \"which of these answers am I\nactually relying on\", and the only moment that fact is known is the moment\nan answer is handed to a form.","operationId":"lookup_answer","parameters":[{"name":"question","in":"query","description":"The question text as the form asks it. Matched by fingerprint, so capitalisation, punctuation and spacing do not have to agree.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The stored answer. `last_used_at` has been stamped with the time of this call.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredAnswer"}}}},"400":{"description":"No `question` parameter, or one with nothing to identify it by","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The caller has not answered this question. The ordinary miss in a form-filling loop: ask the user, then `PUT /me/answers`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/answers/{id}":{"delete":{"tags":["answers"],"summary":"`DELETE /me/answers/{id}` — forget one stored answer.","description":"The question row stays: it is shared, holds no personal data, and may be\nanother user's. What is forgotten is this user's answer to it.\n\n**For a sealed answer that means two rows and it means the ciphertext, not a\nflag.** The bank row goes and the record it named is tombstoned in the same\ntransaction — its ciphertext and its wrapped key nulled — so a delegate\nholding the key to that version holds the key to nothing. An answer deleted\nthrough this route leaves no bytes for anybody to open, which is what makes\nit a deletion rather than a hidden row.","operationId":"delete_answer","parameters":[{"name":"id","in":"path","description":"The answer's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The answer is gone, and so is the ciphertext of the record that held it."},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/application-requests":{"get":{"tags":["vera"],"summary":"`GET /me/application-requests` — what agents have applied for in your name.","description":"Pending first, then newest first: the question this list is opened to answer\nis \"what needs me\". Decided rows stay, because the record of having agreed is\nworth as much as the agreement.","operationId":"list_requests","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of applications made in the caller's name: pending first, then newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationRequest"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/application-requests/{id}/approve":{"post":{"tags":["vera"],"summary":"`POST /me/application-requests/{id}/approve` — record that this application\nwas answered, and with which delegation.","description":"**This route does not create the disclosure.** It cannot: a disclosure is a\nbundle of record keys sealed to the institution, and the keys that seal it\nexist only in the user's browser. So the client does the work — mint the\ngrant with `POST /me/grants`, verify the institution's key against the\ninstitution's own domain, seal with `POST /me/grants/{id}/bundle` — and this\nroute is the last step, linking the answer to the question and telling the\nuser's own trail which application it was for.\n\nThe order matters and is enforced here: the grant must be the caller's, must\nname this institution as its audience, and must already carry a bundle. An\napproval recorded against a grant nobody sealed under would be a queue entry\nsaying \"shared\" about a disclosure that never happened.\n\n## Binding the approval to the disclosure\n\nThose three checks say the grant is the right *account's* and the right\n*institution's*. Three more say it is the answer to this *application*, and\nwithout them the queue would record an answer that does not match the\nquestion it is filed under:\n\n* **The stage.** A bundle is `preview` or `full`, and the two are ordered\n  rather than parallel. An application for a preview may be answered with a\n  preview and nothing more; an application for the full ingest may be\n  answered with either, because disclosing less than was asked for is the\n  candidate's to decide. That is exactly\n  [`crate::capability::kind_within`]'s rule, and it is the same function\n  here so the human path and the agent path cannot drift into disagreeing\n  about which direction is the safe one.\n* **The scopes.** The grant has to carry every scope the application asked\n  for. A grant carrying fewer answers a different question, and recording it\n  as the answer would tell the user their agent's request was met.\n* **Once.** A grant already named by another `application_request` cannot be\n  presented again. One delegation answering two applications would show two\n  institutions as answered by one disclosure that named one audience, and the\n  second of them was never sealed to.\n\nNone of these is a check about the *token*: the grant is real and usable\neither way. They are checks about whether this row may say it was answered by\nit.","operationId":"approve","parameters":[{"name":"id","in":"path","description":"The application request's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveRequest"}}},"required":true},"responses":{"200":{"description":"The request, now `approved` and naming the grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationRequest"}}}},"400":{"description":"The grant is not the caller's, was not issued to this institution, has no sealed bundle yet, was sealed at a stage this application did not ask for, does not carry every scope this application asked for, or has already been recorded as the answer to another application","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such pending request of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/application-requests/{id}/decline":{"post":{"tags":["vera"],"summary":"`POST /me/application-requests/{id}/decline` — say no.","description":"Nothing is sealed and nothing is explained. The institution is not told,\nbecause it was never told an application was coming: in review mode an\napplication is a conversation between the user and their own agent until the\nuser decides otherwise.","operationId":"decline","parameters":[{"name":"id","in":"path","description":"The application request's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The request, now `declined`. Nothing was disclosed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationRequest"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such pending request of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/applications":{"get":{"tags":["applications"],"summary":"`GET /me/applications` — the caller's applications, newest first.","description":"One list, both kinds: the ones recorded by hand through\n`POST /me/applications` and the ones made here through\n`POST /postings/{id}/apply`. That is the point of the table — a person's job\nsearch is one history whether or not this service happened to be in the middle\nof it — and the three id columns are what tells them apart: an application\nmade here names its posting, its employer and the grant it issued, and one\nrecorded by hand names none of them.\n\nEach row carries both accounts of where it stands. `status` is the\napplicant's and `stage` is the employer's; see [`APPLICATION_STAGES`].","operationId":"list_applications","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The applications the caller has recorded or made, newest first, each with the applicant's `status` and the employer's `stage`. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationRecord"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["applications"],"summary":"`POST /me/applications` — record an application.","description":"`submitted_at` is filled in rather than demanded: an application recorded as\nanything but a draft was, by definition, sent, and making the client restate\nthe obvious is how a record ends up with a status of `submitted` and no date.","operationId":"record_application","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordApplicationRequest"}}},"required":true},"responses":{"201":{"description":"The application was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationRecord"}}}},"400":{"description":"`company` was empty, `status` is outside the set, or `job_url` is not an http(s) URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/autofill":{"post":{"tags":["autofill"],"summary":"`POST /me/autofill` — resolve a form's questions against the caller's record.","description":"The response has one entry per question, in the order they were asked, so a\ncaller can zip it straight back onto its own fields.","operationId":"fill","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutofillRequest"}}},"required":true},"responses":{"200":{"description":"One answer per question, in the order asked. `source` says where each came from — `structured` from a profile field, `answer_bank` from something this user answered before, `gap` from nowhere yet. A gap is the ordinary case for a question nobody has answered, not an error: put it to the user and send what they say to `POST /me/autofill/learn`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutofillResponse"}}}},"400":{"description":"No questions, more than the batch limit, a question that is empty or too long, or an `answer_type` outside the closed set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/autofill/learn":{"post":{"tags":["autofill"],"summary":"`POST /me/autofill/learn` — put the user's answer to a gap into the bank.","description":"The other half of the loop: `POST /me/autofill` reports a gap, the caller\nasks the user, and what they say comes here. The next form to ask that\nquestion — at another employer, through another ATS, spelled differently —\nresolves it from the bank.\n\nThe body is `PUT /me/answers`'s, and the write is that route's write. Two\nspellings of one act, deliberately: the auto-fill loop should not have to\nknow that the thing it is filling a gap in is called the answer bank, and the\nanswer it stores should be indistinguishable from one stored any other way —\nsame fingerprint, same type check, same refusal for a document that is not\nthe caller's.\n\nThat includes the sealing. Since blocker 9 the body carries a `record_id` and\nno value, so a caller filling a gap seals the answer first and posts its id\nhere — which is section 8.5's \"`POST /me/autofill/learn` becomes a\nclient-side encrypt-then-store\", and is what keeps this route from being the\ndoor round the answer bank's encryption.","operationId":"learn","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreAnswerRequest"}}},"required":true},"responses":{"200":{"description":"The stored answer, exactly as `PUT /me/answers` returns it. Asking the same question again through `POST /me/autofill` now resolves it with `source: answer_bank`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredAnswer"}}}},"400":{"description":"The same refusals `PUT /me/answers` makes: the question was empty or has nothing to identify it by; a value was sent, which is a field value and is not stored here; `answer_type` is missing or disagrees with the type this question is already recorded as; `record_id` does not name one of the caller's own live `answers:read` records; or `value_doc_id` is not one of the caller's documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/availability":{"put":{"tags":["availability"],"summary":"`PUT /me/availability` — replace the caller's preferences.","operationId":"put_preference","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutPreferenceRequest"}}},"required":true},"responses":{"200":{"description":"The stored preferences, after the write. A PUT replaces: an omitted field clears the stored value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityPreference"}}}},"400":{"description":"A field failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["availability"],"summary":"`PATCH /me/availability` — change some of the caller's preferences.","operationId":"patch_preference","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchPreferenceRequest"}}},"required":true},"responses":{"200":{"description":"The stored preferences, after the merge. An omitted field is left alone; an explicit null clears it. Upserts, so this works before any preferences have been written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityPreference"}}}},"400":{"description":"A field failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/availability-shifts":{"put":{"tags":["availability"],"summary":"`PUT /me/availability-shifts` — set what the caller can work on one day.","description":"Keyed by `day_of_week`, so writing the same day twice updates it rather than\nadding a second row. That is why this is a `PUT` on the collection and not a\n`POST`: the resource is the day, and the caller already knows its name.","operationId":"put_shift","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutShiftRequest"}}},"required":true},"responses":{"200":{"description":"The day, after the write. Writing the same day again updates it — the grid holds at most one row per day.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShiftEntry"}}}},"400":{"description":"`day_of_week` is outside 0..6, or a time is malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/availability-shifts/{id}":{"delete":{"tags":["availability"],"summary":"`DELETE /me/availability-shifts/{id}` — forget what was said about a day.","description":"Distinct from writing `available: false`, which is a definite \"not this day\".","operationId":"delete_shift","parameters":[{"name":"id","in":"path","description":"The row's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The day is no longer recorded"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/capabilities":{"get":{"tags":["vera"],"summary":"`GET /me/capabilities` — the agents this account has paired.","operationId":"list","responses":{"200":{"description":"The caller's capabilities, newest first, with what each has spent of its preset and whether its revocation is finished.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CapabilitySummary"}}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["vera"],"summary":"`POST /me/capabilities` — pair an agent, and mint the auto mode Vera ID that\nreaches it.","description":"One transaction, three rows: the id, the capability, and the trail. They\ncannot exist apart — an id with no capability would authorise nothing and a\ncapability with no id would be unreachable — and the user is told, in the\nnotice, exactly what they have just handed over.","operationId":"pair","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairRequest"}}},"required":true},"responses":{"201":{"description":"The capability, and the auto mode Vera ID that reaches it — shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairedAgent"}}}},"400":{"description":"A key that is not 32 bytes, a signature that does not verify against this account's published signing key, a mandate that disagrees with the request, a preset outside its bounds, or an unusable scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This capability id already exists, or the account has no keyring to sign with","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/capabilities/{id}":{"delete":{"tags":["vera"],"summary":"`DELETE /me/capabilities/{id}` — take the delegation back.","description":"Immediate here, and honest about what \"here\" means: the agent is refused at\nevery route from the next request, and the scope keys it already holds are\nretired at the user's next sign-in, not now. The response says which of those\nhas happened through `rotation_pending`, and the interface must not round it\nup to \"revoked\".\n\nWhat makes the second half happen rather than merely be described is the\n`scope_rotation` row this writes for every scope the capability covered. It\nis on this transaction, and it outlives the capability row, because the debt\nis \"an agent out there holds `SRK_{s,e}`\" rather than \"this table has a\nrevoked row in it\". See [`crate::rotation::mark_pending`].","operationId":"revoke","parameters":[{"name":"id","in":"path","description":"The capability's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"revoke_grants","in":"query","description":"Also revoke every grant this capability issued, which stops the\ninstitutions holding them reading any further. Section 4.5 makes this\nthe user's choice at revoke time rather than a default, because the\ndisclosures an agent made honestly are still disclosures the user meant.","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Revoked server-side. `rotation_pending` is `true` until the user's next sign-in retires the agent's scope keys.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CapabilitySummary"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such capability of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/consents":{"get":{"tags":["regulated"],"summary":"`GET /me/consents` — the caller's consent trail, newest first.","operationId":"list_consents","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The consents the caller has recorded, newest first. Withdrawals are rows of their own with `granted: false`, so the trail reads as a history rather than a current state. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ConsentRecord"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["regulated"],"summary":"`POST /me/consents` — record a consent, or its withdrawal.","description":"Append-only. Withdrawing consent is a new row with `granted: false`, not an\nedit of the old one, because the fact that consent was given on the 3rd\nremains true after it is withdrawn on the 9th — and a trail that can be\nrewritten is not evidence of anything.\n\n# Tying a consent to the application it was given for\n\n`application_id` is the one field here that names something outside this\ntable, and it is optional because most consents are outside an application:\nthe terms of service somebody agrees to at sign-up is a consent to nothing\nin particular, and so is a standing authorisation recorded from the consent\nscreen. Those rows keep a `NULL`, which is the truthful value and not a gap.\n\nWhere it *is* sent — the consent an applicant gives while answering a\nvacancy, on the same visit as `POST /postings/{id}/apply` or the delegated\n`POST /shared/postings/{id}/apply` — this route checks that the application\nnamed is the caller's own before it writes the row. **That check is the\nroute's and cannot be the column's.** The foreign key added by\n`20260905000010_consent_application.sql` proves only that the application\nexists, so a route that bound whatever id it was handed would let anybody\nwith a session file a consent record pointing at a stranger's application —\nwhich is the one way this column can be got wrong that matters, because a\nconsent record is evidence and evidence about the wrong application is\nworse than none.\n\nThe check is a `SELECT` under the caller's own identity, so\n`application_owner` is what makes it true rather than the predicate alone,\nand an application that does not exist and one belonging to somebody else\nare deliberately the same `400`. It names the field rather than answering\n`404`, because the id came out of a request body and the caller's mistake is\nin what they sent rather than in where they sent it.","operationId":"record_consent","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordConsentRequest"}}},"required":true},"responses":{"201":{"description":"The recorded consent. `ip` and `user_agent` are what the server observed, not what the request claimed, and `application_id` echoes the link where one was given and is null where the consent belongs to no application.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentRecord"}}}},"400":{"description":"`consent_type` or `signature_method` is outside its set, a field is too long, or `application_id` does not name an application of the caller's own — which is deliberately the same answer for one that does not exist and one that is somebody else's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/credentials":{"post":{"tags":["credentials"],"summary":"`POST /me/credentials` — record a licence or certification.","operationId":"create_credential","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCredentialRequest"}}},"required":true},"responses":{"201":{"description":"The credential was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialEntry"}}}},"400":{"description":"A missing or oversized field, an unrecognised status, or an expiry before the issue date","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/credentials/{id}":{"delete":{"tags":["credentials"],"summary":"`DELETE /me/credentials/{id}` — and its CDL detail goes with it, by\n`ON DELETE CASCADE`.","operationId":"delete_credential","parameters":[{"name":"id","in":"path","description":"The credential's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The credential, and any CDL detail on it, are gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such credential — including one that belongs to somebody else, which is indistinguishable on purpose","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["credentials"],"summary":"`PATCH /me/credentials/{id}` — change one of the caller's credentials.","description":"The CDL detail is not reachable from here; it has its own route.","operationId":"patch_credential","parameters":[{"name":"id","in":"path","description":"The credential's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchCredentialRequest"}}},"required":true},"responses":{"200":{"description":"The credential, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/credentials/{id}/cdl":{"put":{"tags":["credentials"],"summary":"`PUT /me/credentials/{id}/cdl` — write the commercial detail onto one of the\ncaller's credentials, and mark that credential as a CDL.","description":"A `PUT` because the detail is a singleton with a name the caller already\nknows — the credential's — and it replaces: a body that omits\n`endorsements` records a licence with none, which is a real answer and the\none a form that renders every field submits.\n\nBoth writes are one transaction. `is_cdl` and the existence of a class are\ntwo statements of the same fact, and a partial write would leave a credential\ncarrying a Class A endorsement set while claiming not to be a driving licence.","operationId":"put_cdl","parameters":[{"name":"id","in":"path","description":"The credential's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutCdlRequest"}}},"required":true},"responses":{"200":{"description":"The stored detail, after the write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CdlDetail"}}}},"400":{"description":"A class outside A/B/C, an endorsement outside H/N/T/P/S/X, a restriction outside E/L/M/N/O/V/Z/K, or `id` not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["credentials"],"summary":"`DELETE /me/credentials/{id}/cdl` — forget the class breakdown.","description":"`is_cdl` is left alone deliberately. Removing the detail says the class,\nendorsements and restrictions are no longer recorded; it does not say the\nlicence stopped being a driving licence, and quietly deciding that it did\nwould erase an answer the user gave.","operationId":"delete_cdl","parameters":[{"name":"id","in":"path","description":"The credential's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The detail is gone; the credential and its `is_cdl` flag remain"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such credential, or it has no CDL detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/documents":{"get":{"tags":["documents"],"summary":"`GET /me/documents` — the caller's documents, grouped by kind with the\ndefault of each kind first.","operationId":"list","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The documents the caller holds, by kind, each kind's default first. Metadata only — the bytes come from `/me/documents/{id}/content`. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DocumentRecord"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["documents"],"summary":"`POST /me/documents` — upload a sealed document.","description":"`multipart/form-data`, because that is what a browser file input and a\n`curl -F` both produce and because the alternative — base64 in a JSON field\n— inflates a ten-megabyte ciphertext by a third to say the same thing. The\nparts are:\n\n* `file` — the **ciphertext**, exactly `byte_class + 16` bytes;\n* `document_id` — the UUID the client minted, which is bound into the\n  associated data of both halves and is the id of the record that carries\n  this document's key;\n* `byte_class` — the class the plaintext was padded to;\n* `ct_nonce` — 24 bytes, base64;\n* `doc_type` — one of the nine kinds;\n* `is_default` — optional.\n\nThere is no `label`, no `filename` and no media type, and their absence is\nthe point: all three are field values and all three are inside the ciphertext\nof the document's own record. A client that sends one is refused by name\nrather than having it quietly dropped, because \"I labelled it and the label\nvanished\" is a worse discovery than a 400.\n\n**The id comes from the client.** It used to be generated here. It cannot be\nany more: the id is authenticated into the file's associated data and into\nthe record's, and a server that allocated it would be asking the client to\nencrypt against a value it does not yet know. An id already in use is a 409,\nand deliberately does not say whose — see [`crate::encrypted::write_record`],\nwhich draws the same line for the same reason.\n\nThe bytes are written before the row is inserted. If the insert then fails\nthe file is removed again; the reverse order would allow a row that names a\nfile which was never written, and a row is the only thing the content routes\ntrust.","operationId":"upload","requestBody":{"description":"`file` — the sealed document, exactly `byte_class` + 16 bytes. `document_id` — the UUID the client minted and sealed against. `byte_class` — the padding class. `ct_nonce` — 24 bytes, base64. `doc_type` — one of the nine kinds. `is_default` — optional; `true` promotes this document and demotes the previous default of its kind.","content":{"multipart/form-data":{}}},"responses":{"201":{"description":"The ciphertext is stored and the document is recorded. Write the metadata record next, under the same id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRecord"}}}},"400":{"description":"Not a usable multipart body, no `file` part, a `document_id` that is not a UUID, a `doc_type` outside the set, a padding class outside section 1.6's, a ciphertext that is not exactly `byte_class` + 16 bytes, a nonce of the wrong length, or a `label`, `filename` or media type — none of which this route takes, because all three are field values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A document with this id already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"The sealed document is larger than this service stores","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/documents/{id}":{"delete":{"tags":["documents"],"summary":"`DELETE /me/documents/{id}` — forget a document, bytes and all.","description":"The row goes first and the file second, and the two are not one atomic act:\nthere is no transaction that spans Postgres and a filesystem. The order is\nthe one whose failure mode is survivable — a file with no row is an orphan\nnothing can reach, while a row with no file is a document the owner can see\nand cannot open.\n\nBoth files go: the sealed one the row names, and the superseded plaintext of\na re-sealed document if its owner had not yet spent\n[`forget_legacy`]. Forgetting a document forgets every copy of it this\nservice holds, which is the one place the plaintext is destroyed without\nbeing asked about separately — because the owner has just asked for the\ndocument itself to be gone, and leaving its plaintext behind would be the\nservice keeping a copy of a thing somebody deleted.\n\nThe document's **record** — the metadata and the file key, written under this\nsame id — is not touched here. It is an `encrypted_record` and is deleted\nthrough `DELETE /me/records/{id}`, which destroys its ciphertext and keeps\nits tombstone so another device can tell \"deleted\" from \"never seen\". A\nclient removing a document does both.\n\nAn answer that named this document keeps its place with its `value_doc_id`\ncleared, rather than being deleted with it: the question was still answered,\nand what changed is that the file it pointed at is gone.","operationId":"delete","parameters":[{"name":"id","in":"path","description":"The document's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The document and its bytes are gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["documents"],"summary":"`PATCH /me/documents/{id}` — reclassify or promote one of the caller's\ndocuments. The file itself is immutable: replacing it is an upload and a\ndelete.","operationId":"patch","parameters":[{"name":"id","in":"path","description":"The document's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchDocumentRequest"}}},"required":true},"responses":{"200":{"description":"The document, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRecord"}}}},"400":{"description":"A field failed validation, `id` is not a UUID, or a `label` was sent for a sealed document — where the label lives in the document's record and not in a column","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/documents/{id}/content":{"get":{"tags":["documents"],"summary":"`GET /me/documents/{id}/content` — the owner downloads their own document.","operationId":"content","parameters":[{"name":"id","in":"path","description":"The document's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The stored bytes, with the media type they were accepted as, an `attachment` disposition carrying the filename, and the SHA-256 of the contents as the `ETag`.","content":{"application/octet-stream":{"schema":{"type":"string"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such document, or a metadata-only record from before this service stored bytes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/documents/{id}/legacy":{"delete":{"tags":["documents"],"summary":"`DELETE /me/documents/{id}/legacy` — destroy the plaintext a re-seal\nsuperseded.","description":"The only place in this service where a document's plaintext is deliberately\nunlinked, and it is the **owner** who does it, from a client that has already\nopened the sealed copy. There is no sweep behind this and no operator tool\nbeside it, and that is a decision rather than an omission: the bytes in\nquestion are the only remaining copy of somebody's transcript if their client\nsealed the wrong thing, and a timer that destroyed them on a schedule would\nbe this service making that call on their behalf.\n\nIdempotent, like every other delete here: a document with no superseded\nplaintext is a `204`, because the caller asked for a state.","operationId":"forget_legacy","parameters":[{"name":"id","in":"path","description":"The document's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The superseded plaintext is gone, or there was none. The document itself is untouched."},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/documents/{id}/sealed":{"put":{"tags":["documents"],"summary":"`PUT /me/documents/{id}/sealed` — replace a version 1 document's plaintext\nwith the ciphertext its owner's client has just produced from it.","description":"This is the whole of the transition for documents, and the sequence on the\nclient's side is:\n\n1. `GET /me/documents` shows a row at `crypto_version: 1`;\n2. `GET /me/documents/{id}/content` fetches the plaintext, which this\n   service still serves and still holds;\n3. the client seals it under a fresh file key, writes the metadata record\n   under the document's own id, and calls this route with the ciphertext;\n4. `DELETE /me/documents/{id}/legacy` destroys the plaintext, once its owner\n   has seen the sealed copy open.\n\n**Step 4 is separate on purpose and this route does not do it.** The\nciphertext is written under a *new* storage key, the row is pointed at it in\none transaction, and the old key is recorded in `legacy_storage_key` — so at\nno instant does this document have no readable copy, and if the commit fails\nthe new file is litter rather than the old file being gone. A row update and\na file unlink cannot be one transaction; the order that survives the failure\nis this one.\n\n**What this service cannot check, stated plainly.** It cannot tell whether\nthe ciphertext is the plaintext. A client that seals the wrong bytes, or\nbytes under a key it then forgets, produces a document nobody can open and\nthis route will store it without complaint, because it has no way to form an\nopinion — the same sentence [`crate::encrypted::put_keyring`] has to say\nabout the DMK inside a new wrap. That is exactly why the plaintext survives\nthis call, and why destroying it is a second, deliberate act.\n\nThe multipart body is [`upload`]'s, minus `doc_type` and `is_default`, which\nthis is not an edit of, and minus `document_id`, which is in the path.","operationId":"reseal","parameters":[{"name":"id","in":"path","description":"The document's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"`file` — the sealed document, exactly `byte_class` + 16 bytes. `byte_class` — the padding class. `ct_nonce` — 24 bytes, base64.","content":{"multipart/form-data":{}}},"responses":{"200":{"description":"The document, now at `crypto_version: 2` and with `has_legacy_bytes: true`. The superseded plaintext is still on disk and is served by nothing; `DELETE /me/documents/{id}/legacy` destroys it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRecord"}}}},"400":{"description":"The same refusals `POST /me/documents` makes about shape","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such document of the caller's, or one that never had bytes at all","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`already_sealed` — this document is already at `crypto_version: 2`. A second re-seal is refused rather than being allowed to overwrite a sealed file with another one, which would be an edit of an immutable thing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/driving-equipment":{"post":{"tags":["credentials"],"summary":"`POST /me/driving-equipment` — record a kind of equipment operated.","operationId":"create_driving_equipment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDrivingEquipmentRequest"}}},"required":true},"responses":{"201":{"description":"The equipment was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DrivingEquipmentEntry"}}}},"400":{"description":"A missing or oversized field, a total outside 0–1200 months, or a range that ends before it starts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/driving-equipment/{id}":{"delete":{"tags":["credentials"],"summary":"`DELETE /me/driving-equipment/{id}`.","operationId":"delete_driving_equipment","parameters":[{"name":"id","in":"path","description":"The entry's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The entry is gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["credentials"],"summary":"`PATCH /me/driving-equipment/{id}`.","operationId":"patch_driving_equipment","parameters":[{"name":"id","in":"path","description":"The entry's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchDrivingEquipmentRequest"}}},"required":true},"responses":{"200":{"description":"The entry, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DrivingEquipmentEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/eeo":{"get":{"tags":["regulated"],"summary":"`GET /me/eeo` — the caller's own self-identification answers.","operationId":"get_eeo","responses":{"200":{"description":"The caller's EEO self-identification, or `null` if they have never written any. This is the only read of this record the owner has: it is not in the record document, and it is never an input to matching, search or ranking.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EeoSelfIdentification"}]}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["regulated"],"summary":"`PUT /me/eeo` — replace them.","operationId":"put_eeo","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutEeoRequest"}}},"required":true},"responses":{"200":{"description":"The stored answers, after the write. A PUT replaces: an omitted field clears the stored value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EeoSelfIdentification"}}}},"400":{"description":"A field failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["regulated"],"summary":"`DELETE /me/eeo` — withdraw them.","description":"Voluntary means withdrawable. A self-identification the user no longer wants\non file leaves no row behind, which is a different and stronger state than a\nrow whose every column is null.","operationId":"delete_eeo","responses":{"204":{"description":"There is no self-identification row. Idempotent."},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/export":{"get":{"tags":["portability"],"summary":"`GET /me/export` — everything this service holds about the caller.","description":"One request, one document, no pagination: a record a person cannot take\nwhole is one they cannot take. The read is a series of statements rather\nthan one transaction, which is the honest trade — a bundle assembled while\nthe owner is also editing may catch an edit between two sections, and the\nalternative is holding a repeatable-read snapshot open across the whole\nrecord for a route anyone may call.\n\nThe export is itself an act on the record, so it is written to the trail as\n`account.exported` — carrying how many rows went, and none of what was in\nthem.","operationId":"export","responses":{"200":{"description":"Everything held about the caller, in one document: the account, the profile and all its sections, the regulated islands, the documents' metadata, the answer bank, the applications, the grants, the Vera IDs and the mandates behind them, what agents applied for in this person's name, the verifications, the session history and the custody trail. `tables` names every physical table it drew from. No session token hashes, no Vera IDs and no stored provider key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportBundle"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/grants/{id}/activity":{"get":{"tags":["grants"],"summary":"`GET /me/grants/{id}/activity` — the custody trail for one delegation.","description":"The other half of the lifecycle question. `use_count` and `last_used_at` on\n`GET /grants` say *whether* a delegation is being used; this says what it\ndid — every `audit_log` entry written against this grant, newest first: its\ncreation, each profile read, each regulated island disclosed, each draft\nmade in the owner's name, and its revocation.\n\nSession-authenticated and owner-only. A grant belonging to somebody else is\na 404 rather than a 403, exactly as it is for [`revoke`]: ownership is not\nsomething a stranger gets to probe for, and the alternative would make this\nendpoint a way to ask whether a given grant id exists.\n\nThe trail outlives the grant's usefulness on purpose. Revoking a delegation\ndoes not erase what it did, so this answers for a revoked or expired grant\nexactly as it does for a live one — that is the whole point of asking.\n\nPaged — see [`crate::paging`]. Newest first, so the default page is the\nmost recent entries and `?offset=` walks back through the history.","operationId":"activity","parameters":[{"name":"id","in":"path","description":"The grant's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The audit trail for this grant, newest first. One page: at most `limit`, default 50, maximum 200 — `?offset=` walks back through the history. Answers for revoked and expired grants too — revoking a delegation does not erase what it did, and that is the whole point of asking. The detail is symbolic throughout: grant ids, scope names and the company a draft was for, never a value from the record the grant unlocked.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GrantActivity"}}}}},"400":{"description":"`id` is not a UUID, or `limit`/`offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such grant, or it belongs to somebody else — ownership is not something a stranger gets to probe for","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/grants/{id}/bundle":{"post":{"tags":["grants"],"summary":"`POST /me/grants/{id}/bundle` — seal a set of record keys to the institution\nthis grant was issued to.","description":"The one operation in the system that delegates read access to an outside\nparty, and it happens entirely in the client: by the time a request reaches\nthis handler the sealing is done, and what arrives is 80 bytes per record\nthat only the institution's private key opens.\n\n**The grant must name an audience.** A bundle is sealed to one institution's\nkey, so a delegation naming no institution has nothing to seal to; issuing\none and then discovering there is no recipient is a worse failure than\nrefusing here. `POST /grants {\"audience\": \"acme-university\"}` is where that\nis decided.\n\nOne bundle of each kind per grant, enforced by the schema. Sealing again is\nnot an edit: the entries are bound to named record versions and a named key\ngeneration, so replacing one would retire a disclosure the institution may\nalready have pulled. A second disclosure is a second grant.","operationId":"seal","parameters":[{"name":"id","in":"path","description":"The grant this bundle discloses under","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SealBundleRequest"}}},"required":true},"responses":{"201":{"description":"Sealed and stored. The manifest of what crossed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BundleSummary"}}}},"400":{"description":"A `kind` that is not `preview` or `full`, a sealed key that is not 80 bytes, a statement whose signature does not verify against this account's published signing key, a statement that disagrees with the upload, a `rp_key_id` that names no live registered key of the audience, or an entry naming a record that is not the caller's, is deleted, is at another version, or lies outside the grant's scopes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`revoked` or `expired`: a withdrawn or lapsed delegation cannot be sealed under, which is the re-seal half of what revocation stops","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such grant of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This grant already has a bundle of this kind, the grant names no audience, or the account has no keyring to sign with","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/grants/{id}/bundles":{"get":{"tags":["grants"],"summary":"`GET /me/grants/{id}/bundles` — what this delegation has actually disclosed.","description":"The manifest of section 5.3, and the thing offered in place of a claw-back:\nwhich records, at which versions, to which institution, under which key, and\nwhen — with the signed statement itself, so the owner keeps the consent\nartifact rather than only our summary of it.","operationId":"list_bundles","parameters":[{"name":"id","in":"path","description":"The grant's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Every bundle sealed under this grant, preview first, each with the exact records and versions it disclosed. Never a sealed key.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BundleSummary"}}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such grant of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/inbox":{"get":{"tags":["inbound"],"summary":"`GET /me/inbox` — the caller's own envelopes, newest first, a page at a time.","description":"Envelopes only: no ciphertext. See [`InboxItem::payload`].","operationId":"list_inbox","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0},{"name":"doc_type","in":"query","description":"Only this document type. Absent means every type.","required":false,"schema":{"type":"string"},"example":"pay_statement"},{"name":"schema","in":"query","description":"Only this schema. Absent means every schema.","required":false,"schema":{"type":"string"},"example":"payroll"},{"name":"include_blocked","in":"query","description":"Include the items whose sender has since been blocked. Off by default:\nblocking a sender is a request for their items to leave the inbox, and\nthe flag is here because it is a request for them to leave rather than to\nbe destroyed.","required":false,"schema":{"type":"boolean"},"example":false}],"responses":{"200":{"description":"One page of envelopes, newest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InboxItem"}}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["inbound"],"summary":"`POST /me/inbox` — put a document in your own inbox, or land one you pulled.","description":"Two origins and one route, because they are the same act from this service's\nside: a client sealed something to the account's own key and is writing it\ndown. What separates them is one label, and the label is what the person\nreads afterwards.\n\nNeither is [`ORIGIN_INSTITUTION`] and neither can become it. The schema\nrefuses a non-institution row that names a sender or carries a signature, and\nthe `inbound_item_immutable` trigger refuses any later UPDATE that would\nchange the origin, the connector, the sender or the provenance. So \"an item\nnobody signed for can never be relabelled as one somebody did\" is enforced in\nthe two places that cannot be talked out of it.\n\n**The self-upload** exists because the inbox should be able to hold the whole\npicture. A person who has one payroll statement pushed by their employer and\none from a previous job they still have the PDF of should be able to keep\nboth in one place — and the value of the distinction between them comes\nentirely from its being reliable, which means the unattested one has to be\nstorable and has to be visibly unattested.\n\n**The pull** exists because the push direction needs the sender to have\nintegrated, and on the first day nobody has. It is the bridge and it is\nlabelled as one. See [`crate::connectors`] for why the fetch happens in the\nperson's own client and can happen nowhere else.","operationId":"upload","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadRequest"}}},"required":true},"responses":{"201":{"description":"Stored, labelled `self` or `connector`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxItem"}}}},"400":{"description":"An unknown schema, an unusable padding class, a payload of the wrong length, a hash that is not of it, or a connector this deployment does not run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/inbox/connectors":{"get":{"tags":["inbound"],"summary":"`GET /me/inbox/connectors` — what this deployment can pull from, and what it\nonly intends to.","description":"Behind a session, and that is about honesty rather than secrecy: the list is\nnot sensitive, but it is the input to a screen that asks somebody to hand a\ncredential to a third party, and a route that served it to anonymous callers\nwould be inviting a copy of it to be embedded somewhere it will go stale.\n\nIt serves the planned entries as well as the available ones, with their\nstatus on each. A catalogue that hid what was not built would be a catalogue\nthat could not be checked against what a screen offers, and the screen has to\nbe able to say \"not yet\" in the same list rather than in a footnote.\n\n**It reads no row**, which is worth noticing rather than hiding: the answer is\na property of the build, identical for every caller, and the session is a gate\nrather than a lookup key. [`AuthUser`] is therefore the whole of the handler's\nsignature. There is deliberately no per-account connector state anywhere in\nthis module, because a credential for a vendor is the person's and belongs in\ntheir own client, never in a row here.","operationId":"list_connectors","responses":{"200":{"description":"Every connector this deployment knows about, available or planned","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Connector"}}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/inbox/senders":{"get":{"tags":["inbound"],"summary":"`GET /me/inbox/senders` — the senders this caller has an opinion about.","description":"Only the ones they have decided something about. There is deliberately no row\nfor a sender with no preference, because \"no opinion\" is the state every\ninstitution starts in and materialising it would make the list a directory of\nevery registered institution rather than a record of the person's decisions.","operationId":"list_senders","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of decisions, most recently changed first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SenderPreference"}}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/inbox/senders/{org_id}":{"put":{"tags":["inbound"],"summary":"`PUT /me/inbox/senders/{org_id}` — block a sender, unblock one, or say\nwhether its items may travel under a standing grant.","description":"The two settings are deliberately separate columns and separate fields.\nBlocking is a decision about **receiving** and auto-include is a decision\nabout **releasing**, and collapsing them into one \"trusted sender\" switch\nwould be exactly the conflation rule 5 exists to prevent: a person who is\nhappy for their payroll processor to send them their own pay statements has\nnot thereby agreed that every institution holding a standing grant may read\nthem.\n\nThe organization has to be a **registered** relying party. Anything else is a\n`404`, and that is not pedantry: only a registered institution can push, so a\npreference about anybody else would be a decision with no subject. The lookup\nruns under the caller's own identity through `relying_party_public_read`, so\nnothing is widened to answer it.","operationId":"set_sender","parameters":[{"name":"org_id","in":"path","description":"The institution","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSenderRequest"}}},"required":true},"responses":{"200":{"description":"The decision as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderPreference"}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a registered relying party","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/inbox/{id}":{"get":{"tags":["inbound"],"summary":"`GET /me/inbox/{id}` — one item, with the ciphertext.","description":"Reading it does not mark it read: a client that renders a list by fetching\neach row would otherwise mark everything read on the way past, and \"read\" is\na claim about a person rather than about a request. `POST /me/inbox/{id}/read`\nis the claim.","operationId":"get_item","parameters":[{"name":"id","in":"path","description":"The item","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The envelope and the sealed payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxItem"}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not this caller's item, or no such item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/inbox/{id}/read":{"post":{"tags":["inbound"],"summary":"`POST /me/inbox/{id}/read` — say this item has been read.","description":"Idempotent, and the first stamp wins: a second call keeps the moment the\nperson actually opened it rather than the moment their client retried.","operationId":"mark_read","parameters":[{"name":"id","in":"path","description":"The item","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The item, with `read_at` set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxItem"}}}},"401":{"description":"No session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not this caller's item, or no such item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/keyring":{"get":{"tags":["encrypted"],"summary":"`GET /me/keyring` — the caller's own keyring, or `null`.","description":"`null` rather than a 404 for an account that has never enrolled, matching\nthe regulated islands: \"you hold nothing here\" is an answer, not a missing\nresource, and a v1 account legitimately holds nothing here for the whole of\nthe migration.","operationId":"get_keyring","responses":{"200":{"description":"The caller's keyring, the current epoch of every scope they have written in, and the scopes owed a key rotation because an agent was revoked — or `null` if they have not enrolled. Never the `auth_key` verifier, and never anything that would open a wrap.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Keyring"}]}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["encrypted"],"summary":"`PUT /me/keyring` — store or replace the caller's keyring.","description":"One keyring per user, enforced by the primary key: a second `PUT` replaces\nthe first. That is the normal path rather than an edge case, because three\nordinary events re-wrap the DMK — a password change, a recovery-phrase\nrotation, and the transparent KDF upgrade in section 1.7 — and all three\narrive here as a whole new keyring.\n\n# Every write here is a step-up, enrolment included\n\nThe route stored a replacement on the session alone until this slice, and\nthat made a stolen bearer token an account takeover rather than an account\ncompromise: a keyring is a password, so writing one is *changing* the\npassword, and the thief's next act is to write their own `auth_verifier` and\nlock the owner out of an account whose ciphertext they still cannot read but\nwhose owner can no longer reach it either. The same write can change\n`sign_pub` and `box_pub`, which is what grant statements are verified against\nand what vendors seal to.\n\nSo a request that finds a keyring already there has to prove the credential\nbehind it, in one of exactly two ways:\n\n* **`current_auth_key`** — the 32 bytes the account authenticates with today,\n  checked against the stored verifier through the same\n  [`crate::auth::verify_secret`] a login goes through. This is a password\n  change.\n* **A session minted by `POST /auth/recover`** — which proved possession of\n  the recovery key, and therefore of the DMK, by signing a challenge under a\n  key derived from it. This is a password *reset*, and without it recovery\n  would hand somebody a session that could read their record and never fix\n  the password that lost it. That authority lapses after\n  [`RECOVERY_GRADE_MINUTES`]; the session does not.\n\n**And the first `PUT` for an account is not the exception it used to be.**\n\"There is no keyring, so there is nothing to prove\" reads as though enrolment\nwere the harmless case, and it is the opposite: on a version 1 account —\nwhich is every account that has not enrolled — a bare session was enough to\nset the verifier, the signing key, the box key and both wrap slots in one\nrequest. Whoever did it first owned the account, and the real owner's\npassword stopped working against a route that now checks the verifier the\nthief wrote. The account's own ciphertext was never at risk, because there\nwas none; what was at risk was every future record and the owner's ability to\nsign in at all.\n\nSo enrolment proves the account password: **`current_password`**, checked\nagainst `users.password_hash` through the same [`crate::auth::verify_secret`]\na login goes through. That is a credential the session's bearer may not have\n— a stolen token is not a stolen password — which is the entire property\nbeing bought. The two accounts that reach this path both have it to hand: a\nsignup holds the bootstrap credential it minted seconds earlier, and a\nversion 1 account adopting a keyring is being driven by somebody who just\ntyped their password in.\n\nAnything else is a `403`.\n\nOn **every** write, replacement and enrolment alike, every other session of\nthe account is revoked. On a replacement the tokens they hold were minted\nagainst a credential that no longer exists. On an enrolment the argument is\nthe same one a step further: the account has just acquired a credential none\nof those sessions was minted against, and if one of them was the reason this\nenrolment is happening, leaving it signed in would be the takeover completing\none request later.\n\n**What the server cannot check, stated plainly.** It cannot tell whether the\nDMK inside the new wrap is the same DMK as the one inside the old wrap. If a\nclient writes a keyring wrapping a *different* DMK, every existing\n`encrypted_record` for that user becomes permanently unopenable, and this\nservice will store the new keyring without complaint because it has no way\nto form an opinion. Keeping the DMK constant across a re-wrap is the\nclient's responsibility and `vg_crypto::dmk` is where it is enforced.","operationId":"put_keyring","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutKeyringRequest"}}},"required":true},"responses":{"200":{"description":"Stored. Every write — an enrolment as much as a replacement — also revokes every other session of the account. The response is the keyring as it now stands, which is what the client would read back.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Keyring"}}}},"400":{"description":"A byte string that is not base64, or not the length the construction that produced it emits, or Argon2id parameters below the floor this service stores a keyring at","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`reauthentication_required`: the session alone is not enough to write a keyring. On a first enrolment the request did not carry the account's `current_password`, or carried a wrong one; on a replacement it proved neither the current `auth_key` nor, through the session it arrived on, a recent recovery. Nothing was written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/languages":{"post":{"tags":["background"],"summary":"`POST /me/languages` — record a language, or 409 if it is already recorded.","operationId":"create_language","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLanguageRequest"}}},"required":true},"responses":{"201":{"description":"The language was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageEntry"}}}},"400":{"description":"`name` was empty, or the proficiency is not one of the four","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The caller already records that language. The constraint is per-user, so this says nothing about anyone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/languages/{id}":{"delete":{"tags":["background"],"summary":"`DELETE /me/languages/{id}`.","operationId":"delete_language","parameters":[{"name":"id","in":"path","description":"The language's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The language is gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such language","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["background"],"summary":"`PATCH /me/languages/{id}` — usually to record a proficiency after the fact.","operationId":"patch_language","parameters":[{"name":"id","in":"path","description":"The language's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchLanguageRequest"}}},"required":true},"responses":{"200":{"description":"The language, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such language","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Renaming it would duplicate another entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/links":{"post":{"tags":["contact"],"summary":"`POST /me/links` — add a link.","operationId":"create_link","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLinkRequest"}}},"required":true},"responses":{"201":{"description":"The link was added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkEntry"}}}},"400":{"description":"A missing or unrecognised field, or a URL with a scheme other than http/https","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/links/{id}":{"delete":{"tags":["contact"],"summary":"`DELETE /me/links/{id}`.","operationId":"delete_link","parameters":[{"name":"id","in":"path","description":"The link's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The link is gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["contact"],"summary":"`PATCH /me/links/{id}` — change one of the caller's links.","operationId":"patch_link","parameters":[{"name":"id","in":"path","description":"The link's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchLinkRequest"}}},"required":true},"responses":{"200":{"description":"The link, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/llm-key":{"get":{"tags":["llm-key"],"summary":"`GET /me/llm-key` — whether a key is held, and the last four characters.","description":"The decryption here is the narrowest use the stored key has: open the\nenvelope, take four characters, drop the plaintext. It exists so a user can\ntell *which* key they stored — the one from the right account, not the one\nthey rotated last week — without the API ever being able to hand it back.\n\nThe full key is not returned by this endpoint or any other.","operationId":"get_me","responses":{"200":{"description":"Whether a key is held, and its last four characters. `{\"present\": false}` exactly, when there is none. The key itself is not returned here or anywhere.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyStatus"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["llm-key"],"summary":"`PUT /me/llm-key` — store or replace the caller's API key.","description":"Validated, then envelope-encrypted, then upserted: the plaintext exists as a\nlocal for the length of [`crate::crypto::MasterKey::seal`] and is dropped\nbefore the statement runs. The row and its audit entry share a transaction,\nso the trail cannot record a key that was not stored, or miss one that was.\n\nOne key per user, enforced by the primary key: a second `PUT` replaces the\nfirst rather than accumulating a credential nobody remembers holding.","operationId":"put_me","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetKeyRequest"}}},"required":true},"responses":{"200":{"description":"Stored, replacing any previous key. One key per user: a second PUT replaces the first rather than accumulating a credential nobody remembers holding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyStatus"}}}},"400":{"description":"An unsupported provider, or an empty or oversized key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["llm-key"],"summary":"`DELETE /me/llm-key` — destroy the caller's stored key.","description":"Idempotent: deleting a key that is not there is a 204, not a 404, because\nthe caller asked for a state (\"no key stored\") and that state holds either\nway. The audit entry is written only when a row actually went away, so the\ntrail counts deletions rather than requests.\n\nThis is a real delete, not a soft one. There is no revoked-at column to\nleave the ciphertext lying around behind — a user withdrawing a key means\nthe bytes are gone.","operationId":"delete_me","responses":{"204":{"description":"No key is stored. A real delete, not a soft one — and idempotent, since the caller asked for a state rather than an act."},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/matches":{"get":{"tags":["matching"],"summary":"`GET /me/matches` — the seeker's ranked matches.","description":"Best first, then most recently scored, then by posting id so a page boundary\nis stable. Only open postings appear: the join says `status = 'open'` and\n`posting_public_read` says it again to Postgres, so a vacancy withdrawn since\nthe score was computed drops out of the list on the next read rather than\nwaiting for the recompute that will delete the row.\n\nNothing here computes anything. The rows were written by a `match.recompute`\njob — see [`recompute`] — which is what keeps a list route off the critical\npath of a scoring pass over five hundred postings.","operationId":"list_mine","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The caller's matches to open postings, best first, each with the score and the factors behind it. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SeekerMatch"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/notifications":{"get":{"tags":["notifications"],"summary":"`GET /me/notifications` — the caller's notices, unread first.","description":"Unread ahead of read rather than a plain reverse-chronological list, because\nthe question this route is opened to answer is \"what have I not dealt with\",\nand a client that had to page through last month's read notices to find one\nunread item would be paging for the server's convenience. Within each group\nit is newest first.","operationId":"list","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of the caller's notices: unread first, then newest first. At most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/notifications/read-all":{"post":{"tags":["notifications"],"summary":"`POST /me/notifications/read-all` — clear the badge.","description":"`WHERE read_at IS NULL` rather than an unconditional write, for two reasons:\nit touches only the rows that need touching, and it cannot overwrite an\nearlier `read_at` with a later one — the same property the single-notice\nroute gets from its `COALESCE`.","operationId":"mark_all_read","responses":{"200":{"description":"How many notices went from unread to read. Zero is a success: there was nothing to clear.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkedRead"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/notifications/unread-count":{"get":{"tags":["notifications"],"summary":"`GET /me/notifications/unread-count` — the number a client puts on the bell.","description":"Its own route rather than a field on the list, because a client that only\nwants the badge should not have to fetch fifty rows to compute it, and one\nthat has the list already can count what it holds. Served by the\n`(user_id, read_at)` prefix of the table's index.","operationId":"unread_count","responses":{"200":{"description":"How many of the caller's notices are unread","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnreadCount"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/notifications/{id}/read":{"post":{"tags":["notifications"],"summary":"`POST /me/notifications/{id}/read` — mark one notice read.","description":"Idempotent, and `COALESCE` is what makes it so: the second call leaves the\nfirst call's timestamp alone, so `read_at` answers \"when did they first see\nthis\" rather than \"when did their client last re-render\". Answers the row, so\na client can replace it in place without a second request.","operationId":"mark_read","parameters":[{"name":"id","in":"path","description":"The notification's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The notice, now carrying a `read_at`. Marking an already-read notice read again is a 200 with the original timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such notice — including one belonging to somebody else, which is deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/orgs":{"get":{"tags":["orgs"],"summary":"`GET /me/orgs` — the organizations the caller belongs to, and their role in\neach.","description":"Under `/me` with the rest of the owner-facing surface, and for the same\nreason: it answers from the session's user and takes no id from the request.\nA caller who belongs to nothing gets an empty list, which is the ordinary\nanswer for somebody who is not an employer.","operationId":"list_mine","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The caller's organizations, oldest membership first. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Membership"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/phones":{"post":{"tags":["contact"],"summary":"`POST /me/phones` — add a phone number.","operationId":"create_phone","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePhoneRequest"}}},"required":true},"responses":{"201":{"description":"The number was added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneEntry"}}}},"400":{"description":"A missing, oversized or unrecognised field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/phones/{id}":{"delete":{"tags":["contact"],"summary":"`DELETE /me/phones/{id}`.","operationId":"delete_phone","parameters":[{"name":"id","in":"path","description":"The number's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The number is gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such number","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["contact"],"summary":"`PATCH /me/phones/{id}` — change one of the caller's numbers.","operationId":"patch_phone","parameters":[{"name":"id","in":"path","description":"The number's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchPhoneRequest"}}},"required":true},"responses":{"200":{"description":"The number, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such number","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/records":{"get":{"tags":["encrypted"],"summary":"`GET /me/records` — the caller's own ciphertext, a page at a time.","description":"Oldest first, by `(created_at, id)`, because the caller of this route is a\nclient synchronising a whole record: an ascending, tie-broken order means a\nrow written during the walk lands at the end rather than shifting a page\nboundary underneath it.","operationId":"list_records","parameters":[{"name":"scope","in":"query","description":"Only this scope. Absent means every scope.","required":false,"schema":{"type":"string"},"example":"history:read"},{"name":"record_type","in":"query","description":"Only this record type. Absent means every type.","required":false,"schema":{"type":"string"},"example":"work_history"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of the caller's live records, oldest first. Ciphertext and wrapped keys, with the metadata that names them. Deleted records are not in this list.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EncryptedRecord"}}}}},"400":{"description":"An unknown scope, or an unusable page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/records/{id}":{"get":{"tags":["encrypted"],"summary":"`GET /me/records/{id}` — one record version.","operationId":"get_record","parameters":[{"name":"id","in":"path","description":"The record id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The row: ciphertext, wrapped key, and every component of the associated data they were sealed under.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EncryptedRecord"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such record for this caller, or it has been deleted. Somebody else's record is also a 404 — the reply does not distinguish between a record that is not yours and one that does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["encrypted"],"summary":"`PUT /me/records/{id}` — write a record version.","description":"With no `If-Match`, this creates: the id must not already be in use, and the\nrow is written at version 1. With `If-Match: <n>`, this replaces version `n`\nwith version `n + 1`, and a version that has moved underneath the caller is\na `409` rather than a lost edit.\n\nThe caller mints the id, because the id is authenticated into the ciphertext\nit is uploading. A server that allocated it would be asking the client to\nencrypt against a value it does not yet know.","operationId":"put_record","parameters":[{"name":"id","in":"path","description":"The record id, minted by the client","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"If-Match","in":"header","description":"The record version being replaced. Absent means create. A version that has moved is a 409.","required":false,"schema":{"type":["string","null"]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutRecordRequest"}}},"required":true},"responses":{"200":{"description":"Replaced. The row as it now stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EncryptedRecord"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EncryptedRecord"}}}},"400":{"description":"An unknown scope, an unusable padding class, a ciphertext that is not exactly `byte_class + 16` bytes, or a wrap or nonce of the wrong length","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such record for this caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The id is already in use (on a create), the version has moved (on a replace), or `write_epoch` is not the scope's current epoch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["encrypted"],"summary":"`DELETE /me/records/{id}` — destroy a record's bytes and keep its name.","description":"The row survives, holding an id, a version, a scope and a `deleted_at`; the\nciphertext and the wrapped key are nulled, and the schema's tombstone\nconstraint is what makes \"deleted\" and \"holds no bytes\" the same state. That\nis a real destruction rather than a soft one: there is nothing left to\ndecrypt, and a later dump does not contain the record.\n\nThe row is kept because a client synchronising from another device has to be\nable to tell \"deleted\" from \"never seen\", and because the deletion is an act\non the record that belongs in the custody trail beside every other one.","operationId":"delete_record","parameters":[{"name":"id","in":"path","description":"The record id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The record holds no ciphertext. Idempotent: deleting an already-deleted record is a 204, because the caller asked for a state."},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such record for this caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/references":{"post":{"tags":["background"],"summary":"`POST /me/references` — record a reference.","operationId":"create_reference","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReferenceRequest"}}},"required":true},"responses":{"201":{"description":"The reference was recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferenceEntry"}}}},"400":{"description":"A missing, oversized or malformed field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/references/{id}":{"delete":{"tags":["background"],"summary":"`DELETE /me/references/{id}`.","operationId":"delete_reference","parameters":[{"name":"id","in":"path","description":"The reference's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The reference is gone"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["background"],"summary":"`PATCH /me/references/{id}` — change one of the caller's references.","operationId":"patch_reference","parameters":[{"name":"id","in":"path","description":"The reference's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchReferenceRequest"}}},"required":true},"responses":{"200":{"description":"The reference, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferenceEntry"}}}},"400":{"description":"A field failed validation, or `id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/scopes/{scope}/rotate":{"post":{"tags":["encrypted"],"summary":"`POST /me/scopes/{scope}/rotate` — retire a scope root key and adopt its\nsuccessor.","description":"Owner-authenticated and owner-only, which here is a property of the path\nrather than a check inside it: there is no id in this route but the caller's\nown session, every statement below is keyed on `auth.user_id`, and row-level\nsecurity is the second line under them. A caller holding a grant token or an\nagent's Vera ID reaches a `401` at the router, because neither is a session —\nand that is the right refusal rather than an oversight. An agent that could\nask for a rotation could ask for one that re-seals to itself.\n\nThe work is the client's and this route is the exchange:\n\n```text\n  client:  SRK_old = HKDF(DMK, scope || e)          from the keyring\n           SRK_new = HKDF(DMK, scope || e + 1)\n           for each live record:  rotate_wrapped_dek(SRK_old, SRK_new, row, AAD_rec)\n           for each live agent:   reseal_scope_key(agent_box_pub, SRK_new, AAD_cap)\n  server:  epoch -> e + 1, store the wraps, store the seals, clear the debt\n```\n\nOne transaction, and the ordering inside it matters: the epoch row is taken\nfirst, which locks it against a concurrent `PUT /me/records/{id}` — that\nroute reads and pins the same row before it writes — so the coverage check\nbelow cannot race a write that lands under the old epoch after it ran.","operationId":"rotate","parameters":[{"name":"scope","in":"path","description":"One of the grant catalogue's scopes","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateScopeRequest"}}},"required":true},"responses":{"200":{"description":"Rotated. The scope's new epoch, and what was stored. Any `rotation_pending` this scope carried is cleared, and a capability whose every scope has now been rotated since it was revoked stops reporting one too.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotatedScope"}}}},"400":{"description":"An unknown scope, a wrap or nonce of the wrong length, a record named twice, a capability named twice, or more records or capabilities than one rotation carries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`from_epoch` is not the scope's current epoch, the records in the body are not exactly the live records in the scope, a record's version has moved, the capabilities in the body are not exactly the ones live over this scope — including an attempt to re-seal to a revoked one — or a `payload_version` is not the next one. Nothing was written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/sensitive-id":{"get":{"tags":["regulated"],"summary":"`GET /me/sensitive-id` — the caller's own date of birth and SSN last four.","operationId":"get_sensitive_id","responses":{"200":{"description":"The caller's sensitive identifiers, or `null` if they have never written any. Never part of any profile document.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SensitiveId"}]}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["regulated"],"summary":"`PUT /me/sensitive-id` — replace them.","operationId":"put_sensitive_id","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutSensitiveIdRequest"}}},"required":true},"responses":{"200":{"description":"The stored identifiers, after the write. A PUT replaces: an omitted field clears the stored value. `ssn_last4` is four digits — the full number is not storable by this service.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveId"}}}},"400":{"description":"`ssn_last4` is not four digits, or `date_of_birth` is not a plausible past date","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["regulated"],"summary":"`DELETE /me/sensitive-id` — forget them.","operationId":"delete_sensitive_id","responses":{"204":{"description":"There is no sensitive-identifier row. Idempotent."},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/sessions":{"get":{"tags":["auth"],"summary":"`GET /me/sessions` — the caller's live sessions, newest first.","description":"Neither revoked nor expired, because this list exists to be acted on; see\nthe module docs. Paged like every other collection this service serves, so\nan account that has signed in from a great many places still gets a bounded\nanswer.\n\nOne statement rather than a list followed by a lookup per row: the login\naddress comes back on the same page through a `LEFT JOIN LATERAL` on the\n`auth.login` entry, which the index added by\n`20260905000002_session_login_ip.sql` resolves by session id. `LEFT` and not\nan inner join, so a session whose trail entry is gone is still listed,\nwithout an address.","operationId":"list","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"One page of the caller's live sessions, newest first. Exactly one of them is `current`. Never a token or a token hash.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionSummary"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/sessions/revoke-others":{"post":{"tags":["auth"],"summary":"`POST /me/sessions/revoke-others` — end every live session but this one.","description":"The act a person reaches for when they have lost a device or suspect\nsomebody else is signed in: one call, no list to read first, and the browser\nthey are sitting in front of keeps working. Doing it by revoking rows one at\na time would leave the outcome depending on which of them the client\nhappened to know about.\n\nThe count is of *live* sessions, so it means \"this many credentials stopped\nworking\" rather than \"this many rows were touched\": already-revoked and\nexpired sessions are excluded by the same predicate [`list`] uses, and a\nsecond call therefore answers `0` rather than re-reporting the first call's\nwork.\n\nAudited once, with the count and the session that was kept, rather than once\nper row. What happened here is a single decision by a person, and a trail\nthat recorded it as forty separate revocations would be harder to read than\nthe act it describes. The individual sessions are identifiable from the\ntrail anyway: each has an `auth.login` entry naming it.","operationId":"revoke_others","responses":{"200":{"description":"How many live sessions were ended. The session making the request is not one of them and is still usable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokedOthers"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/sessions/{id}":{"delete":{"tags":["auth"],"summary":"`DELETE /me/sessions/{id}` — end one of the caller's sessions.","description":"Effective on that session's very next request, which is the property\nserver-side sessions were chosen for in the first place.\n\nThree decisions worth stating:\n\n* **A session that is not the caller's is a `404`**, exactly as though it\n  did not exist. `WHERE user_id = $2` is what settles it, so this route is\n  not an oracle for whether some other account holds a given session id.\n* **Idempotent, by `COALESCE`.** A repeated call answers `204` and leaves\n  the first revocation's timestamp alone, so `revoked_at` says when the\n  session was actually ended rather than when somebody last clicked.\n* **The current session is not special.** Revoking the session making the\n  request is the same act as `POST /auth/logout` and is allowed to be: a\n  person looking at a list of their sessions and choosing the one they are\n  sitting in front of has said what they meant, and a refusal would be this\n  route inventing a rule the rest of the service does not have.","operationId":"revoke","parameters":[{"name":"id","in":"path","description":"The session's row id, as `GET /me/sessions` lists it","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Revoked, effective on that session's next request"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such session of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/settings":{"patch":{"tags":["notifications"],"summary":"`PATCH /me/settings` — turn the email channel on or off.","description":"Session only and self only like the rest of `/me`, and deliberately one route\nrather than a per kind matrix. Twelve switches is a preferences screen nobody\nvisits; one is a thing a person can find. The finer grained version of \"which\nof these do you want to hear about\" already exists and is the feed.\n\nAn empty body is a `200` that changed nothing, which is the honest answer for\na merge with nothing in it and lets a client read the current settings without\na route of its own.","operationId":"patch_settings","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettingsPatch"}}},"required":true},"responses":{"200":{"description":"The settings as they now stand","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Settings"}}}},"400":{"description":"The body is not an object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/totp":{"get":{"tags":["auth"],"summary":"`GET /me/totp` — whether this account holds a second factor.","description":"The read behind the Security card, and the read the console's own gate uses\nto tell \"you have not enrolled\" from \"you have not verified this session\".\nIt is not in the four routes the feature was specified as, and it is here\nbecause both of those screens would otherwise have to infer the answer from a\nrefusal on an unrelated route — which is how a client ends up reporting \"you\nare not an administrator\" to somebody who simply has not typed six digits\nyet.\n\nServes no key material; see [`TotpStatus`].","operationId":"status","responses":{"200":{"description":"Whether a second factor is enrolled, whether it is enabled, and how many recovery codes are left. Never the secret and never a code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpStatus"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["auth"],"summary":"`DELETE /me/totp` — take the second factor off, at the cost of passing it.","description":"A code is required, and a recovery code is accepted, because the person who\nmost needs this route is the person whose phone is gone. Without the second\nspelling, losing an authenticator would mean losing the console with no way\nback short of an operator at psql.\n\n**Not idempotent in the way `DELETE /me/llm-key` is.** Deleting a factor that\nis not there is a `404` rather than a `204`, because the caller sent a\ncredential and a route that answered \"done\" without checking it would be a\nroute that reports success for a wrong code. The recovery codes go with the\nrow: a real delete, not a soft one.","operationId":"disable","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeRequest"}}},"required":true},"responses":{"200":{"description":"The factor is gone, along with its recovery codes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpStatus"}}}},"400":{"description":"The code is neither six digits nor a recovery code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`totp_invalid`: the code does not match","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No second factor to remove","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`totp_throttled`: five failed verifications in five minutes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/totp/confirm":{"post":{"tags":["auth"],"summary":"`POST /me/totp/confirm` — prove the secret, and enable the factor.","description":"The second half of enrolment: a code computed from the stored secret is the\nonly evidence this service can have that the secret reached an authenticator\nintact, and it is evidence worth insisting on — the alternative is an account\nthat is locked out of the console by its own second factor.\n\nIdempotent in the only sense that matters: confirming a factor that is\nalready enabled leaves `enabled_at` alone, so the column says when the factor\nwas established rather than when somebody last typed a code at it.\n\nA recovery code is **not** accepted here. The point of the call is to prove\nthe *authenticator* holds the secret, and a recovery code proves only that\nthe enrolment response was read.","operationId":"confirm","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeRequest"}}},"required":true},"responses":{"200":{"description":"Enabled. `/admin` will now ask this session to verify.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TotpStatus"}}}},"400":{"description":"The code is not six digits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`totp_invalid`: the code does not match the stored secret in any accepted window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No enrolment to confirm","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`totp_throttled`: five failed verifications from this session inside five minutes. `Retry-After` is not sent; the window slides.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/totp/enrol":{"post":{"tags":["auth"],"summary":"`POST /me/totp/enrol` — mint a secret, store it disabled, and show it once.","description":"The first half of enrolment. Nothing is satisfied by the row this writes:\n`enabled_at` is null until [`confirm`] proves the secret with a code computed\nfrom it, and [`crate::admin::require_admin`] reads that column and not this\none.\n\n**An enabled factor is not replaced.** A second call for an account that\nalready holds one is a `409`, and the way past it is `DELETE /me/totp` with a\ncode — which is to say, by passing the factor. Allowing a silent replacement\nwould mean somebody holding a stolen session could take a second factor off\nby enrolling over it, which is the whole of what the factor is for. An\nenrolment that was *never confirmed* is replaced freely: it satisfies\nnothing, and the person on the other end of it is somebody whose first\nattempt did not land.\n\nThe secret is sealed with [`crate::crypto::MasterKey::seal`] before the\nstatement runs — the same envelope the LLM key vault uses, for the same\nreason: it must be readable back, so it cannot be a digest, so it is\nencrypted under a key that lives outside the database.","operationId":"enrol","responses":{"200":{"description":"A new secret, its `otpauth://` URI and ten recovery codes — shown here and never again. Nothing is enabled yet: send a code to `POST /me/totp/confirm` to finish.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Enrolment"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This account already holds an enabled second factor. Remove it with `DELETE /me/totp` first, which costs a code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/vera-ids":{"get":{"tags":["vera"],"summary":"`GET /me/vera-ids` — the ids this account has handed out.","description":"Revoked ones stay listed, like grants: a record of what an agent was once\nable to do is worth as much as the list of what one can do now.","operationId":"list","responses":{"200":{"description":"The caller's Vera IDs, newest first. No ids.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VeraIdSummary"}}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["vera"],"summary":"`POST /me/vera-ids` — mint a Vera ID in **review mode**.","description":"Review mode only, deliberately. Auto mode is not a flag on this route: it is\nminted by pairing an agent at `POST /me/capabilities`, which takes the\nagent's own keys, a preset, and a mandate the account signed. Making the\npowerful mode reachable by passing `{\"kind\": \"auto\"}` here would make a\ndeliberate upgrade into a typo, and section 4.3's guarantee is not one to\nhand over by accident.","operationId":"mint","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintVeraIdRequest"}}},"required":true},"responses":{"201":{"description":"The id, shown once. Hand it to the agent now: this service keeps only a hash and no later route can reissue it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintedVeraId"}}}},"400":{"description":"The label is too long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/vera-ids/{id}":{"delete":{"tags":["vera"],"summary":"`DELETE /me/vera-ids/{id}` — revoke an id.","description":"Immediate and idempotent. Every later request presenting it is refused, and\nso is every `/agent/*` call under a capability it reaches, because an auto\nmode Vera ID and its capability are revoked together — a capability still\nissuing grants under a withdrawn id would be the id's revocation meaning\nnothing — and the agent's node is retired with them, so that the tree says\nthe same thing about that agent as the row does.\n\nWhat this does **not** do is retire the scope keys an auto mode agent\nalready holds. That needs the user's own keys — this service has never held a\nDMK and cannot derive a scope root key — so it happens in their client at\ntheir next sign-in. What this route does do is make sure that happens: every\nscope the capability covered is written down as owing a rotation, on the same\ntransaction as the revocation, and `GET /me/keyring` serves that debt to the\nclient which discharges it through `POST /me/scopes/{scope}/rotate`. See\n[`crate::rotation`] and section 4.5. Nothing here claims the cryptographic\nhalf has already happened.","operationId":"revoke","parameters":[{"name":"id","in":"path","description":"The Vera ID's row id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Revoked, effective on the agent's next request"},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such id of the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/verifications":{"get":{"tags":["verifications"],"summary":"`GET /me/verifications` — every check on the caller's own record.","description":"Newest first, and all of them: superseded, failed and expired rows stay\nlisted. A verification history that quietly dropped its failures would be a\nworse record than none. Paged — see [`crate::paging`].","operationId":"list","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The caller's verifications, newest first — including the superseded, failed and expired ones. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Verification"}}}}},"400":{"description":"`limit` or `offset` is outside what this service serves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["verifications"],"summary":"`POST /me/verifications` — start a check on the caller's own record.","description":"Session-authenticated and self-only, like everything under `/me`: the row is\nwritten for the session's user, and no user id is taken from the request.\n\nThe row always starts `pending` unless the checker itself said otherwise,\nand the manual one never does. Repeating a kind is allowed and creates a\nsecond row — a re-check after a failure or an expiry is a new check, not an\nedit of the old one, and the summary reports the strongest of them.\n\n- 400 — an unknown `kind`, or a `provider` that is not a name we know\n- 401 — no session\n- 501 `provider_not_implemented` — a planned vendor, named but not built\n- 502 `verification_*` — the checker was named, and could not start\n- 201 — the pending verification","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVerificationRequest"}}},"required":true},"responses":{"201":{"description":"The verification, `pending`. No endpoint creates a verified one; the status changes only through the completion route.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"400":{"description":"An unknown `kind`, or a `provider` that is not a name this service knows","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"`provider_not_implemented`: a vendor this layer is shaped for — `persona`, `truv`, `argyle`, `checkr` — named before its integration exists. Deliberately not a silent fallback to the manual checker.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`verification_*`: the checker could not start the check","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/verifications/{id}/complete":{"post":{"tags":["verifications"],"summary":"`POST /me/verifications/{id}/complete` — record the outcome of a check.","description":"**This stands in for a vendor webhook.** In a deployment with a real checker\nthe outcome arrives as a signed callback from the vendor and is applied by a\nhandler that authenticates it; this route exists so the layer is complete\nand exercisable while there is no vendor to call back. See the module docs.\n\n`verified_at` is set by the same statement that sets the status, and cleared\non any other outcome, so a row can never carry a verification timestamp and\na status that disagrees with it. A row belonging to somebody else is a 404,\nidentical to one that does not exist.\n\n**A check a platform operator has decided is not completable from here.**\n[`crate::review`] is the queue where a manual check is actually ruled on, and\na stand-in that could overwrite a rejection would make that queue advisory.\nThe person may start a fresh check of the same kind — repeating a kind has\nalways been allowed, and a re-check after a refusal is a new check.\n\n- 400 — `id` is not a UUID, or `status` is not `verified`/`failed`/`expired`\n- 401 — no session\n- 404 — no such verification, or not the caller's\n- 409 — an operator has already decided this check\n- 200 — the verification as it now stands","operationId":"complete","parameters":[{"name":"id","in":"path","description":"The verification's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteVerificationRequest"}}},"required":true},"responses":{"200":{"description":"The verification as it now stands. `verified_at` is set by the same statement that sets the status, and cleared on any other outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"400":{"description":"`id` is not a UUID, or `status` is not one of `verified`, `failed`, `expired`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such verification — including one that belongs to somebody else, which is indistinguishable on purpose","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A platform operator has already decided this check. Start a new one of the same kind rather than overwriting theirs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/verifications/{id}/credential":{"get":{"tags":["verifications"],"summary":"`GET /me/verifications/{id}/credential` — the subject's own copy of the\nsigned attestation for one of their verified checks.","description":"**Why the subject may mint this.** Nothing here is the subject's word. The\npayload is built from the stored row — which only [`crate::review`] and the\nvendor path can move to `verified` — and signed by the platform key, so a\nperson can no more mint a credential for an unverified check than they can\nverify it. What this route adds is possession: until now the only copy was\nthe one handed to the operator who approved the check, and an attestation\nits subject cannot get hold of is one they cannot show anybody.\n\nA row that is not `verified` is a `409` rather than a `404`. The distinction\nis worth the extra state: \"you have no such check\" and \"your check has not\ncome back yet\" are different answers, and only the caller's own rows are\nvisible here either way.","operationId":"credential","parameters":[{"name":"id","in":"path","description":"The verification","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A freshly minted credential for the check, and the claim it asserts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssuedCredential"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such verification, or not the caller's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The check is not `verified`, so there is nothing to attest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/me/work-eligibility":{"get":{"tags":["regulated"],"summary":"`GET /me/work-eligibility` — the caller's own eligibility answers.","description":"A separate call rather than a section of the profile document, and that is\nthe point of the phase: the island is not part of what [`crate::profile`]\nassembles, so the owner reads it the same way a delegate does, deliberately,\nby asking for it.","operationId":"get_eligibility","responses":{"200":{"description":"The caller's eligibility answers, or `null` if they have never written any. Not part of the record document `GET /shared/profile` serves: this island is read on purpose or not at all.","content":{"application/json":{"schema":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WorkEligibility"}]}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"put":{"tags":["regulated"],"summary":"`PUT /me/work-eligibility` — replace the caller's eligibility answers.","operationId":"put_eligibility","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutEligibilityRequest"}}},"required":true},"responses":{"200":{"description":"The stored answers, after the write. A PUT replaces: an omitted field clears the stored value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkEligibility"}}}},"400":{"description":"A field failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"delete":{"tags":["regulated"],"summary":"`DELETE /me/work-eligibility` — forget the caller's eligibility answers.","operationId":"delete_eligibility","responses":{"204":{"description":"There is no eligibility row. Idempotent: deleting what is not there is a success, because the caller's intent — that we hold none of this — is satisfied either way."},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs":{"post":{"tags":["orgs"],"summary":"`POST /orgs` — create an organization; the caller becomes its owner.","description":"The two writes are one act and share the request's transaction: an\norganization with no owner would be a row nobody could ever administer, so it\nmust not be possible for the first insert to survive the second failing.\n\n# Why this one asks for a confirmed address\n\nThis route and [`invite_member`] are the only two in the service that do, and\nthe line between them and everything else is worth stating: an institution is\na thing other people's records are disclosed *to*, and it is the one object\nhere that a stranger can bring into existence. An account behind an address\nnobody has answered is an account nobody can be traced back through, and\nmaking that the founding credential of a company on this market would be the\ncheapest possible way in.\n\nNothing on the seeker's side is gated. A person with an unconfirmed address\nkeeps their whole record, every route over it, every grant they issue and\nevery application they send, because their record is theirs and proving an\naddress to us was never a condition of holding it. See\n[`crate::auth::require_verified_email`].\n\nThe id is minted here rather than by `DEFAULT gen_random_uuid()`, which is the\none thing about this handler that needs explaining. `organization` is under\nrow-level security, and the policy that admits a member's own organization\ncannot admit this insert — at that instant there is no membership row for it\nto find. So the handler names the organization it is about to create,\n[`crate::db::Db::act_in_org`] puts that id in the transaction's context, and\nboth inserts land under a policy keyed on it. The id is 122 bits of CSPRNG\noutput generated a line earlier; naming it is not a claim about anything that\nalready exists.","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrgRequest"}}},"required":true},"responses":{"201":{"description":"The organization was created and the caller is its owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Membership"}}}},"400":{"description":"`name` was empty, `website` is not an http(s) URL, or `slug` normalised to nothing usable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`email_unverified`: the caller has not confirmed their own email address. `POST /auth/resend-verification` sends another link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That slug is taken","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/applications/{application_id}":{"patch":{"tags":["pipeline"],"summary":"`PATCH /orgs/{id}/applications/{application_id}` — move an application\nthrough the pipeline.","description":"Any stage may follow any other; see this module's documentation for why there\nis no transition graph. Moving to the stage the row already holds is a `200`\nthat writes nothing, audits nothing and — the part that matters — notifies\nnobody.\n\nA real move is one transaction: the row, the sync job for the applicant, and\ntwo audit entries. The job is enqueued on that same transaction through\n[`crate::monitor::enqueue`], so there is no state in which a candidate has\nbeen told about a move that did not commit.\n\nThe applicant's own `GET /me/applications` reflects the new stage on the very\nnext request. Their `status` — their own account of the application — is not\ntouched by this handler and never will be: nothing an employer's request does\nreaches that column. What the sync job does with it is a narrower thing,\ndecided on the applicant's own identity and only while the column still says\nwhat this service last put there — see [`crate::monitor`].","operationId":"set_stage","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"application_id","in":"path","description":"The application's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetStageRequest"}}},"required":true},"responses":{"200":{"description":"The application as it now stands. Setting the stage it already held is a 200 that changed nothing and notified nobody.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Applicant"}}}},"400":{"description":"`stage` is missing or outside the set, or an id is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"`card_required` — this organization has no payment method on file and the stage asked for is one that means taking a new candidate in. It applies on every plan but `contract`, including the free one and including its first free pull: an owner or an admin adds a card at `POST /orgs/{id}/billing/card`. Or `upgrade_required` — this organization is on the free plan and its included pull-throughs are used. Sifting, reading and rejecting stay free and unlimited under both; a candidate it has already advanced can still be moved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_privilege` — the statement tried to change something other than the stage, which the database refuses. Or `billing_suspended` — this organization's billing is suspended and the stage asked for is one that means taking the candidate in (`interview`, `offer`, `hired`). Everything else on this side still works, including reading the applicants it already holds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such application addressed to it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/applications/{application_id}/access-request":{"post":{"tags":["access"],"summary":"`POST /orgs/{id}/applications/{application_id}/access-request` — ask a\ncandidate for a delegation.","description":"The membership is proved first and the organization context earned from it,\nexactly as every other employer route does; the application is then read\nunder that context, so an application addressed to somebody else's company is\na `404`.\n\nOne open question at a time. A second request while the first is still\npending is a `409`, enforced by a partial unique index rather than by this\nhandler's check alone — otherwise a company could turn a notification list\ninto a channel for nagging somebody.","operationId":"request","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"application_id","in":"path","description":"The application's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"}}},"required":true},"responses":{"201":{"description":"The request, now pending. The candidate has a notice in their list and nothing has been delegated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequest"}}}},"400":{"description":"An id is not a UUID, `scopes` is empty or carries a scope this service does not enforce, or `message` is too long","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such application of it, or the caller is not a member — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This organization already has a pending request to this candidate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/applications/{application_id}/notes":{"get":{"tags":["pipeline"],"summary":"`GET /orgs/{id}/applications/{application_id}/notes` — what this company has\nwritten about this application.","description":"Newest first. Admitted by `application_note_in_org`, which is keyed on the\norganization context [`crate::orgs::authorize`] earned — so the notes one\ncompany holds are unreachable from another's session, and unreachable from\nthe applicant's at any route in this service.","operationId":"list_notes","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"application_id","in":"path","description":"The application's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"This organization's notes on the application, newest first. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Note"}}}}},"400":{"description":"An id is not a UUID, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such application addressed to it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["pipeline"],"summary":"`POST /orgs/{id}/applications/{application_id}/notes` — write one.","description":"The insert reads the application it is about in the same statement, so there\nis no window between \"is this application ours\" and \"attach a note to it\",\nand no way for the `org_id` on the note to disagree with the one on the\napplication: it is copied from the row rather than taken from the caller.\nZero rows inserted is the `404`.\n\nThe applicant is **not** notified, and that is the whole difference between a\nnote and a stage change. A stage is what the employer has decided, which the\ncandidate is owed; a note is the employer thinking out loud, which they are\nnot. Nothing here writes to the applicant's trail either — no fact about them\nhas changed.","operationId":"add_note","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"application_id","in":"path","description":"The application's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddNoteRequest"}}},"required":true},"responses":{"201":{"description":"The note, as the list will show it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Note"}}}},"400":{"description":"`body` was empty or too long, or an id is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such application addressed to it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/applications/{application_id}/profile":{"get":{"tags":["applications"],"summary":"`GET /orgs/{id}/applications/{application_id}/profile` — the employer reads\nthe applicant, through the grant the application issued.","description":"One of the two routes in this service that read one person's record on\nanother person's session — the other is [`applicant_records`], which serves\nthe sealed half of the same disclosure — and every step of getting there is a\nproof rather than a claim. The five of them are [`follow`], which both routes\nshare; this one additionally demands `profile:read` of the delegation, and\nthen assembles the document with [`crate::profile::shared_view`], the same\ncode, to the field, that answers `GET /shared/profile`.\n\nThe read is recorded exactly as a delegated read is — a `profile.shared` row\nin the *applicant's* audit trail naming the grant, one row per regulated\nisland disclosed, and the grant's use counter — so\n`GET /me/grants/{id}/activity` shows an applicant when the company they\napplied to actually looked.\n\nThe employer's index of candidates is [`crate::pipeline`], and it is a\nseparate module for a reason this route makes plain: everything here is one\ndisclosure, made through one delegation, recorded in one person's trail.\n`GET /orgs/{id}/postings/{posting_id}/applicants` lists applications and\ndeliberately names nobody — it hands back this path per row and leaves the\ndisclosure to be made here, deliberately, one candidate at a time.","operationId":"applicant_profile","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"application_id","in":"path","description":"The application's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The applicant's profile, cut down to the sections the application's grant delegates — byte for byte the document `GET /shared/profile` returns for the same grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FullProfile"}}}},"400":{"description":"An id is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session, or the grant has expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`revoked` — the applicant has withdrawn the delegation — or `insufficient_scope`, a posting that asked for something other than `profile:read`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such application of it, or the caller is not a member — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/applications/{application_id}/records":{"get":{"tags":["applications"],"summary":"`GET /orgs/{id}/applications/{application_id}/records` — the employer reads\nthe **sealed** disclosure, with its own key.","description":"The route above serves the plaintext shared profile: everything this service\nstill holds in the clear, cut to the grant's scopes. This one serves what it\ndoes not hold in the clear — the sealed bundle the applicant's own client\nsealed to this institution, byte for byte the document `GET /shared/records`\nreturns, opened by the institution's private key in the institution's own\nenvironment and by nothing here. Both routes exist because both are true at\nonce during the cutover; this is the one the product is built on.\n\nIt is a sibling rather than a field on the profile because the two are\ndifferent reads of different things: this one is parameterised by the *stage*\nof the two-stage gate, it is metered, and it is the one an ATS integration\npolls. Folding a bundle into a profile response would also mean an employer's\nconsole fetching key material it has no way to open every time it rendered a\ncandidate's name.\n\n# Why an application needs its own door onto the bundle\n\n`GET /shared/records` takes a **grant token**, and an application's\ndelegation deliberately has none: the employer is a roster of accounts this\nservice already authenticates, so there is nobody to hand a bearer secret to\nand [`crate::grants::issue_without_token`] mints none. Without this route the\nsealed half of the human paths would be unreachable — the applicant's client\ncould seal a preview that nobody could ever pull.\n\nSo the credential is a session and a membership row, exactly as the profile\nread takes them, and the delegation is *followed* rather than presented. See\n[`follow`] for the five proofs that stand in for a token, and\n[`crate::bundles::serve`] for everything after them — which is one\nimplementation shared with the token door, so the recruiter's console and the\nrecruiter's ATS cannot see two different disclosures.\n\n# What it will not disclose\n\nNothing the grant does not carry. The bundle was checked against the\ndelegation's scopes when it was sealed and every entry is checked again here,\nat read time, because a record can be rewritten into another scope in\nbetween — a record outside them is reported in `stale` rather than served.\nThe `full` stage additionally refuses a suspended institution and is metered\nas an ingestion; a preview is free.","operationId":"applicant_records","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"application_id","in":"path","description":"The application's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"query","description":"`preview` (the default) or `full`.","required":false,"schema":{"type":"string"},"example":"preview"}],"responses":{"200":{"description":"The sealed bundle of the requested stage and the ciphertext it opens — the same document `GET /shared/records` serves, for an institution reading through its own session instead of a grant token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedRecords"}}}},"400":{"description":"An id is not a UUID, or a `kind` that is not `preview` or `full`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session, or the grant has expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`revoked` — the applicant has withdrawn the delegation — `relying_party_required` for an organization that has not registered, `wrong_audience` where the delegation names another institution, or `billing_suspended` on `kind=full`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such application of it, the caller is not a member — deliberately the same answer — or `bundle_not_sealed`: the applicant's client has not sealed a bundle of this stage, so its keys exist nowhere","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/billing":{"get":{"tags":["billing"],"summary":"`GET /orgs/{id}/billing` — the plan, the status, the allowance and what this\nperiod has cost so far.","description":"Four things an owner asks in one request: which plan, how much of the\nincluded allowance is left, how many pull-throughs this period came to and at\nwhat price — and `state`, the same answer in a sentence, so that a screen can\nsay \"10 free pulls, 3 used\" without three clients agreeing on how to phrase\nit. The standing fee is reported beside the metered amount rather than folded\ninto it, because \"what our usage cost\" and \"what we pay to be here\" are\ndifferent questions.\n\n**Owner only**, and that is a stricter gate than any other employer route in\nthis service. The argument is the same one that puts the roster behind an\nadmin: a recruiter's job is candidates, and what the company pays is a fact\nabout the company rather than about the work. An organization's owner is the\nperson who signed for it.\n\nAn organization with no `org_billing` row gets the free plan and an active\nstatus rather than a `404`, because that is genuinely what it is on. See\n[`Billing::of`].","operationId":"overview","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The plan and status this organization is on, what it has run up since its last invoice, and how many invoices it has. An organization nobody has priced is on the free plan and active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingOverview"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: billing is the owner's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member of it — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/billing/card":{"post":{"tags":["billing"],"summary":"`POST /orgs/{id}/billing/card` — begin putting a card on file, and say where\nto send the browser.","description":"**Owner or admin.** One step wider than the rest of this module's employer\nsurface, which is owner-only, and the widening is deliberate: reading what a\ncompany pays is a fact about the company, and *adding a payment method* is an\nadministrative errand an owner should be able to delegate without handing over\nthe plan, the invoices and the usage. It is still not a recruiter's — a\nrecruiter's job is candidates.\n\n# What it answers with, and what it does not do\n\nA URL. The browser goes there, the card is typed into a page the gateway\nserves, and the gateway redirects back to `success_url`. **The card is not on\nfile when this route answers** — it is on file when the gateway's signed event\narrives at [`crate::stripe::webhook`] and [`store_payment_method`] runs. That\nis stated here because it is the one thing about this route that can surprise:\na client that flipped a screen to \"card added\" on the redirect alone would be\nwrong for as long as the event took, and the honest thing for it to do is\nre-read `GET /orgs/{id}/billing` and watch `has_payment_method`.\n\nThe consequence for an operator is worth stating too: a deployment with a\nsecret key and **no** `STRIPE_WEBHOOK_SECRET`, or one whose endpoint is not\nregistered at Stripe, will hand out working Checkout pages and never record a\nsingle card — and every pull-through will go on being refused\n`402 card_required`. `deploy/README.md` says so beside the nginx note.\n\n# Why a redirect rather than a form here\n\nBecause the application's Content-Security-Policy forbids external script, so\nthere is nowhere to put a card field that is not this service's own — and a\ncard field that *is* this service's own would mean a card number arriving\nhere, which is the one thing this whole design is arranged to prevent. See\n[`crate::stripe`].\n\nAn unconfigured deployment is a `503` through [`gateway_not_configured`] and\n**before anything is written**; a gateway that cannot host a card page —\n[`crate::payments::HttpGateway`] — is the same `503`, because for this purpose\na gateway that cannot take a card is no gateway.","operationId":"create_card_session","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Where to send the browser. The card is on file once the gateway's signed event has been applied, which is after the redirect — poll `GET /orgs/{id}/billing` for `has_payment_method`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardSession"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: adding a card is an owner's or an admin's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member of it — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`gateway_unavailable`: the gateway could not be reached. Carries no text of the gateway's; the detail is in the log.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`gateway_not_configured`: this deployment has no payment gateway, or has one that cannot collect a card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/billing/customer":{"post":{"tags":["billing"],"summary":"`POST /orgs/{id}/billing/customer` — create this institution's record at the\npayment gateway.","description":"**Owner only**, like everything else an institution reads about its own\nmoney, and for the same reason: what the company pays is a fact about the\ncompany rather than about the work.\n\n**Idempotent.** An organization that already has an `external_customer_id`\ngets its billing row back unchanged and the gateway is not called — which is\nwhat makes a double-click, a retry after a timeout, or an operator and an\nowner both pressing the button produce one customer rather than several bills\nto reconcile. A body carrying an `email` still updates the stored contact,\nbecause that is a fact about this service's record rather than a second\ncustomer.\n\nThis is the one route in this service that reaches a payment provider inside\na request, and the contrast with [`admin_set_billing`] — which deliberately\ndoes not — is the argument for it. Pricing an institution is a local act that\nshould not fail because somebody else's API is down. Creating the gateway's\nrecord of a company *is* the call: there is no version of this route that\nsucceeds without it, so doing it asynchronously would mean answering `202` and\nmaking the owner poll for a boolean. The deadline is\n[`crate::payments::TIMEOUT`], which is shorter than [`crate::REQUEST_TIMEOUT`]\nso that a slow gateway cannot get the request rolled back underneath it.\n\nA gateway failure is a `502` carrying no gateway text, through\n`From<GatewayError>`; an unconfigured deployment is a `503`, through\n[`gateway_not_configured`] and **before anything is written**, so that a\nrefusal leaves nothing behind.","operationId":"create_customer","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"Optional. The company's billing contact, if it wants to name one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewCustomerRequest"}}},"required":true},"responses":{"200":{"description":"The organization's billing as it now stands, with `has_gateway_customer` true. Answered unchanged, without calling the gateway, for an institution that already had one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Billing"}}}},"400":{"description":"`id` is not a UUID, or `email` is not an address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: billing is the owner's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member of it — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`gateway_unavailable`: the gateway could not be reached, or refused. Carries no text of the gateway's; the detail is in the log.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`gateway_not_configured`: this deployment has not been given a payment gateway. A `503` rather than a `501` because the route is implemented and the deployment is unfinished.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/billing/usage":{"get":{"tags":["billing"],"summary":"`GET /orgs/{id}/billing/usage` — the line items behind the bill.","description":"Owner only, like the overview. Newest first, paged, and it serves the\n`pull_through` rows themselves rather than a summary — which is the whole\npoint of the route: an invoice a customer cannot take apart into the specific\nacts it charges for is an invoice they have to take on trust, and this\nservice is not in the business of asking for trust.\n\nBilled and unbilled rows both, because the question \"what did we pay for in\nMarch?\" is asked long after March was invoiced. `billed_at` and `invoice_id`\non each row say which bill it went onto.","operationId":"usage","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","description":"Only acts at or after this moment, RFC 3339. Absent, from the beginning.","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"Only acts strictly before this moment, RFC 3339. Absent, up to now.\n\nHalf-open on purpose: `from` of one month and `to` of the next then\npartition the meter exactly, with no act counted twice and none missed.","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The pull-throughs in the window, newest first, billed and unbilled alike. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PullThrough"}}}}},"400":{"description":"`id` is not a UUID, `from`/`to` is not RFC 3339, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: billing is the owner's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/invoices":{"get":{"tags":["billing"],"summary":"`GET /orgs/{id}/invoices` — the bills, newest first.","description":"Owner only. Every one of them is `draft` on this build, and the field says\nso rather than the route pretending otherwise: an invoice becomes `open` when\na configured payment gateway has taken it, and there is no gateway here yet.","operationId":"invoices","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"This organization's invoices, newest first. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}},"400":{"description":"`id` is not a UUID, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: billing is the owner's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/members":{"get":{"tags":["orgs"],"summary":"`GET /orgs/{id}/members` — who else is in this organization.","description":"Any member may read it, recruiters included: a roster is how a team knows who\ncan act on its behalf, and hiding it from the people it names would not make\nthe organization safer. It is the read half of the resource the three routes\nbelow write.","operationId":"list_members","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The organization's members, oldest first, each with their role and the address their account is registered under","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrgMember"}}}}},"400":{"description":"`id` is not a UUID, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member of it — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["orgs"],"summary":"`POST /orgs/{id}/members` — put an existing account on the roster.","description":"An invite in the sense that it grants a place, not in the sense that it sends\nanything: no acceptance token is minted, no message goes to the address, and\nthe account is added at once. That shape survives the arrival of outbound\nmail unchanged, and the reason is worth stating now that a message *could* be\nsent. Mailing an invitation to an address that has never registered would\nmean minting a credential for a stranger and telling them a company had named\nthem; what this route does instead is look up an account that already exists\nand add it. The lookup is the check, and mail would not have made it stronger.\n\n**Both addresses have to be confirmed.** The caller's, because putting\nsomebody on an institution's roster reaches past the caller's own record —\nthe same argument [`create`] makes. And the *invitee's*, because a member of\nan institution is a party this service will let read other people's records\nthrough a delegation, and an account behind an unanswered address is an\naccount whose owner has never been shown to exist. Both refusals are\n`403 email_unverified` and the message says which of the two it was, because\nthe caller has already proved they administer this organization and the two\nhave completely different fixes.\n\nAn address with no active account behind it is `404 no_such_user`, and not the\nflat `404` the org routes use for \"not yours\": the two failures need\ndifferent fixes, and the caller has already proved they administer this\norganization, so the answer tells them which one it was.","operationId":"invite_member","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteMemberRequest"}}},"required":true},"responses":{"201":{"description":"The account is on the roster","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgMember"}}}},"400":{"description":"`email` is not an address, or `role` is outside the set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: the caller is a recruiter, or is an admin trying to create an owner. Or `email_unverified`, which means either the caller or the account being added has not confirmed its email address; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization or not a member of it; or `no_such_user` — nothing is registered under that address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That account is already a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/members/{member_id}":{"delete":{"tags":["orgs"],"summary":"`DELETE /orgs/{id}/members/{member_id}` — take somebody off the roster.","description":"The same two rules as the role change, for the same reasons: an admin may not\nremove an owner, and the last owner cannot be removed at all. Removing a\nmember destroys their place in the organization and nothing else — their\naccount, their record and every grant they hold are untouched, because none of\nthem was the organization's to begin with.","operationId":"remove_member","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member_id","in":"path","description":"The membership row's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The member is off the roster"},"400":{"description":"An id is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: a recruiter, or an admin removing an owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such member of it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That would leave the organization with no owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["orgs"],"summary":"`PATCH /orgs/{id}/members/{member_id}` — change what a member may do.","description":"Three refusals beyond the guard, and each one is a rule the rank alone cannot\nstate:\n\n* An **admin may not touch an owner**, in either direction — not demote one,\n  and not create one. An organization where the second-ranking role can rewrite\n  the first has one role, not two.\n* The **last owner may not be demoted**. `409`, because the request is\n  well-formed and the caller is permitted; it is the organization's state that\n  refuses. A silent no-op here would leave a caller believing they had changed\n  something.\n* An unknown member id is a `404`, as is one belonging to another\n  organization: the row has to be on *this* roster, which the statement says\n  rather than a preceding check, so there is no window between the two.","operationId":"set_member_role","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member_id","in":"path","description":"The membership row's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetRoleRequest"}}},"required":true},"responses":{"200":{"description":"The member's role as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgMember"}}}},"400":{"description":"`role` is missing or outside the set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: a recruiter, or an admin acting on an owner or creating one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such member of it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That change would leave the organization with no owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/postings":{"get":{"tags":["postings"],"summary":"`GET /orgs/{id}/postings` — the organization's own postings, in every status.","description":"The employer's view, and the only way to read a `draft` or a `closed`\nposting at all. Newest first, because the thing a recruiter opens this list\nto find is almost always the one they wrote most recently.","operationId":"list_for_org","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","description":"`draft`, `open` or `closed`. Absent, every posting the organization has.","required":false,"schema":{"type":"string"},"example":"open"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The organization's postings, newest first. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Posting"}}}}},"400":{"description":"`id` is not a UUID, `status` is outside the set, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["postings"],"summary":"`POST /orgs/{id}/postings` — write a vacancy.","description":"Any member may, recruiters included: see this module's documentation for why\nthe rank that guards the roster does not guard the work.\n\nThe posting is created in `draft` unless the body says otherwise, so the\nroute that writes an advertisement and the act that publishes it are\nseparable — an employer can compose one over several `PATCH`es and publish\nonce. Creating one directly as `open` is allowed, because forcing a\ntwo-request dance on a client that has the whole posting in hand would be\nceremony rather than safety.","operationId":"create","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostingRequest"}}},"required":true},"responses":{"201":{"description":"The posting as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Posting"}}}},"400":{"description":"A field failed validation: an empty `title` or `description`, an `employment_type` or `status` outside its set, a scope this service does not enforce, or a malformed screening question","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member of it — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/postings/{posting_id}":{"delete":{"tags":["postings"],"summary":"`DELETE /orgs/{id}/postings/{posting_id}` — take a vacancy off the books.","description":"A real delete rather than a fourth status, because `closed` already *is* the\nwithdrawn state: a posting an employer wants to keep and stop advertising is\n`closed`, and one they want gone was written by mistake.\n\nOne thing does now reference a posting, and its foreign key is the reason\nthis is still a delete rather than a refusal. `application.posting_id` is\n`ON DELETE SET NULL`: an employer removing an advertisement must not thereby\nremove somebody else's record of having answered it. The applicant keeps the\nrow, the company name and the job title they applied under; what they lose is\nthe link, because the thing it linked to is gone. The grant goes on existing\nand goes on being revocable, and the employer — whose reach over it came from\nthe application row naming their organization — keeps it, because that row is\nstill theirs and still names them. See\n`20260819000010_applying_to_a_posting.sql`.","operationId":"delete","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"posting_id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"The posting is gone"},"400":{"description":"An id is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such posting of it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["postings"],"summary":"`PATCH /orgs/{id}/postings/{posting_id}` — edit a vacancy, or publish it.","description":"Read-modify-write inside the request's one transaction, like every other\n`PATCH` here, so the fields the body did not mention keep the values the row\nactually holds rather than the values the client last saw.\n\nA posting on another organization's roster of vacancies is a `404`, and the\n`org_id` predicate is in the statement rather than in a check before it, so\nthere is no window between \"is it ours\" and \"change it\".","operationId":"patch","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"posting_id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchPostingRequest"}}},"required":true},"responses":{"200":{"description":"The posting, after the change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Posting"}}}},"400":{"description":"A field failed validation, a required field was cleared, or an id is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such posting of it, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/postings/{posting_id}/applicants":{"get":{"tags":["pipeline"],"summary":"`GET /orgs/{id}/postings/{posting_id}/applicants` — one vacancy's queue.","description":"The route the notice from `POST /postings/{id}/apply` used to be the only\nsubstitute for. Newest first, because a queue is worked from the top.\n\nThe posting is confirmed to be this organization's *before* the applications\nare read, and a posting belonging to somebody else is the same `404` as one\nthat does not exist. That check is not redundant with the `a.org_id = $1`\npredicate underneath it: without it, naming another company's posting would\nanswer `200` with an empty list, which tells a caller the id was real.\n\nEvery stage is included by default, `rejected` and `withdrawn` among them. A\npipeline that hid its own outcomes would be a pipeline nobody could audit;\n`?stage=` is there for the client that wants one column of a board.","operationId":"list_applicants","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"posting_id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"stage","in":"query","description":"One of `applied`, `screening`, `interview`, `offer`, `hired`, `rejected`,\n`withdrawn`. Absent, every application to this posting.","required":false,"schema":{"type":"string"},"example":"screening"},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The applications to this posting, newest first, each with its stage, the answers submitted with it, the state of the delegation it issued and the path to read the applicant's profile through it. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Applicant"}}}}},"400":{"description":"An id is not a UUID, `stage` is outside the set, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such posting of it, or the caller is not a member — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/postings/{posting_id}/candidates":{"get":{"tags":["matching"],"summary":"`GET /orgs/{id}/postings/{posting_id}/candidates` — who the engine matched to\nthis vacancy.","description":"The employer's half of the matching engine, and the counterpart to\n`GET /orgs/{id}/postings/{posting_id}/applicants`: that route lists the\npeople who *answered* the advertisement, and this one lists the people whose\nrecord answers it whether or not they have ever seen it.\n\nBest first. **Every row carries the score and the factors behind it and no\nfield of anybody's record at all** — which used to be true with an exception,\nand is now true without one. The five-field summary this route followed a\ndelegation to read is gone with the columns it read; a recruiter who wants to\nread a candidate follows [`Candidate::profile_path`], which gates the read,\nredacts it to the grant's scopes and writes it into that candidate's trail.\n\nThe delegation is still reported — its status, its scopes, when it expires —\nbecause a recruiter has to be able to tell a candidate they may read from one\nwho has withdrawn, and that is metadata about a grant rather than content of a\nrecord.\n\nThe path is `/orgs/{id}/postings/{posting_id}/candidates` rather than the\nshorter `/org/postings/{id}/candidates`: every employer route in this service\nnames the organization it is acting for, because [`crate::orgs::authorize`]\nis what earns the organization context and it cannot be inferred from a\nposting id without first reading a row the caller may have no right to.","operationId":"list_candidates","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"posting_id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"The candidates matched to this posting, best first, each with the score and the factors behind it. No row carries a field of anybody's record: `permitted` says what the candidate's delegation would allow and `profile_path` is where it can be redeemed. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}}}}},"400":{"description":"An id is not a UUID, or the page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, no such posting of it, or the caller is not a member — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/relying-party":{"get":{"tags":["relying-party"],"summary":"`GET /orgs/{id}/relying-party` — this organization's own registration.","description":"Any member may read it, recruiters included, for the same reason any member\nmay read the roster: knowing whether your employer may receive a disclosure\nat all is not a privilege, and hiding it from the people it governs would not\nmake it safer.","operationId":"get_registration","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The registration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Registration"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a member, or this organization has not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["relying-party"],"summary":"`POST /orgs/{id}/relying-party` — apply to become an institution this service\nwill disclose records to.","description":"Admin or owner: this is the row that opens the gate for everybody on the\nroster, so it is not something a recruiter grants their own organization.\n\n**The registration lands in `pending` and an administrator moves it.** That\nis the change this slice makes to a route that used to write `registered`\nitself, and it is the whole of the enforcement: [`live_registration`] has\nadmitted one status since the gate was written, so a registration that has\nnot been reviewed cannot complete a shared read without another line being\nadded anywhere. See the module documentation for what is checked and why none\nof it costs the institution anything.\n\nTwo writes on the request's one transaction, and they commit together:\n\n* the registration, carrying the business-register fields and the named\n  responsible person, in `pending`;\n* the acceptance of the relying-party agreement, in\n  `relying_party_agreement`, naming the version the client showed and\n  recording the address and user agent **this service observed** rather than\n  anything the request claimed.\n\nA registration whose body does not accept the agreement is a `400` and\nneither row is written. There is nothing useful to store in a half-made\napplication: an administrator would only have to refuse it, and the\ninstitution would have learnt from a queue what a validation error could have\ntold them immediately.","operationId":"register","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Applied. The registration is `pending` until an administrator verifies it, and a pending institution may publish keys, post jobs and triage applicants but may not complete a shared read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Registration"}}}},"400":{"description":"`kind` is not one of the five, `domain` is not a bare host, a vetting field is missing or too long, `responsible_person_email` is not an address, or `agreement_accepted` is not true","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: this needs admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such organization, or the caller is not a member","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/relying-party/api-keys":{"get":{"tags":["relying-party"],"summary":"`GET /orgs/{id}/relying-party/api-keys` — what credentials this institution\nholds, and what each has been used for. Never the credentials themselves.","description":"Admin or owner, unlike the roster and the key list: a recruiter has no reason\nto enumerate the integrations, and `last_used_at` on a machine credential is\noperational information about the institution's systems.","operationId":"list_api_keys","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Every credential, newest first, with its use counter","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: this needs admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a member of this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["relying-party"],"summary":"`POST /orgs/{id}/relying-party/api-keys` — mint a credential that\nauthenticates the institution rather than a person in it.","description":"Admin or owner. The token is returned **once**; this service stores its\nSHA-256 and cannot show it again, which is why the response says so in the\nfield's own documentation rather than in a page somebody has to find.","operationId":"mint_api_key","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintApiKeyRequest"}}},"required":true},"responses":{"201":{"description":"Minted. The token is in this response and nowhere else, ever again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintedApiKey"}}}},"400":{"description":"An empty label, or an expiry that is not a positive number of days","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: this needs admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a member, or this organization has not registered as a relying party","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/relying-party/api-keys/{key_id}":{"delete":{"tags":["relying-party"],"summary":"`DELETE /orgs/{id}/relying-party/api-keys/{key_id}` — withdraw a credential.","description":"Binds on the very next request, like a session revocation and for the same\nreason: the credential is a row and not a signed claim, so the gate reads the\nrevocation rather than waiting for an expiry to arrive.","operationId":"revoke_api_key","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"key_id","in":"path","description":"The credential's id, from the mint or the listing","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Revoked, or already revoked"},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: this needs admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such credential in this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/relying-party/keys":{"get":{"tags":["relying-party"],"summary":"`GET /orgs/{id}/relying-party/keys` — every key this organization has\nregistered, revoked ones included.","description":"Deliberately wider than [`registry`], which serves live keys only: an\ninstitution auditing its own rotation needs to see the generation it retired\nlast quarter, and a client about to seal must not.\n\nWhich is why [`PublicKeyView::revoked_at`] is populated here and `null`\nthere. A listing that showed a withdrawn key and a live one identically would\nbe an audit of a rotation that cannot say what was rotated.","operationId":"list_keys","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Every key, newest first, with its evidence and — for a withdrawn one — its `revoked_at`","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicKeyView"}}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a member, or not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["relying-party"],"summary":"`POST /orgs/{id}/relying-party/keys` — register a public key and its\nattestation, and append it to the transparency log.","description":"Admin or owner. This is the material a seeker's client will seal a record key\nto, so it is the one write in this module where getting the authorisation\nwrong is not an inconvenience.\n\nThree things happen in one transaction, and the order is the point:\n\n1. The leaf is computed over the registration's canonical bytes and appended\n   to `relying_party_key_log`, which hands back the `seq` the row is filed\n   under. That is the leaf's *name*, not its index — see [`Log`].\n2. The key row is written, pointing at that leaf.\n3. The tree is read back, the leaf's **position** in it is looked up, and the\n   proof is cut against the tree **as it now stands** at that position. A tree\n   head is signed over it, and both are stored beside the institution's\n   attestation.\n\nThe registration-time proof and head are stored as the record of what was\npublished at that moment. What a *client* is served is a proof against the\ncurrent head ([`load_keys`]), because a proof against a tree nobody else is\nchecking any more is not evidence anybody can corroborate.\n\n**The signature is verified before anything is stored.** The institution\nsigns [`attestation_statement`] with its own Ed25519 attestation key, and\nthis route checks it with `verify_strict` against that key as this service\nalready holds it — a key that is itself a registered, logged key. Two\nconsequences worth stating:\n\n* An institution bootstraps by registering its Ed25519 attestation key\n  first, with a statement signed by that same key. That is a self-signed\n  announcement, and it is worth exactly what a self-signed announcement is\n  worth: nothing on its own. What gives it weight is that it goes into the\n  log, so the day an operator publishes a second attestation key for an\n  institution, that act is permanent and public.\n* An attestation that does not verify is a `400`. Not a row in a weaker\n  state: a client would refuse to seal to it, so storing it would be storing\n  a registration that cannot be used and telling the institution it worked.\n\n# The applicants who were here first\n\nRegistering a live [`SEAL_ALG`] key is the moment this institution becomes\nable to receive a disclosure, and it is therefore the moment the delegations\nthat were waiting on it stop being unsealable. An employer may take\napplications as a plain organization for weeks; each one mints a grant\nnaming nobody, [`crate::bundles::seal`] refuses a grant that names nobody,\nand the applicant was told in those words that they could share their record\nonce the employer registered. So this route calls\n[`crate::grants::bind_waiting_audiences`], which is where the argument for\ndoing it — and for the rows it must not touch — is set out at length.","operationId":"register_key","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterKeyRequest"}}},"required":true},"responses":{"201":{"description":"Registered and logged. Registering a live `x25519` key also names this institution on the delegations that were minted for it while it could receive nothing sealed, so their owners can share a record under them at last","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicKeyView"}}}},"400":{"description":"A key that is not 32 bytes, a signature that is not 64, an empty document, or a window that ends before it starts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: this needs admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a member, or this organization has not registered as a relying party","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This `key_id` and `alg` are already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/orgs/{id}/relying-party/keys/{key_id}":{"delete":{"tags":["relying-party"],"summary":"`DELETE /orgs/{id}/relying-party/keys/{key_id}` — stop future seals to a key.","description":"Admin or owner. Revoking stops *future* seals immediately and does nothing to\nbundles already sealed to it, which is the honest and unavoidable position\nsection 9.4 states: a private key that leaked cannot un-decrypt what it\nalready can. The row is stamped rather than deleted, and its leaf stays in the\nlog, because a log that forgets is not a log.\n\n## Why the algorithm is part of naming a key\n\nA key is unique by `(org_id, key_id, alg)` and not by `key_id`: an\ninstitution may perfectly well publish `2026-q3` as both the X25519 key\nclients seal to and the Ed25519 key it announces with, and the schema says\nso. Matching on the label alone therefore revoked **both** — so retiring one\nsealing generation also silently withdrew the attestation key that signs for\nevery other one, and clients were left unable to verify anything for that\ninstitution.\n\nSo:\n\n* `?alg=` names one row exactly, and exactly that row is revoked.\n* Without it, one live key under the label is revoked, which is what every\n  existing caller means and gets.\n* Without it, **two** live keys under the label are a `409` and nothing is\n  revoked. Guessing between them is the behaviour this route had; refusing is\n  the one it should have had, and the caller retries naming the algorithm.","operationId":"revoke_key","parameters":[{"name":"id","in":"path","description":"The organization's id","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"key_id","in":"path","description":"The institution's own label for the key","required":true,"schema":{"type":"string"}},{"name":"alg","in":"query","description":"Which algorithm's key to revoke: `x25519` or `ed25519`.\n\nOptional, because a `key_id` identifies one key in the common case and\nmaking every caller name the algorithm would break the ones that already\ndo not. It is not optional when the label is shared — see\n[`revoke_key`].","required":false,"schema":{"type":"string"},"example":"x25519"}],"responses":{"204":{"description":"Revoked, or already revoked"},"400":{"description":"`alg` is not one of the two","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`insufficient_role`: this needs admin","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such key in this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Two live keys share this `key_id`. Name the one to revoke with `?alg=`; nothing was revoked by this call","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/postings":{"get":{"tags":["postings"],"summary":"`GET /postings` — every open vacancy, newest first.","description":"The public feed, and the first route in this service that answers a stranger\nwith rows. It is not throttled, and that is a decision rather than an\noversight: this is an advertisement, the readers it is written for include\ncrawlers, and a job board that answers Googlebot with `429` has published\nnothing. What bounds the work is [`crate::paging`] — a caller gets at most\n200 rows per request and 50 by default, whoever they are — and the partial\nindex the feed reads in order.\n\nThe statement says `status = 'open'` and `posting_public_read` says it again\nto Postgres, so a draft cannot reach this route even if the `WHERE` clause is\none day mislaid.","operationId":"list_public","parameters":[{"name":"limit","in":"query","description":"Rows to return. 1 to 200; defaults to 50. A value outside that range is\na `400` naming the maximum rather than a silently shortened page.","required":false,"schema":{"type":"integer","format":"int64","maximum":200,"minimum":1},"example":50},{"name":"offset","in":"query","description":"Rows to skip. Defaults to 0.","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":0}],"responses":{"200":{"description":"Open postings, newest first, each with the employer behind it. One page: at most `limit`, default 50, maximum 200.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicPosting"}}}}},"400":{"description":"The page is out of range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/postings/{id}":{"get":{"tags":["postings"],"summary":"`GET /postings/{id}` — one open vacancy, with its structured data.","description":"The page a search engine indexes and a candidate applies from. A posting that\nis a draft, is closed, or does not exist are all the same `404`: an\nunpublished vacancy is not a secret worth an oracle, but neither is it\nsomething a stranger gets to learn the existence of.","operationId":"get_public","parameters":[{"name":"id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The posting, the employer, and `json_ld` — schema.org `JobPosting` for Google for Jobs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicPosting"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such posting, or it is not open — deliberately the same answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/postings/{id}/apply":{"post":{"tags":["applications"],"summary":"`POST /postings/{id}/apply` — answer a vacancy.","description":"Takes no body, and that is a decision rather than an omission. Everything an\napplication is made of is already on file: the posting says what it wants and\nwhat it asks to be delegated, and the applicant's record says the rest. A body\nhere would be a second place to put a screening answer, and the one place is\nthe profile and the answer bank — which is what makes the next application\ncheaper than this one.\n\nThe order of operations is the security argument, and the reads come first:\nthe posting is fetched, the duplicate refused and the employer's sealing key\nlooked up before anything is minted, so a second application to the same\nvacancy costs a `SELECT` and creates no delegation. The writes then all land\non the request's one transaction — the grant, the row, the notice's job — so\nthere is no state in which an employer has been told about an application\nthat does not exist, or holds a delegation for one.\n\nThe response carries the sealing instruction as well as the application: the\ngrant, the scopes, the employer's registry slug and whether they can receive\na sealed record. That is what the applicant's client needs in order to finish\nthe disclosure at `POST /me/grants/{id}/bundle`, and it is here rather than\nbehind a second call because the moment to seal is this moment — the person\nis present and their keys are unlocked. See [`Delegated`].","operationId":"apply","parameters":[{"name":"id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"The application, the delegation it issued — with the institution to seal to and whether it can receive a seal — and the screening questions split into the ones the auto-fill engine answered and the ones it could not. A gap is the ordinary case for a question nobody has answered before: put it to the user and send what they say to `POST /me/autofill/learn`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationSubmitted"}}}},"400":{"description":"`id` is not a UUID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such posting, or it is not open — deliberately the same answer, as everywhere else a posting is read without a credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The caller has already applied to this posting","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/rp/keys/log/proof":{"get":{"tags":["relying-party"],"summary":"`GET /rp/keys/log/proof` — inclusion and consistency proofs on demand.","description":"Two questions, one route, because a client asks both at once and a mirror\nasks the second one repeatedly:\n\n* `?leaf_index=N[&tree_size=M]` — prove that leaf `N` is in the tree of `M`\n  leaves, defaulting to the current head. This is what an auditor uses to\n  check a key it was told about through some other channel.\n* `?first=M[&tree_size=N]` — prove that the tree of `M` leaves is a prefix of\n  the tree of `N`. **This is the anti-rollback endpoint.** A client that\n  pinned a head asks this before accepting a newer one, and an operator that\n  removed or rewrote an entry cannot answer it.\n\nOpen, with no credential, for the reason the registry and the tree head are:\na proof that only the operator's own clients can fetch is not a proof anybody\ncan corroborate, and the mirrors this design depends on hold no account here.\n\nThe proofs are computed rather than stored, and that is safe in a way storing\nevidence is not: a proof is a derivation from the leaves, the leaves are\nappend-only, and any proof this route emits is checked by the client against\na root the log's key signed. There is nothing here for a wrong answer to\nachieve except a refusal.","operationId":"log_proof","parameters":[{"name":"leaf_index","in":"query","description":"The leaf to prove inclusion of. With `tree_size`, or against the current\nhead when that is omitted.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"first","in":"query","description":"The earlier tree size to prove consistency from.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"tree_size","in":"query","description":"The tree to prove against. Defaults to the current head.","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"The proof, and the head to check it against","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogProof"}}}},"400":{"description":"Neither `leaf_index` nor `first` was given, both were, or a size is outside the log","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/rp/keys/log/sth":{"get":{"tags":["relying-party"],"summary":"`GET /rp/keys/log/sth` — the current signed tree head.","description":"Open, for the reason the registry is. A client pins the latest head it has\nseen, refuses to accept one describing a smaller tree, and gossips it, so that\nan operator serving two different trees to two different clients is\ndetectable. None of that works if the head is behind a credential the\noperator issues.","operationId":"sth","responses":{"200":{"description":"The tree size, the Merkle root, the signed artifact over both, and the log's publication key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedTreeHead"}}}}}}},"/rp/{slug}/keys":{"get":{"tags":["relying-party"],"summary":"`GET /rp/{slug}/keys` — the keys a seeker's client may seal to for one\ninstitution, with the evidence that lets it refuse them.","description":"**Open, with no credential at all**, and that is the deliberate widening this\nmodule makes. A public key that had to be asked for with a credential would\nnot be a public key, and a client that has to authenticate to learn who it is\nabout to seal to is a client that cannot check the operator. The same\nargument `crate::attest::pubkey` makes for the platform's verifying key.\n\nWhat it serves is narrow: live keys of an institution whose registration says\n`registered`, and their evidence. A pending or suspended institution answers\n`404`, exactly as one that does not exist — the registry is not a directory of\napplications, and the id space should not be probeable for who is in review.\n\nThe `proof_verification_implemented` field on the response is `true` — it\nmirrors [`PROOF_VERIFICATION_IMPLEMENTED`], which mirrors\n`vg_crypto::attest::PROOF_VERIFICATION_IMPLEMENTED` — and it is the most\nimportant thing in it: a client reads it to decide whether it may hand over a\nuser's record keys at all. See the module documentation.","operationId":"registry","parameters":[{"name":"slug","in":"path","description":"The institution's organization slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The institution and its live keys, each with transparency-log and domain-attestation evidence","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistryResponse"}}}},"404":{"description":"No registered relying party under this slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/shared/advance":{"post":{"tags":["grants"],"summary":"`POST /shared/advance` — the institution says it wants to ingest.","description":"Stage two of the gate. It grants nothing and unlocks nothing: it marks the\ngrant advanced and raises a full-disclosure request on the candidate's queue,\nand the full bundle comes into existence only when their client seals it. The\nsealed keys for the full set do not exist before that moment — which is the\ndifference between this and a server that holds both bundles and decides\nwhich to hand over.\n\nIdempotent. A second ask is the same ask: `advanced_at` is set once and the\ncandidate is told once, so this cannot become a channel for nagging somebody.\n\nThe honest sentence belongs at this step and is in the notice the candidate\nreceives: once an institution pulls these fields, they have them. Future\nreads and future updates can be stopped at any time; what has already been\nsent cannot be taken back.","operationId":"advance","responses":{"200":{"description":"The candidate has been asked to release the full set. `full_bundle_sealed` says whether it exists yet; while it is `false` the keys exist nowhere.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Advanced"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired` or `unknown_grant`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"`card_required`: this institution has no payment method on file, which is refused on every plan but `contract` and before the first pull rather than before the first charge. Or `upgrade_required`: it is on the free plan and has used its included pull-throughs. Triage stays free and unlimited under both, and a candidate it has already advanced can still be re-asked for.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`relying_party_required`, `wrong_audience`, `revoked` — or `billing_suspended`, when this institution's billing is suspended. Advancing is the moment it takes a candidate in and therefore the moment it is charged; every preview it has already triaged and every disclosure already made to it are unaffected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[],"relying_party":[]}]}},"/shared/agent/draft-application":{"post":{"tags":["agent"],"summary":"`POST /shared/agent/draft-application` — a third-party agent drafts for the\nuser who granted it authority.","description":"`Authorization: Bearer <grant-token>`, not a session, and the same\nacceptance rule `POST /grants/verify` and `GET /shared/profile` apply: the\nsignature is checked, then the *row*, so a revoked grant stops working on\nits next use however good its signature still is. The grant must then carry\n[`REQUIRED_SCOPE`].\n\nWhose profile is drafted from — and, in live mode, whose key is spent — is\ntaken from the grant row. There is no user id in the path, the query or the\nbody, so a holder cannot aim this at anyone but the user who delegated to it.\n\n- 400 — a missing or empty `job_title`, `company` or `job_description`\n- 401 — no, malformed, unsigned, expired or unknown grant\n- 403 `revoked` — a genuine grant its owner withdrew\n- 403 `insufficient_scope` — a valid grant without `applications:submit`\n- 409 `no_llm_key` — live mode, and the *grant's user* has stored no key\n- 429 `draft_quota` — hosted mode, and the *grant's user's* daily allowance\n  is spent. A delegate spends the owner's allowance, exactly as it spends\n  the owner's key, and cannot supply either.\n- 502 `provider_*` — the provider refused, failed, or answered with nonsense\n- 200 — `{\"draft\", \"provider\", \"model\"?, \"on_behalf_of\"}`","operationId":"shared_draft","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftRequest"}}},"required":true},"responses":{"200":{"description":"The draft, what produced it, and the user it was made for. `on_behalf_of` comes from the grant row — the holder presented a token, not a user id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedDraftResponse"}}}},"400":{"description":"A missing, empty or oversized `job_title`, `company` or `job_description`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired` or `unknown_grant` in the `error` field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`revoked` — withdrawn by its owner — or `insufficient_scope`, a genuine grant that does not carry `applications:submit`. Reading a profile and acting in its owner's name are separate delegations, and one never implies the other.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"`no_llm_key`: this process is in live mode and the grant's user has stored no key. The delegate cannot supply one — the key spent is always the profile owner's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"`draft_quota`: this process is in hosted mode and the grant's user has spent today's allowance. A delegate spends the owner's allowance exactly as it spends the owner's key, and can supply neither.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"`provider_*`: the provider refused, failed, or answered with nonsense","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`unavailable`: the grant row could not be read, so there is no verdict. Never answered as a yes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[]}]}},"/shared/answers":{"put":{"tags":["answers"],"summary":"`PUT /shared/answers` — store an answer in the *grantor's* bank, on a grant\ncarrying [`WRITE_SCOPE`].","description":"The delegated twin of [`put_answer`], and the only route in this service that\nlets anybody but the owner write to the answer bank. Three things make it the\nsame act as the owner's rather than a wider one:\n\n* **Whose bank it is comes from the grant row**, resolved by\n  [`grants::authorize`], never from the request. There is no user id in the\n  body to get wrong and none to aim somewhere else.\n* **It goes through [`store_answer`]**, so the fingerprint, the type check\n  against the question already on file, and the ownership check on a named\n  document are the owner's own rules — a delegate cannot store an answer the\n  owner could not have stored.\n* **It is written down.** Every call leaves an `answer.written` row in the\n  owner's trail naming the grant, and bumps the delegation's counters through\n  [`grants::record_action`] — an act in somebody's name, not a view of their\n  record, which is why it is `record_action` and not `record_use`.\n\nThe audit detail carries the question's *fingerprint* and never the answer.\nA trail that quoted the value would be a second copy of the bank, kept in the\none table `DELETE /me/account` deliberately does not cascade.\n\n# What sealing took away from this route, stated plainly\n\nAn answer's value is now a record, and writing a record takes a key this\nservice has never held — so **a delegate can no longer teach the bank\nsomething new through this door**. What it can do is what [`store_answer`]\nlets it do: name a record that is already one of the owner's own, live, in\nscope [`READ_SCOPE`] and of type [`ANSWER_RECORD_TYPE`], and file it under a\nquestion's fingerprint. A record the owner's client has not written does not\nexist to be named.\n\nThat is a real narrowing, it is the intended product behaviour, and it is not\na gap this slice left by accident. The\nform-filler case the route was built for — an agent told something the bank\ndoes not have — needs the agent to seal into the owner's scope, which is what\na **capability** is for (section 4.3) and which has no write route yet. Until\none exists, an agent that learns something hands it to the person, and the\nperson's own client stores it. Section 8.5 is the same sentence about\n`autofill.rs`: the resolver moves to the client, and so does the learning.","operationId":"shared_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreAnswerRequest"}}},"required":true},"responses":{"200":{"description":"The stored answer, in the grantor's bank, in exactly the shape `PUT /me/answers` returns.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoredAnswer"}}}},"400":{"description":"The same refusals `PUT /me/answers` makes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired`, or `unknown_grant` in the `error` field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`revoked` — withdrawn by its owner — or `insufficient_scope`: a genuine grant that does not carry `answers:write`. `answers:read` does not imply it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[]}]}},"/shared/documents/{id}/content":{"get":{"tags":["documents"],"summary":"`GET /shared/documents/{id}/content` — a delegate downloads a document\nthrough a grant.","description":"`Authorization: Bearer <grant-token>`, not a session, and the same three\nchecks every grant-authenticated route makes: the signature, the row, and\nthen [`DOCUMENTS_SCOPE`]. Whose documents are reachable comes from the grant\n*row* — the id in the path selects among that user's documents and cannot\nreach anyone else's, so a delegate holding a grant from one person cannot\nenumerate a second person's files by guessing ids.\n\nThe disclosure is written to the owner's audit trail before the bytes are\nsent: which document, of what kind, under which grant. Never the filename\nand never anything from inside the file — a trail that copied out what it\nwas recording the disclosure of would double the problem it exists to solve.","operationId":"shared_content","parameters":[{"name":"id","in":"path","description":"The document's id, as it appears in the shared profile","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The stored bytes. The owner's trail gains a `document.shared` row and the grant's use count moves.","content":{"application/octet-stream":{"schema":{"type":"string"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired`, or `unknown_grant`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`relying_party_required` — the reader is not a registered institution; `wrong_audience`; `revoked`; or `insufficient_scope` — a genuine grant that is not a delegation of documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The granting user has no such document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[],"relying_party":[]}]}},"/shared/postings/{id}/apply":{"post":{"tags":["applications"],"summary":"`POST /shared/postings/{id}/apply` — an agent applies for the user who\ngranted it authority.","description":"`Authorization: Bearer <grant-token>`, not a session, and the same acceptance\nrule every other `/shared` route applies: the signature, then the *row*, then\nthe scope — which here is [`crate::agent::REQUIRED_SCOPE`], the scope that\nhas always meant \"act in my name\" and until now unlocked only drafting.\n\nWhose application this is comes from the grant row. There is no user id in\nthe path, the query or the body, so a holder cannot aim this at anybody but\nthe user who delegated to it — the same property `POST /shared/profile` and\nthe draft route have.\n\nThe screening questions are resolved here, at submission time, against the\nrecord as it stands now: an agent that drafted last week and submits today\nsubmits today's answers, which is the only honest thing for a record that has\nsince been edited.\n\nThe owner is told. An application sent in somebody's name while they were not\nlooking is precisely the event this service's notifications exist for, and it\ncarries the application id so the client can open what was sent.","operationId":"delegated_apply","parameters":[{"name":"id","in":"path","description":"The posting's id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DelegatedApplyRequest"}}},"required":true},"responses":{"201":{"description":"The application, the delegation it issued, and the screening questions split into the ones the auto-fill engine answered and the ones it could not — the same body the owner's own `POST /postings/{id}/apply` returns.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationSubmitted"}}}},"400":{"description":"`id` is not a UUID, or `draft` is longer than this service stores","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired` or `unknown_grant`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`revoked` — withdrawn by its owner — or `insufficient_scope`, a genuine grant that does not carry `applications:submit`. Reading somebody's record and applying in their name are separate delegations, and one never implies the other.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such posting, or it is not open","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The grant's owner has already applied to this posting","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[]}]}},"/shared/profile":{"get":{"tags":["profile"],"summary":"`GET /shared/profile` — a third party reads a profile through a grant.","description":"`Authorization: Bearer <grant-token>`, not a session. The token goes through\nexactly the checks `POST /grants/verify` applies — same function, so the two\ncannot drift — and then must carry [`REQUIRED_SCOPE`], through the same\nscope gate every grant-authenticated route uses.\n\nThe user whose profile is returned comes from the grant *row*, never from a\npath or query parameter. A caller therefore cannot ask for a profile; it can\nonly present a delegation and receive the one it was given.\n\nWhat comes back is assembled section by section from the scopes the grant\ncarries — see [`FullProfile::redacted_for_share`], which is the only place\nthat decides. A missing scope is never an error here — only `profile:read`\ndecides whether there is a read at all.\n\n**What it no longer carries is the headline of the route.** The narrative\ncore, the positions held, the qualifications and the skills are ciphertext\nsince `20260828000002_retire_plaintext_profile.sql`, so a delegate who wants\nthem takes the sealed key bundle route in [`crate::bundles`] and opens the\nrecords in their own environment. This document is what is left that the\nserver can still read on their behalf.\n\n- 401 `malformed` / `invalid_signature` / `expired` / `unknown_grant`\n- 403 `revoked` — genuine, but withdrawn by its owner\n- 403 `insufficient_scope` — a valid grant for something other than this","operationId":"shared","responses":{"200":{"description":"The sections of the record this service can still read, cut down to the ones this grant's scopes delegate. `profile:read` alone yields `phones`, `links` and the current address. `history:read` fills in `languages` and `driving_equipment`; `credentials:read` fills in `credentials` with their CDL detail; `preferences:read` fills in `availability` and `availability_shifts`; `identity:read` fills in the address history. An ungranted section is an empty list — or a null `availability` — never an error. References are withheld from every delegate whatever the grant carries. The four regulated islands — `work_eligibility`, `sensitive_id`, `eeo` and `consents` — and the two delegate-only sections — `documents` on `documents:read` and `answers` on `answers:read` — are *absent* from the document rather than empty unless the grant carries their scope, and every island that is disclosed is written to the owner's audit trail. `verifications` is empty and `verification_credentials` absent at every scope while a signed check note is the person's alone, which is this deployment's default; `verification_status` carries the one sentence a reader gets instead, and the person themselves is who to ask. The narrative core, the work history, the education and the skills are not in this document at any scope: they are ciphertext, and a delegate reads them by opening a sealed bundle. Whose profile it is comes from the grant row — a caller cannot ask for a profile, only present a delegation and receive the one it was given.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FullProfile"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired`, or `unknown_grant` in the `error` field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`relying_party_required` — no institution credential, or one belonging to an organization that is not a registered relying party; `wrong_audience` — a registered institution presenting a delegation that was issued to a different one; `revoked` — withdrawn by its owner; or `insufficient_scope`, a genuine grant for something other than this","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"`unavailable`: the grant row could not be read, so there is no verdict. Never answered as a yes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[],"relying_party":[]}]}},"/shared/records":{"get":{"tags":["grants"],"summary":"`GET /shared/records` — the encrypted shared read.","description":"The institution presents the two credentials every shared read now takes —\nits own, in `X-Verigrant-Relying-Party`, and the grant token in\n`Authorization` — and receives ciphertext plus the keys sealed to it. It\ndecrypts in its own environment, with its own private key, which this\nservice does not hold and has no route that would need.\n\n`vg-rp` is the reference implementation of the other side, and\n`tests/capability_grants.rs` drives it end to end: what the browser sealed is\nwhat the institution opens, through the real routes, with the server holding\nonly what it holds here.\n\nWhat comes back is exactly the bundle of the requested kind: a preview\nreturns the previewed subset and nothing else, because the full set's keys do\nnot exist until the user seals them. See [`advance`].","operationId":"shared_records","parameters":[{"name":"kind","in":"query","description":"`preview` (the default) or `full`.","required":false,"schema":{"type":"string"},"example":"preview"}],"responses":{"200":{"description":"The sealed bundle and the ciphertext it opens. Every component of each record's associated data travels with it, so a reader that has never seen the row can tell an honest one from a substituted one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedRecords"}}}},"400":{"description":"A `kind` that is not `preview` or `full`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`malformed`, `invalid_signature`, `expired` or `unknown_grant`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"`card_required` or `upgrade_required`, on `kind=full` only. The first: this institution has no payment method on file, which is refused on every plan but `contract` and before the first pull rather than before the first charge. The second: it is on the free plan and has used its included pull-throughs. A full set it has already taken is still readable under both — these gate new ingestions, not old ones.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"`relying_party_required`, `wrong_audience`, or `revoked` — a withdrawn delegation stops the next read, which is what revocation can do and the whole of what it can do. On `kind=full` also `billing_suspended`: the full set is the ingestion an institution is charged for, so a suspended one cannot take it. A preview is free and is never refused on those grounds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"`bundle_not_sealed`: no bundle of this kind exists. For `full` that is the two-stage gate working — the keys have not been sealed, so they exist nowhere. `POST /shared/advance` asks the user to seal them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"grant":[],"relying_party":[]}]}},"/stripe/webhook":{"post":{"tags":["billing"],"summary":"`POST /stripe/webhook` — Stripe's account of what happened, signed.","description":"Reached from the internet at `/api/stripe/webhook`: nginx proxies `/api/` to\nthis service with the prefix stripped, so the path configured in Stripe's\ndashboard has an `/api` on the front that this router never sees. That is the\nsame arrangement every other route in this service is reached under.\n\nUnauthenticated in the session sense and *not* unauthenticated: the\n`Stripe-Signature` header is the credential and it is checked before anything\nelse happens, against the raw bytes exactly as sent — which is why this takes\n[`Bytes`] and not a parsed value. Nothing that has not verified is allowed to\nreach the database; the same rule [`crate::webhooks`] is built on.\n\n- 200 — the signature checked out. `queued` if it is an event this service\n  acts on, `ignored` if it is not. Both are a success, because both are:\n  Stripe delivered, we authenticated it, and there is nothing for it to retry.\n- 400 — the signature header is missing or malformed, or the body is not a\n  JSON object\n- 401 — the signature did not match, or the delivery is outside the\n  five-minute window. One answer for both, deliberately: a caller that cannot\n  sign has no business learning which half it got wrong.\n- 501 — no `STRIPE_WEBHOOK_SECRET` in this deployment. A `501` rather than a\n  `401` because the caller did nothing wrong.","operationId":"webhook","requestBody":{"description":"A Stripe event object. Signed — see the `Stripe-Signature` header. Reached from the internet at `/api/stripe/webhook`.","content":{"text/plain":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"The signature checked out. `queued` for the four event types this service acts on, `ignored` for every other one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Received"}}}},"400":{"description":"A missing or malformed `Stripe-Signature`, or a body that is not a JSON object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`invalid_signature`: the signature did not match, or the delivery is outside the five-minute replay window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"`webhook_not_configured`: no `STRIPE_WEBHOOK_SECRET` in this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/users":{"post":{"tags":["users"],"summary":"`POST /users` — hash the password, insert the user, place them in the tree,\nrecord the creation in the audit log, and mail a link that proves the\naddress. The first four share a transaction, so the custody trail cannot lose\nan entry and the account cannot exist without a place — see\n[`crate::tree::place_account`].","description":"# Why the message is sent and its failure is not\n\nThe account is the thing this route exists to create, and it is created\nwhether or not a relay answers. A deployment with no outbound mail at all —\nwhich is every deployment until an operator sets `VERIGRANT_SMTP_HOST` — logs\nthe message it would have sent and answers `201` exactly as before; a\ndeployment whose relay is having a bad afternoon does the same and logs why.\nThe remedy for a message that did not arrive is\n`POST /auth/resend-verification`, which the account holder can reach from the\nmoment they have a session, and the cost of the alternative is an account\nthat could not be created because somebody else's mail server was down.\n\nThe send is deliberately the last thing here and is bounded by\n[`crate::mail::IN_BAND_DEADLINE`], which is shorter than the edge deadline in\n[`crate::REQUEST_TIMEOUT`]. That is not tidiness: a request cut off by the\nedge has its future dropped, which rolls\n[`crate::db::unit_of_work`]'s transaction back, so a relay that accepted the\nconnection and then said nothing would turn this route into an account that\nwas never created.","operationId":"create_user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"201":{"description":"The account was created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUser"}}}},"400":{"description":"The email or the password was rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That email is already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled: this is one of the two unauthenticated writes, limited per remote address. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/users/{id}":{"get":{"tags":["users"],"summary":"`GET /users/{id}` — the public fields of one user, to a caller holding a\nsession.","description":"**This route used to be open, and that was a user-enumeration hole.** An\nunauthenticated caller could walk ids and learn, for each one, whether an\naccount existed and what its email address was. Three things close it, and\nall three are needed:\n\n* **A session is required.** Enumerating now costs an account, which is a\n  thing that can be rate-limited, audited and disabled — as opposed to a\n  stranger with a UUID generator, who is none of those.\n* **It is throttled per client address**, on a budget of its own rather than\n  the one `POST /auth/login` and `POST /users` share, so that a caller\n  grinding the directory cannot spend everybody else's login allowance and a\n  burst of logins cannot lock out lookups. See [`crate::ratelimit`], which\n  also explains why the client address is resolved through a trusted proxy\n  rather than read off the socket.\n* **The response is [`DirectoryUser`]**, so what a successful walk yields is\n  a display name somebody chose to publish rather than the address they log\n  in with.\n\nWhat is deliberately *not* hidden is existence: a live id is a 200 and an\nunknown one is a 404. Collapsing those would mean a session-holder could not\nresolve an id they legitimately hold, which is the entire purpose of the\nroute; the enumeration risk is answered by the cost and the payload above,\nnot by pretending every id is unknown.","operationId":"get_user","parameters":[{"name":"id","in":"path","description":"The user's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The user's public fields: the id, and the display name they chose. Never the email address — see `DirectoryUser`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUser"}}}},"400":{"description":"`id` is not a UUID — a malformed path rather than a lookup that came up empty","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Throttled per client address, on a budget of its own. `Retry-After` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/vera/applications":{"post":{"tags":["vera"],"summary":"`POST /vera/applications` — an agent applies in the user's name.","description":"The only thing a Vera ID in **review mode** can do, and the first thing an\nauto mode one does. It writes a row and notifies the user. It discloses\nnothing: the institution is not told, no key moves, and an ignored request\nstays ignored forever rather than ripening into a yes.\n\nOne unanswered application per id and institution, enforced by a partial\nunique index rather than by this handler alone. Without it an agent in a loop\ncould turn somebody's approval queue into a denial of attention, and the\nqueue is the product.\n\n# Why an auto mode application is signed and a review mode one is not\n\nBecause they are not the same act. A review mode application is a question\nput to a human, and the worst a stranger holding the id can do with it is\nask. An auto mode application is the *first half of an issuance*: the row it\nwrites is exactly what `POST /agent/grants` looks for, and no human sees it\nbefore the records are sealed. A stranger who could file one under a leaked\nauto mode id would choose the institution, and the agent's own next call — or\na race against it — would seal a real disclosure to an org the attacker\nregistered.\n\nSo this route demands the same two credentials the `/agent/*` routes do as\nsoon as the id turns out to be in auto mode: [`capability::authenticate_agent`],\nwhich re-establishes the holder and then verifies an Ed25519 signature over\nthis exact request under the `agent_sign_pub` the user's mandate authorises.\nAn auto mode id with no signature, a signature by another key, or a signature\nover a different body is a `401` and no row.\n\nIt follows that an auto mode id which has never been paired cannot apply\neither, because there is no capability row and therefore no key any signature\ncould verify under. That is the correct answer rather than an edge case: an\nunpaired auto mode id is an id whose auto-issuance nobody has authorised.","operationId":"apply","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyRequest"}}},"required":true},"responses":{"201":{"description":"The application, pending in the user's queue. Nothing has been disclosed. `auto_issue` says whether this id may issue without a human.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Applied"}}}},"400":{"description":"`rp` is not a registered institution, or `scopes` is empty or carries a scope this service does not enforce","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"No usable Vera ID, or — for an **auto mode** id — no request signature, or one that does not verify against the key the mandate authorises","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An auto mode id whose capability is revoked, unpaired, or outside the window the user signed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This id already has an unanswered application to this institution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"vera_id":[]}]}},"/vera/applications/{id}":{"get":{"tags":["vera"],"summary":"`GET /vera/applications/{id}` — what happened to an application.","description":"The agent's way of learning that a human answered, without any way to hurry\nthem. It carries the status and, once approved, the grant's id — and never\nthe grant's **token**, which was handed to the user's client once and is\ntheirs to deliver.","operationId":"application","parameters":[{"name":"id","in":"path","description":"The application request's id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The application, as its own agent may see it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationRequest"}}}},"401":{"description":"No usable Vera ID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such application of this id's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"vera_id":[]}]}},"/webhooks/payments/{provider}":{"post":{"tags":["billing"],"summary":"`POST /webhooks/payments/{provider}` — the payment gateway's account of what\nbecame of a charge, signed.","description":"The same scheme, the same window, the same `202`, and deliberately so: a\nsecond signature construction in the same service would be a second thing to\nget subtly wrong, and this one has been argued over. What differs is three\nthings and each has a reason:\n\n* The secret is `VERIGRANT_WEBHOOK_SECRET_PAYMENTS` rather than one per\n  provider — see [`PAYMENTS_SECRET_NAME`].\n* `{provider}` is a payment gateway's name rather than a verification\n  vendor's, checked against the ones this build can construct.\n* The queued job is [`PAYMENT_WEBHOOK`], applied by\n  [`crate::billing::apply_payment`], which finds the invoice by the gateway's\n  own charge id and moves it — and the institution's standing with it.\n\nWhat the callback carries is [`crate::billing::PaymentEvent`], whose shape is\npart of the assumed contract documented in [`crate::payments`] and is\ntherefore one edit away from whatever the real gateway sends.\n\n- 202 — the signature checked out and the result is queued\n- 400 — the signature or timestamp header is missing or malformed, or the\n  body is not a JSON object\n- 401 — the signature did not match, or the delivery is outside the replay\n  window. One answer for both, for the reason [`verification`] gives.\n- 404 — `{provider}` is not a payment gateway this build knows\n- 501 — a known gateway with no shared secret configured in this deployment","operationId":"payment","parameters":[{"name":"provider","in":"path","description":"The payment gateway's name, e.g. `http`","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"What became of one charge. Signed — see the `X-Verigrant-Signature` and `X-Verigrant-Timestamp` headers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentEvent"}}},"required":true},"responses":{"202":{"description":"The signature checked out and the result is queued. Deliberately not `200`: the result has been accepted, not yet applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"}}}},"400":{"description":"A missing or malformed signature header, or a body that is not a JSON object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`invalid_signature`: the signature did not match, or the delivery is outside the five-minute replay window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a payment gateway this build knows","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"`webhook_not_configured`: a known gateway, with no `VERIGRANT_WEBHOOK_SECRET_PAYMENTS` set in this deployment. A `501` rather than a `401` because the caller did nothing wrong.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/verifications/{provider}":{"post":{"tags":["verifications"],"summary":"`POST /webhooks/verifications/{provider}` — a vendor's verdict, signed.","description":"Unauthenticated in the session sense and *not* unauthenticated: the HMAC is\nthe credential, and it is checked before anything else happens. See the\nmodule docs for the scheme, the replay window and why the work is queued\nrather than done.\n\n- 202 — the signature checked out and the result is queued\n- 400 — the signature or timestamp header is missing or malformed, or the\n  body is not JSON\n- 401 — the signature did not match, or the delivery is outside the replay\n  window. One answer for both, deliberately: a caller that cannot sign has\n  no business learning which half it got wrong.\n- 404 — `{provider}` is not a vendor this service knows\n- 501 — a known vendor with no secret configured in this deployment","operationId":"verification","parameters":[{"name":"provider","in":"path","description":"`persona`, `truv`, `argyle` or `checkr`","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"The vendor's account of one event. Signed — see the `X-Verigrant-Signature` and `X-Verigrant-Timestamp` headers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}},"required":true},"responses":{"202":{"description":"The signature checked out and the result is queued. Deliberately not `200`: the verdict has been accepted, not yet applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"}}}},"400":{"description":"A missing or malformed signature header, or a body that is not a JSON object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"`invalid_signature`: the signature did not match, or the delivery is outside the five-minute replay window. One answer for both — a caller that cannot sign does not get to learn which half it got wrong.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not a vendor this service knows","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"`webhook_not_configured`: a known vendor, with no shared secret set in this deployment. A `501` rather than a `401` because the caller did nothing wrong.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"schemas":{"Accepted":{"type":"object","description":"What the route answers with once a delivery is authenticated and queued.","required":["status","job_id"],"properties":{"job_id":{"type":"string","format":"uuid","description":"The `job_queue` row this delivery became. Worth returning: it is what an\noperator quotes when a vendor asks what happened to a callback, and it\ndiscloses nothing — it names a queue row, not a user."},"status":{"type":"string","description":"Always `queued`. The result has been accepted for processing and has\ndeliberately not been applied yet — see the module docs.","example":"queued"}}},"AccessRequest":{"type":"object","description":"One access request, as either side reads it.\n\nThe same shape for both, deliberately: what the employer asked and what the\nseeker answered is one fact, and a company seeing a different summary of it\nfrom the person who decided it is how two parties end up disagreeing about\nwhat was agreed.\n\nWhat is *not* here is the seeker. A request names the person only by the\napplication it came through, so the employer's copy of this row discloses\nnothing about them that they had not already sent.","required":["id","org_id","organization","scopes","status","created_at"],"properties":{"application_id":{"type":["string","null"],"format":"uuid","description":"The application this was asked through, or `null` if it has since gone."},"created_at":{"type":"string","format":"date-time"},"decided_at":{"type":["string","null"],"format":"date-time","description":"When the seeker answered, or `null` while they have not."},"grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation approving this minted. `null` while pending and forever on\na decline."},"id":{"type":"string","format":"uuid"},"message":{"type":["string","null"],"description":"The recruiter's sentence about why, if they wrote one."},"org_id":{"type":"string","format":"uuid"},"organization":{"type":"string","description":"The company's name, joined in so the seeker's list is readable without a\nsecond call. The employer's own copy carries their own name, which is\nharmless and keeps one shape.","example":"Aperture Freight"},"scopes":{"type":"array","items":{"type":"string"},"description":"What is being asked for, in the symbolic scope names a grant carries.","example":["profile:read","documents:read"]},"status":{"type":"string","description":"`pending`, `approved` or `declined`.","example":"pending"}}},"AddNoteRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/applications/{application_id}/notes`.","required":["body"],"properties":{"body":{"type":"string","description":"What the recruiter wants to remember. Free text, required, at most\n10 000 characters.","example":"Phoned Tuesday, no answer. Try the Hull depot number."}}},"AddressEntry":{"type":"object","required":["id","address_type","is_current","created_at"],"properties":{"address_type":{"type":"string","description":"One of `current`, `prior`, `mailing`."},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"from_date":{"type":["string","null"],"format":"date","description":"When the person moved in; part of the address *history* a regulated\napplication asks for."},"id":{"type":"string","format":"uuid"},"is_current":{"type":"boolean"},"line1":{"type":["string","null"]},"line2":{"type":["string","null"]},"postal_code":{"type":["string","null"]},"region":{"type":["string","null"],"description":"State, province or county, depending on the country."},"to_date":{"type":["string","null"],"format":"date"}}},"AdminUserCounts":{"type":"object","description":"The per-account counters behind [`AdminUserDetail`].","required":["active_sessions","grants_total","grants_active","verifications","applications","documents","org_memberships"],"properties":{"active_sessions":{"type":"integer","format":"int64","description":"Sessions that are neither revoked nor expired — how many places this\naccount is currently logged in from."},"applications":{"type":"integer","format":"int64"},"documents":{"type":"integer","format":"int64"},"grants_active":{"type":"integer","format":"int64","description":"The ones that would be honoured on a request right now."},"grants_total":{"type":"integer","format":"int64","description":"Every delegation this person has ever issued."},"org_memberships":{"type":"integer","format":"int64","description":"How many organizations this account belongs to. An employer is a user\nwho belongs to one; this is the field that says which accounts are."},"verifications":{"type":"integer","format":"int64"}}},"AdminUserDetail":{"type":"object","description":"One account and the *shape* of what hangs off it.\n\nThe counts are the point of the detail route, and the line they walk is the\none this module is about: how much of a record exists is an operational fact\n— it explains a slow export, a large bill, a support ticket about a missing\ndocument — and what is *in* it is not an administrator's business. So every\nnumber here is a `count(*)` and none of them is accompanied by a row.\n\nTwo nested objects rather than one flattened one, so that a client reading\nthis and a client reading `GET /admin/users` are looking at the same\n[`AdminUserSummary`] under the same key instead of at two shapes that happen\nto share field names.","required":["user","counts"],"properties":{"counts":{"$ref":"#/components/schemas/AdminUserCounts"},"user":{"$ref":"#/components/schemas/AdminUserSummary"}}},"AdminUserSummary":{"type":"object","description":"One account, as an administrator sees it.\n\nEverything on it is *about the account* and nothing is *from the record*:\nthe id, the address it logs in with, the name it chose, whether it may act,\nwhether it administers the service, and when it was created and last\nchanged. There is no field here that a grant would otherwise be needed for,\nand there is deliberately no `password_hash` — a shape that carried one would\neventually be logged.","required":["id","email","status","is_admin","role","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"display_name":{"type":["string","null"]},"email":{"type":"string","example":"ada@example.com"},"id":{"type":"string","format":"uuid"},"is_admin":{"type":"boolean","description":"Whether this account administers the platform. The same fact as `role`,\nwhich the schema guarantees cannot disagree with it."},"role":{"type":"string","description":"`user` or `admin`. The platform role — never an organization's.","example":"user"},"status":{"type":"string","description":"`active` or `suspended`. A suspended account cannot log in and its live\nsessions stop working on their very next request.","example":"active"},"updated_at":{"type":"string","format":"date-time"}}},"Advanced":{"type":"object","description":"The answer to `POST /shared/advance`.","required":["grant_id","advanced_at","full_bundle_sealed"],"properties":{"advanced_at":{"type":"string","format":"date-time","description":"When this candidate was advanced. Set once and never moved: a second ask\nis the same ask."},"full_bundle_sealed":{"type":"boolean","description":"Whether the full bundle exists yet. `false` means the request is with the\nuser and the sealed keys do not exist anywhere — see the module\ndocumentation on why that is the point rather than a delay."},"grant_id":{"type":"string","format":"uuid"}}},"AgentInboxItem":{"type":"object","description":"One envelope as the account's **own** agent reads it.\n\nRead the field list as the answer to a question worth asking out loud: what\ncan an agent acting for this person be told about their inbox without being\nhanded anything it cannot open?\n\nThe envelope, which the operator already holds, and one further fact:\n[`AgentInboxItem::kept_record_id`], which says whether the owner has re-keyed\nthis item into their record and therefore whether there is a ciphertext the\nagent's own sealed scope key opens. There is deliberately **no payload here**.\nAn inbound payload is a sealed box to the account's X25519 key, derived from\nthe master key, and an agent that could open one would be an agent holding the\nmaster key rather than the preset it was paired for. So the content reaches an\nagent by exactly one route: the owner opens the item in their own browser,\nkeeps it, and from then on `GET /agent/records` serves it like any other\nrecord. That is the same re-keying every other reader goes through, and it\nkeeps \"the agent reads on the user's own key\" true rather than nearly true.","required":["id","doc_type","schema","schema_version","byte_class","key_epoch","payload_hash","provenance","received_at"],"properties":{"byte_class":{"type":"integer","format":"int32"},"doc_type":{"type":"string"},"id":{"type":"string","format":"uuid"},"kept_record_id":{"type":["string","null"],"format":"uuid","description":"The record this item was kept into, which is the item's own id, or `null`\nwhere the owner has not kept it.\n\nThe id is the same value on purpose: a kept item is written under the id\nof the envelope it came from, so an agent handed this list can ask\n`GET /agent/records` for a record it can name rather than guessing which\nof them is which."},"key_epoch":{"type":"integer","format":"int32","description":"Which generation of the owner's key these bytes were sealed to. On this\nshape as well as on the owner's because it is part of the envelope the\noperator already holds, and because an agent triaging a feed should not\nhave a smaller picture of what it cannot open than of what it can."},"payload_hash":{"type":"string"},"provenance":{"$ref":"#/components/schemas/Provenance"},"read_at":{"type":["string","null"],"format":"date-time"},"received_at":{"type":"string","format":"date-time"},"schema":{"type":"string"},"schema_version":{"type":"string"}}},"AgentMetrics":{"type":"object","required":["runs","suggestions","hosted_drafts","hosted_tokens_in","hosted_tokens_out"],"properties":{"hosted_drafts":{"type":"integer","format":"int64","description":"Drafts this platform has paid for, ever: the sum of `agent_usage.drafts`\nacross every account and every day.\n\nThe first figure on this page that is about **money**, which is why it is\nhere rather than left to be derived from the audit trail. In `mock`,\n`live` and `claude-cli` mode it is zero and stays zero, because nobody at\nVerigrant is billed for a draft in any of them; in `hosted` mode it is\nwhat the deployment has spent, and the two token figures beside it are\nthe units the provider bills in. Drafts paid for with a user's own key\nare deliberately not counted — they cost this platform nothing. See\n[`crate::agent::usage`]."},"hosted_tokens_in":{"type":"integer","format":"int64","description":"Input tokens the provider reported for those drafts. Zero where it\nreported none, which is every draft the mock and the CLI produced."},"hosted_tokens_out":{"type":"integer","format":"int64","description":"Output tokens, likewise."},"runs":{"type":"integer","format":"int64","description":"`agent.run` jobs ever enqueued — how often somebody asked the loop to\nwork."},"suggestions":{"type":"integer","format":"int64","description":"The proposals those runs produced and have not had replaced."}}},"AgentPending":{"type":"object","description":"One pending application, as the agent that made it reads it.","required":["request_id","org_id","org_slug","org_domain","scopes","kind","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"kind":{"type":"string","example":"preview"},"org_domain":{"type":"string","description":"Where to fetch the institution's own attestation from — **directly**,\nover its own TLS, with this service not in the path. An agent that skips\nthat check is a hole straight through the architecture.","example":"acme-university.edu"},"org_id":{"type":"string","format":"uuid"},"org_slug":{"type":"string","example":"acme-university"},"reference":{"type":["string","null"]},"request_id":{"type":"string","format":"uuid"},"scopes":{"type":"array","items":{"type":"string"}}}},"AgentRecord":{"type":"object","description":"One ciphertext row, as the agent receives it: exactly what\n`GET /me/records` hands the owner's own client, because the agent opens it\nwith a scope key derived from the same tree.","required":["id","scope","record_type","record_version","write_epoch","byte_class","ciphertext","ct_nonce","wrapped_dek","dek_nonce","created_at","updated_at"],"properties":{"byte_class":{"type":"integer","format":"int32"},"ciphertext":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}},"created_at":{"type":"string","format":"date-time"},"ct_nonce":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}},"dek_nonce":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}},"id":{"type":"string","format":"uuid"},"record_type":{"type":"string"},"record_version":{"type":"integer","format":"int64"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"wrapped_dek":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}},"write_epoch":{"type":"integer","format":"int32"}}},"Allowance":{"type":"object","description":"What an institution has used of its included allowance, for the screen that\nreports it.\n\nThe same count the gate decides on — `count(*)` over [`BILLABLE_KINDS`],\nlifetime — so the number an owner reads and the number that refuses them are\none number. A usage screen that computed an allowance differently from the\ngate would be a screen that says \"7 of 10 used\" on the request that answers\n`402`.","required":["included","used","billed","gated"],"properties":{"billed":{"type":"integer","format":"int64","description":"Of those, the ones already drawn onto an invoice.\n\nHere so that the amount on the current period's usage can be\nreconstructed by whoever is reading it: the included pulls are counted\nover the institution's whole history, so what this period gets free is\n`included - billed`, capped at what this period contains. A screen that\ncould not see this number would have to take [`Usage::free_pulls`] on\ntrust.","example":2},"gated":{"type":"boolean","description":"Whether running out refuses the next pull-through. See\n[`Billing::gated_by_allowance`]."},"included":{"type":"integer","format":"int64","description":"`org_billing.included_free_pulls`: ten on `free` and `per_pull`, forty on\n`platform`.","example":10},"remaining":{"type":["integer","null"],"format":"int64","description":"What is left of the included pulls, or `null` on `contract`, whose pulls\nare unlimited *and* priced at nothing so that an allowance would mean\nnothing either.\n\nOn `free` this is the number that, at zero, refuses the next\npull-through — see `gated`. On `per_pull` and `platform` it is the number\nthat, at zero, starts charging the unit price. Both are \"how many more\nare included\", which is the question an owner is asking.","example":7},"used":{"type":"integer","format":"int64","description":"Billable pull-throughs recorded for this institution, ever. Previews are\nnot in it, on any plan.","example":3}}},"ApiKeySummary":{"type":"object","description":"An API key as its organization sees it afterwards: everything except the key.","required":["id","label","created_at","use_count"],"properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"last_used_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"},"use_count":{"type":"integer","format":"int64"}}},"Applicant":{"type":"object","description":"One application in an employer's pipeline.\n\nWhat is *not* here is the point: no name, no email address, no field of the\napplicant's profile. See this module's documentation — the employer reads the\nperson by following the delegation at [`Applicant::profile_path`], which is\naudited into the applicant's own trail. A list that named candidates would be\na disclosure with no record of itself.","required":["application_id","stage","answers","created_at","updated_at","note_count","profile_path"],"properties":{"answers":{"type":"array","items":{"type":"object"},"description":"What the auto-fill engine made of this posting's screening questions at\nthe moment of applying — one entry per question, in the order the posting\nasked them. A record of what was *submitted*, not something recomputed\nfrom a profile that has since changed."},"application_id":{"type":"string","format":"uuid","description":"The id every other route in this module takes."},"created_at":{"type":"string","format":"date-time"},"delegation":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Delegation","description":"The delegation this application issued, as it stands right now.\n\n`null` only if the grant row is gone, which nothing can produce while the\napplication is here — both cascade from the same account. A `null` here\nmeans the profile read would answer `404`."}]},"note_count":{"type":"integer","format":"int64","description":"How many internal notes this organization holds on this application.","example":2},"profile_path":{"type":"string","description":"**How to read this applicant.**\n`GET /orgs/{id}/applications/{application_id}/profile`, spelled out so a\nclient does not have to assemble it — the route that follows the\ndelegation, cuts the document to the grant's scopes, and records the read\nin the applicant's trail.","example":"/orgs/0192.../applications/0193.../profile"},"stage":{"type":"string","description":"One of [`crate::applications::APPLICATION_STAGES`].","example":"screening"},"submitted_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":"string","format":"date-time","description":"When the row last changed, which for an application in a pipeline means\nwhen its stage last moved."}}},"ApplicationRecord":{"type":"object","description":"One application this person sent.","required":["id","company","status","answers","stage","created_at","updated_at"],"properties":{"answers":{"type":"array","items":{"type":"object"},"description":"What the auto-fill engine made of the posting's screening questions at\nthe moment of applying — one entry per question, in the order the posting\nasked them. Stored rather than recomputed, because it is a record of what\nwas submitted. An empty array for an application recorded by hand."},"ats_vendor":{"type":["string","null"],"description":"The applicant tracking system it went through, lower-cased. Free text:\nnew ones appear, and refusing a true answer because it was not in a list\nwritten in 2026 would be worse than storing it.","example":"greenhouse"},"company":{"type":"string","example":"Aperture Robotics"},"created_at":{"type":"string","format":"date-time"},"draft":{"type":["string","null"],"description":"The covering letter this application was sent with, or `null` where there\nwas none — which is every application recorded by hand and every one made\nthrough `POST /postings/{id}/apply`, the route that takes no body. An\napplication submitted from an `agent_suggestion` carries the draft the\nagent wrote and the applicant read before authorising it."},"grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation issued when this application was made. It is an ordinary\ngrant: it is listed by `GET /me/grants`, and `DELETE /me/grants/{id}`\nwithdraws it, after which the employer's next read of the profile is\nrefused."},"id":{"type":"string","format":"uuid"},"job_title":{"type":["string","null"]},"job_url":{"type":["string","null"]},"org_id":{"type":["string","null"],"format":"uuid","description":"The employer, on the same terms as `posting_id`."},"posting_id":{"type":["string","null"],"format":"uuid","description":"The vacancy this answered, for an application made through\n`POST /postings/{id}/apply`. `null` for one recorded by hand, and `null`\nagain if the employer later deletes the posting — the record of having\napplied is the applicant's and does not go with it."},"stage":{"type":"string","description":"One of [`APPLICATION_STAGES`]. **What the employer has done** — written\nas `applied` here and moved from there only by\n`PATCH /orgs/{id}/applications/{application_id}`, which notifies the\napplicant each time it changes. See [`crate::pipeline`]."},"status":{"type":"string","description":"One of `draft`, `submitted`, `acknowledged`, `interviewing`, `offer`,\n`hired`, `rejected`, `withdrawn`. **What the applicant says** — see\n[`APPLICATION_STAGES`] for the employer's half."},"submit_grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation that *permitted* this submission, where one did. Not to be\nconfused with `grant_id`, which is the delegation this submission\n*issued*: that one goes to the employer, this one is what an agent held\nin order to act in the applicant's name at all. `null` for an application\nthe person made in their own session, which needs no delegation because\nthere is nobody in the middle."},"submitted_at":{"type":["string","null"],"format":"date-time","description":"Null while the application is a draft."},"updated_at":{"type":"string","format":"date-time"}}},"ApplicationRequest":{"type":"object","description":"One application an agent made in the user's name.","required":["id","org_id","organization","org_slug","org_domain","scopes","kind","source","status","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"decided_at":{"type":["string","null"],"format":"date-time"},"grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation that answered it. `null` while pending and forever on a\ndecline."},"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"`preview` or `full`.","example":"preview"},"note":{"type":["string","null"]},"org_domain":{"type":"string","example":"acme-university.edu"},"org_id":{"type":"string","format":"uuid"},"org_slug":{"type":"string","example":"acme-university"},"organization":{"type":"string","description":"The institution's name and slug, joined in so the queue is readable\nwithout a second call — and its domain, because the client that approves\nthis has to fetch the attestation from that host itself.","example":"Acme University"},"reference":{"type":["string","null"],"description":"The agent's own reference for what it applied to. Free text, shown to\nthe user, never interpreted."},"scopes":{"type":"array","items":{"type":"string"},"example":["profile:read"]},"source":{"type":"string","description":"`review` or `auto`: which mode the agent applied in.","example":"review"},"status":{"type":"string","description":"`pending`, `approved` or `declined`.","example":"pending"}}},"ApplicationSubmitted":{"type":"object","description":"What one application came back with.\n\n`autofilled` and `gaps` are a partition of `application.answers` — the same\nentries, sorted into the two piles a client does something different with.\nThey are repeated rather than left to be filtered on `source` because the\nthing a candidate's client does next is put the gaps to the user, and a\nresponse that made it derive that list would be a response that let two\nclients derive it differently. The record keeps them together and in the order\nthe posting asked them, which is the order they were submitted in.","required":["application","delegated","autofilled","gaps"],"properties":{"application":{"$ref":"#/components/schemas/ApplicationRecord","description":"The row, as `GET /me/applications` will show it from now on."},"autofilled":{"type":"array","items":{"$ref":"#/components/schemas/FilledAnswer"},"description":"The questions the engine answered, from the profile or from the answer\nbank."},"delegated":{"$ref":"#/components/schemas/Delegated","description":"What the delegation this application issued actually carries. Echoed back\nso a client can show the applicant what they just handed over without a\nsecond call to `GET /me/grants`."},"gaps":{"type":"array","items":{"$ref":"#/components/schemas/FilledAnswer"},"description":"The questions it could not. **These are the ones to put to the user**, and\nwhat they say goes to `POST /me/autofill/learn` — after which the next\nposting to ask resolves them from the bank."}}},"Applied":{"type":"object","description":"The answer to `POST /vera/applications`.","required":["request_id","status","source","org_id","org_slug","org_domain","scopes","kind","auto_issue"],"properties":{"auto_issue":{"type":"boolean","description":"Whether this id may issue without a human. `false` in review mode, said\non the wire so an agent knows to wait rather than to poll."},"kind":{"type":"string","example":"preview"},"org_domain":{"type":"string","description":"Where the institution's own attestation lives. An agent about to seal\nmust fetch it from here **directly**, not through this service — see\nsection 3.4 and `web::client::grant::verify_key`.","example":"acme-university.edu"},"org_id":{"type":"string","format":"uuid"},"org_slug":{"type":"string","example":"acme-university"},"request_id":{"type":"string","format":"uuid"},"scopes":{"type":"array","items":{"type":"string"}},"source":{"type":"string","example":"review"},"status":{"type":"string","description":"`pending` in both cases. In auto mode the agent then issues against it\nimmediately through `POST /agent/grants`; in review mode it waits for a\nhuman.","example":"pending"}}},"ApplyRequest":{"type":"object","description":"Request body for `POST /vera/applications`.","required":["rp","scopes"],"properties":{"kind":{"type":["string","null"],"description":"`preview` (the default) or `full`.","example":"preview"},"note":{"type":["string","null"],"description":"One sentence for the person whose queue this lands in."},"reference":{"type":["string","null"],"description":"The agent's own reference for what it applied to: a posting id, a URL, a\njob title.","example":"Night shift supervisor, req 4417"},"rp":{"type":"string","description":"The registered institution being applied to, by slug.","example":"acme-university"},"scopes":{"type":"array","items":{"type":"string"},"description":"What is being asked for, in the symbolic scope names a grant carries.","example":["profile:read"]}}},"ApproveRequest":{"type":"object","description":"Request body for `POST /me/application-requests/{id}/approve`.","required":["grant_id"],"properties":{"grant_id":{"type":"string","format":"uuid","description":"The delegation the caller's client issued and sealed a bundle under."}}},"ApprovedRequest":{"allOf":[{"$ref":"#/components/schemas/AccessRequest"},{"type":"object","required":["org_slug","relying_party_registered","delegated"],"properties":{"delegated":{"$ref":"#/components/schemas/Delegated","description":"The delegation this approval minted, and everything the seeker's client\nneeds in order to seal a bundle under it: the grant, the scopes asked\nfor, when it lapses, and where it goes."},"org_slug":{"type":"string","description":"The institution that asked, by registry slug — the handle\n`GET /rp/{slug}/keys` is keyed on and therefore the only one a client can\nseal to.\n\nBeside the request rather than only inside `delegated` because this is\nwhere a client reading a decided request looks for it: the row it already\nholds names the organization by *name*, and a name is not something to\nseal a record to. See [`Delegated::audience`], which is the same value\nfor the same reason.","example":"acme-university"},"relying_party_registered":{"type":"boolean","description":"Whether that institution can receive a sealed record — the same fact as\n[`Delegated::relying_party_registered`], repeated at the level a client\nreading the decided request reads it at."}}}],"description":"What `POST /me/access-requests/{id}/approve` answers with: the decided\nrequest, and what to seal.\n\nThe request is flattened, so every field this route has ever returned is\nstill at the top level and a client reading `status` or `grant_id` does not\nhave to be changed. What is added beside it is [`Delegated`] — the same\nstruct `POST /postings/{id}/apply` answers with, deliberately, because the\ntwo human disclosure paths hand the client the same job: seal these scopes,\nto this institution, under this grant.\n\nThe alternative was to widen [`AccessRequest`] itself, and it is the wrong\nshape: that row is read by *both* sides — the employer lists it too — and a\nsealing instruction is addressed to exactly one of them."},"ApprovedVerification":{"type":"object","description":"The `200` body of [`approve`]: the decided check, and the credential it\nissued.\n\nTwo fields rather than one flattened object, so that a client reading this\nand a client reading `GET /admin/verifications` are looking at the same\n[`QueuedVerification`] under the same key.","required":["verification","credential"],"properties":{"credential":{"type":"string","description":"The signed attestation, as `cred.v1.<payload>.<signature>`.\n\nCheckable by `POST /credentials/verify`, and — the point — by anybody at\nall with the platform's public key and fifteen lines of ed25519. Not\nstored anywhere: it is a pure function of the row above and the key, so\nthis same token is what `GET /shared/profile` will mint for a delegate.","example":"cred.v1.<payload-base64url>.<signature-base64url>"},"verification":{"$ref":"#/components/schemas/QueuedVerification"}}},"AskRequest":{"type":"object","description":"Request body for asking.","required":["scopes"],"properties":{"message":{"type":["string","null"],"description":"One sentence for the person being asked. Optional, and shown to them as\nthe notice's body.","example":"We would like to read your CV before the interview."},"scopes":{"type":"array","items":{"type":"string"},"description":"The scopes being asked for, validated against `KNOWN_SCOPES` by exactly\nthe function that validates a grant's — an approval mints a grant from\nthis array verbatim, so a scope that could not be minted must never reach\nthe row.","example":["profile:read","documents:read"]}}},"AskedQuestion":{"type":"object","description":"One question, as the form asks it.","required":["text"],"properties":{"answer_type":{"type":["string","null"],"description":"What the field accepts, if the caller knows: one of `boolean`, `single`,\n`multi`, `text`, `numeric`, `date`, `file`. Omitted, the answer comes\nback in whatever type the record holds it as.","example":"text"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"The choices the field offers, for a `single` or `multi` question. When\nthey are given they are binding — see [`choose`].","example":["Yes","No"]},"text":{"type":"string","description":"The question text, exactly as it appears on the page — label, asterisk,\npunctuation and all. Normalising it is this service's job.","example":"What is your email address?"}}},"AttestationInput":{"type":"object","description":"The domain attestation an institution registers alongside a key.","required":["document","signature","issued_at","attestation_key_id"],"properties":{"attestation_key_id":{"type":"string","description":"Which of the institution's registered Ed25519 keys signed it.\n\nWhen the key being registered *is* that Ed25519 key, this names it and\nthe statement is self-signed, which is how an institution bootstraps: it\npublishes its attestation key first, signing for itself, and every key\nafter that is signed by a key already in the log.","example":"attest-2026"},"document":{"type":"string","description":"The exact bytes the institution publishes at\n`/.well-known/verigrant-relying-party.json`, base64.\n\nHeld verbatim, never re-serialised: the statement's signature covers this\ndocument's SHA-256, and a round trip through a JSON library that reorders\na key or normalises a number produces a document that no longer hashes to\nwhat was signed."},"issued_at":{"type":"string","format":"date-time","description":"When the institution issued this announcement. Inside the signature, and\nwhat makes a stale announcement refusable."},"signature":{"type":"string","description":"Ed25519 over the canonical attestation statement — **not** over the\ndocument — by the institution's own attestation key, base64.\n\nThe statement is what binds the domain, the key, the window and the\nmoment of issuance together; the document is hashed into it. Signing the\ndocument alone would leave every one of those bindings to be inferred by\nwhoever parses the JSON, which is not a binding at all."}}},"AuditEntry":{"type":"object","description":"One entry of the custody trail.\n\nThe same columns [`crate::portability`] hands the owner of a record, plus the\n`user_id` it necessarily omits there — an export is already one person's, and\nthis viewer is not.","required":["id","action","detail","created_at"],"properties":{"action":{"type":"string","description":"`auth.login`, `grant.created`, `identity.shared`, `admin.user.suspended`, …","example":"auth.login"},"created_at":{"type":"string","format":"date-time"},"detail":{"type":"object","description":"What was recorded about the act. Never the values disclosed — see\n[`crate::regulated`], which writes that a regulated field was shared and\nnever what it said."},"grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation the act was performed under, for the entries that had one."},"id":{"type":"integer","format":"int64"},"ip":{"type":["string","null"],"description":"The remote address, as text."},"user_id":{"type":["string","null"],"format":"uuid","description":"The account the entry is about. Null for the acts that had no user to\nname — a failed login against an address that matches no account."}}},"AutofillRequest":{"type":"object","description":"Request body for `POST /me/autofill`.","required":["questions"],"properties":{"questions":{"type":"array","items":{"$ref":"#/components/schemas/AskedQuestion"},"description":"The questions the form is asking, in the order it asks them. The\nresponse comes back in the same order and with the same length, so a\ncaller can zip it back onto its own fields without matching on text."}}},"AutofillResponse":{"type":"object","description":"The response of `POST /me/autofill`.","required":["answers","resolved","gaps"],"properties":{"answers":{"type":"array","items":{"$ref":"#/components/schemas/FilledAnswer"},"description":"One entry per question asked, in the order asked."},"gaps":{"type":"integer","description":"How many did not. These are the questions to put to the user, and the\nanswers to send to `POST /me/autofill/learn`.","minimum":0},"resolved":{"type":"integer","description":"How many came back with an answer, from either source.","minimum":0}}},"AvailabilityPreference":{"type":"object","required":["updated_at"],"properties":{"currently_employed":{"type":["boolean","null"]},"desired_locations":{"type":["array","null"],"items":{"type":"string"}},"desired_pay":{"type":["string","null"],"description":"A decimal string, e.g. `\"72150.00\"`. Money is `numeric` in the database\nand a string on the wire so that no client parses it into a double on\nthe way past — see [`crate::validate::money`].","example":"72150.00"},"desired_pay_currency":{"type":["string","null"],"description":"An ISO 4217 code, upper-case."},"desired_pay_period":{"type":["string","null"],"description":"One of `hourly`, `daily`, `weekly`, `biweekly`, `monthly`, `annual`."},"earliest_start_date":{"type":["string","null"],"format":"date","description":"The date the user could start; an application asks it directly."},"employment_type_sought":{"type":["string","null"]},"max_travel_pct":{"type":["integer","null"],"format":"int32"},"remote_preference":{"type":["string","null"],"description":"One of `onsite`, `hybrid`, `remote`."},"updated_at":{"type":"string","format":"date-time"},"willing_relocate":{"type":["boolean","null"]},"willing_travel":{"type":["boolean","null"]}}},"Billing":{"type":"object","description":"What an organization is on, as the database holds it — or as it is when there\nis no row at all.\n\nThe absence of a row is a legitimate state and the common one: `POST /orgs`\nwrites nothing here, so an organization is on the free plan and active until\nan operator says otherwise. Every read in this module goes through\n[`Billing::of`], so no caller has to remember that.","required":["org_id","plan","status","currency","unit_price_cents","monthly_fee_cents","annual_fee_cents","included_free_pulls","has_gateway_customer","has_payment_method"],"properties":{"annual_fee_cents":{"type":"integer","format":"int32","description":"What a `contract` institution pays a year, as agreed with a person.\n\n**Recorded here and charged nowhere**: an annual platform contract is\ninvoiced outside this meter, by a finance department against a purchase\norder. It is here so that \"what did we agree?\" has an answer an operator\nand an owner can both read. Zero means none recorded.","example":1200000},"billing_email":{"type":["string","null"],"description":"Where this institution wants its invoices sent, if it has said. Read back\nso that [`create_customer`] is not a write-only route: an owner who set a\nbilling contact can see which address the gateway was given.\n\nThe company's, never an account's — see [`NewCustomer::billing_email`]."},"card_brand":{"type":["string","null"],"description":"Which card, as much of it as identifies one: `visa`, `mastercard`. `None`\nwhen there is no card, or when the gateway would not describe the one\nthere is — a cosmetic read is never allowed to decide whether a card is on\nfile.","example":"visa"},"card_last4":{"type":["string","null"],"description":"The last four digits of it, which are printed on every receipt anybody has\never been handed and cannot be charged. They are here so that \"a card is\non file\" can be answered with *which* card, which is the question an owner\nactually has in front of an expired one.","example":"4242"},"card_on_file_at":{"type":["string","null"],"format":"date-time","description":"When the card was put on file. A different fact from `updated_at`, which\nmoves every time an operator touches the plan."},"currency":{"type":"string","description":"ISO 4217.","example":"USD"},"has_gateway_customer":{"type":"boolean","description":"Whether the payment gateway holds a customer for this institution.\n\nA boolean and not the id: the id is the operator's key into a third-party\nsystem and there is no reason for it to travel to a browser. This is the\n\"payment method on file\" an owner sees, and it is `false` until\n[`create_customer`] or the invoicing sweep has made one."},"has_payment_method":{"type":"boolean","description":"Whether there is a card on file — `org_billing.payment_method_id IS NOT\nNULL`.\n\nA boolean and not the id, for the reason `has_gateway_customer` is one:\nthe id is a key into a third-party system and there is no question a\nbrowser can answer with it. This is the field an owner's screen branches\non, and it is the field [`require_may_take_in`] refuses on — see\n[`card_required`]. `false` until a Checkout session completes and the\ngateway's signed event says which card the customer ended up with."},"included_free_pulls":{"type":"integer","format":"int32","description":"How many pull-throughs this institution gets before it has to be on a\npaid plan. Consulted on `free` and on no other plan — see\n[`Billing::gated_by_allowance`].","example":10},"monthly_fee_cents":{"type":"integer","format":"int32","description":"What this institution pays every month before a single candidate moves.\nCharged on `platform` and nowhere else; see [`Billing::monthly_fee`].","example":20000},"note":{"type":["string","null"],"description":"The operator's sentence about this arrangement, if there is one.\n\nRead back by the institution's own owner, which is deliberate: this is\ntheir billing record, a note on it they may not see would be a fact about\nthem kept from them, and an operator's private working belongs in the\naudit trail — which is not theirs.","example":"12-month contract, three campuses, renews in September"},"org_id":{"type":"string","format":"uuid"},"plan":{"type":"string","description":"One of [`PLANS`].","example":"per_pull"},"status":{"type":"string","description":"One of [`BILLING_STATUSES`]. Only `suspended` refuses anything.","example":"active"},"unit_price_cents":{"type":"integer","format":"int32","description":"What one pull-through costs on a `per_pull` or `platform` plan. Zero on\nthe others; see [`Billing::unit_price`].\n\nOn `contract` it is a *record* rather than a charge: what the negotiated\nannual works out at per candidate, which is the figure both sides quote at\nrenewal. [`Billing::unit_price`] answers zero for that plan whatever this\ncolumn says.","example":1200},"updated_at":{"type":["string","null"],"format":"date-time"}}},"BillingOverview":{"allOf":[{"$ref":"#/components/schemas/Billing"},{"type":"object","required":["usage","allowance","state","invoices"],"properties":{"allowance":{"$ref":"#/components/schemas/Allowance","description":"What is left of the included pull-throughs, and whether running out\nrefuses anything."},"invoices":{"type":"integer","format":"int64","description":"How many invoices this organization has, so a client knows whether\n`GET /orgs/{id}/invoices` is worth a request."},"state":{"type":"string","description":"The same facts in one sentence somebody can read without a key to the\nfields: \"10 free pulls, 3 used\", \"platform plan, 42 pulls this month\".\n\nServed by the service rather than assembled by each client, for the\nreason [`Usage::amount_cents`] is: there are three clients that would\nhave to agree about what a plan means — this service's own front end, an\ninstitution's console, and whoever writes the third one — and a sentence\neach of them composes separately is three descriptions of one bill. It is\ndeliberately a *sentence* and not a template with a code: a client that\nwants to render its own has every number it needs above.","example":"10 free pulls, 3 used"},"usage":{"$ref":"#/components/schemas/Usage"}}}],"description":"What `GET /orgs/{id}/billing` answers with."},"BundleEntry":{"type":"object","description":"One record's key, sealed to the institution.","required":["record_id","record_version","sealed_dek"],"properties":{"record_id":{"type":"string","format":"uuid","description":"The record this entry opens. Must be the caller's own, live, and in a\nscope the grant carries."},"record_version":{"type":"integer","format":"int64","description":"The version it opens, and only that version.","example":7},"sealed_dek":{"type":"string","description":"`seal(R_pub, DEK, AAD_grant)`, base64. Exactly 80 bytes.","example":"base64 of 80 bytes"}}},"BundleSummary":{"type":"object","description":"A bundle as its owner reads it back: the manifest of what crossed.","required":["id","grant_id","kind","rp_org_id","rp_org_slug","rp_key_id","rp_key_fingerprint","statement","statement_sig","issued_by","created_at","entries"],"properties":{"created_at":{"type":"string","format":"date-time"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/ManifestEntry"},"description":"Which records, at which versions. Never a sealed key: the owner has no\nuse for one and it is the institution's to hold."},"grant_id":{"type":"string","format":"uuid"},"id":{"type":"string","format":"uuid"},"issued_by":{"type":"string","description":"`user` today; the agent slice adds `agent`.","example":"user"},"kind":{"type":"string","description":"`preview` or `full`.","example":"preview"},"rp_key_fingerprint":{"type":"string","description":"SHA-256 of the exact public key this bundle was sealed to, base64."},"rp_key_id":{"type":"string","example":"2026-q3"},"rp_org_id":{"type":"string","format":"uuid"},"rp_org_slug":{"type":"string","example":"acme-university"},"statement":{"type":"string","description":"The signed statement, base64, byte for byte as it was signed."},"statement_sig":{"type":"string"}}},"Candidate":{"type":"object","description":"One candidate matched to an employer's vacancy.\n\nThe default shape carries **no PII at all**: a score, the factors behind it,\nand — for a candidate who has applied — the state of the delegation and the\npath that reads them properly. That is the same bargain\n[`crate::pipeline::Applicant`] makes, and for the same reason: a list that\nquietly named people would be a disclosure with no record of itself.\n\n**There is no exception left.** A `profile` field carrying five read fields\nof the candidate's record stood here until the plaintext cutover dropped the\ncolumns behind it; the note below this struct records what went with it.\nWhat remains is [`Candidate::profile_path`], which is not the record but the\nroute to it — present only when the candidate has applied to this posting and\nthe grant that application issued is still active — and following it is what\nwrites the read into that candidate's own trail and counts it against the\ndelegation. The list hands out a door, never what is behind it.","required":["score","reasons","matched_at","scored_at","applied","permitted"],"properties":{"application_id":{"type":["string","null"],"format":"uuid","description":"The application, for a candidate who has applied. `null` otherwise, and\nwith it every other field below."},"applied":{"type":"boolean","description":"Whether this candidate has applied to this posting. A match is not an\napplication: most rows here are people the engine thinks fit a vacancy\nthey may never have seen."},"delegation":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Delegation","description":"The delegation the application issued, as it stands right now."}]},"matched_at":{"type":"string","format":"date-time"},"permitted":{"type":"array","items":{"type":"string"},"description":"What that delegation permits — the grant's scopes, so a client can tell\nan empty summary from a withheld one without guessing.","example":["profile:read","history:read"]},"profile_path":{"type":["string","null"],"description":"**How to read this candidate**: the route that follows the delegation,\ncuts the document to the grant's scopes and records the read in their\ntrail.","example":"/orgs/0192.../applications/0193.../profile"},"reasons":{"type":"array","items":{"type":"object"},"description":"One entry per factor. Ratios, counts and verdicts — never a value from\nthe candidate's record. See this module's header."},"score":{"type":"number","format":"double","description":"0 to 100, out of the factors that applied.","example":78.5},"scored_at":{"type":"string","format":"date-time"},"stage":{"type":["string","null"],"description":"One of [`crate::applications::APPLICATION_STAGES`]."}}},"CapabilitySummary":{"type":"object","description":"A capability as its owner reads it back. No key material of any kind.","required":["id","vera_id","scopes","preview_only","grant_ttl_days","max_lookups_per_grant","max_grants","grants_issued","not_before","not_after","created_at","rotation_pending","agent_box_pub","payload_version","holds_scope_keys"],"properties":{"agent_box_pub":{"type":"string","description":"The agent's X25519 public key, base64 — what a re-sealed scope key is\nsealed to.\n\nServed back to the owner's own session because the owner's client is the\nparty that has to perform a rotation, and it cannot seal the new scope key\nto an agent whose public key it does not hold. The browser had this value\nat pairing, typed in from the agent's own device, and a later sign-in on\nanother machine has never seen it.\n\nNothing is disclosed by serving it: it is a public key the account itself\nsupplied, it opens nothing, and this route is the account reading its own\nrow.","example":"base64 of 32 bytes"},"created_at":{"type":"string","format":"date-time"},"grant_ttl_days":{"type":"integer","format":"int64"},"grants_issued":{"type":"integer","format":"int32"},"holds_scope_keys":{"type":"boolean","description":"Whether a rotation of one of this capability's scopes must re-seal the\nnew scope key to it.\n\n`true` for a capability that is neither revoked nor past its window, which\nis exactly [`rotation_pending`](Self::rotation_pending)'s complement and\nis deliberately served rather than left for a client to derive. The rotate\nroute refuses a body that names a capability outside this set — that\nrefusal is what stops a revoked agent being handed the key the rotation\nexists to keep from it — so a client that computed the set differently\nwould have its rotations refused and a client that guessed generously\nwould be asking to undo the revocation.\n\nOne definition, in one place, and this is it. See [`crate::rotation`]."},"id":{"type":"string","format":"uuid"},"label":{"type":["string","null"]},"max_grants":{"type":"integer","format":"int32"},"max_lookups_per_grant":{"type":"integer","format":"int32"},"not_after":{"type":"string","format":"date-time"},"not_before":{"type":"string","format":"date-time"},"payload_version":{"type":"integer","format":"int32","description":"Which generation of this capability's sealed payload the service holds,\nstarting at 1 for the pairing.\n\nA counter rather than a key, served for the same reason `agent_box_pub`\nis: a re-seal is bound to `payload_version + 1` in its associated data, so\na client that did not know the current number could not produce one the\nrotation would accept."},"preview_only":{"type":"boolean"},"revoked_at":{"type":["string","null"],"format":"date-time"},"rotation_pending":{"type":"boolean","description":"**Revoked here, not yet revoked in the mathematics.**\n\n`true` when this capability has been revoked or has expired and the\nuser's client has not yet rotated the scope epochs it covered. Until that\nhappens the agent's retained scope keys still open records written before\nthe revocation, and any copy it kept is still readable. The interface\nmust say so rather than showing a clean \"revoked\"; see section 4.5.\n\nIt goes `false` when the owner's client has rotated **every** scope this\ncapability covered, through `POST /me/scopes/{scope}/rotate`; see\n[`crate::rotation`], which is where that is decided."},"scopes":{"type":"array","items":{"type":"string"}},"vera_id":{"type":"string","format":"uuid"}}},"CardSession":{"type":"object","description":"Where to send the browser, and the gateway's name for the attempt.","required":["session_id","url"],"properties":{"session_id":{"type":"string","description":"The gateway's id for this session. Returned so that a support\nconversation can name the attempt; it is not stored, because what decides\nwhether a card is on file is the signed event that follows, not a session\nthis service handed out.","example":"cs_test_a1B2c3"},"url":{"type":"string","description":"The URL the browser must go to. The one field this route exists to\nproduce.","example":"https://checkout.stripe.com/c/pay/cs_test_a1B2c3"}}},"CdlDetail":{"type":"object","description":"What a commercial driving licence adds to a credential.","required":["credential_id","cdl_class","created_at"],"properties":{"cdl_class":{"type":"string","description":"`A`, `B` or `C`.","example":"A"},"cmv_years":{"type":["integer","null"],"format":"int32","description":"Years driving a commercial motor vehicle, as the application asks it.\nNot derived from the work history: a driver can have driven at three of\nfive employers."},"created_at":{"type":"string","format":"date-time"},"credential_id":{"type":"string","format":"uuid","description":"The credential this describes. It is also the row's primary key: there\nis at most one of these per credential, by the shape of the table."},"endorsements":{"type":["array","null"],"items":{"type":"string"},"description":"Any of `H` (hazmat), `N` (tank), `T` (doubles/triples), `P`\n(passenger), `S` (school bus), `X` (hazmat and tank).","example":["H","N"]},"med_card_expiration":{"type":["string","null"],"format":"date","description":"The DOT medical certificate expires separately from the licence, and an\nexpired one grounds an otherwise valid CDL."},"restrictions":{"type":["array","null"],"items":{"type":"string"},"description":"Any of `E`, `L`, `M`, `N`, `O`, `V`, `Z`, `K` — what the licence does\nnot permit."}}},"ChargeRun":{"type":"object","description":"What one on-demand billing run did.\n\nThe invoice and the outcome together, because either alone is a half answer:\nan operator who asked for a bill to be drawn and charged needs to know what\nwas drawn *and* what the gateway said about it, and a route that returned only\nthe invoice would leave \"did the card work?\" to a second request against a\ncolumn that has since been overwritten by a callback.","required":["org_id","outcome","detail"],"properties":{"detail":{"type":"string","description":"The same thing in a sentence, for an operator's console. Prose, not a\ncontract: branch on `outcome`.\n\nWhere the gateway refused, this carries what `invoice.last_error` carries\n— which is deliberately not on the customer's own screens and is\ndeliberately on this one: it is this service's classification of a third\nparty's failure, and the person reading it here is the operator who acts\non it.","example":"the gateway took the charge; the invoice is open until its signed event arrives"},"invoice":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Invoice","description":"The invoice this run drafted, as it stands *after* the charge — so its\n`status` is `draft`, `open` or `paid` according to what the gateway said.\n`None` only when there was nothing to draw."}]},"org_id":{"type":"string","format":"uuid"},"outcome":{"type":"string","description":"One of [`CHARGE_OUTCOMES`]. The field a client branches on.","example":"presented"}}},"CheckOutcome":{"type":"string","description":"A single check's verdict. Two words and no third: a check that could not be\nrun is an `alert` saying so, because \"I could not tell\" and \"it is fine\" are\nnot the same answer and only one of them is safe to show in green.","enum":["ok","alert"]},"ClaimRequest":{"type":"object","description":"Request body for `POST /auth/claim`.\n\nNo `Debug`: it holds the value that authorises the claim and, inside the\nkeyring, the account's new `auth_key`.","required":["claim_sha256","claim_auth","keyring"],"properties":{"claim_auth":{"type":"string","description":"The 32 bytes derived from the claim secret under the authentication\nlabel, base64. Checked against the stored Argon2id hash through the same\n[`crate::auth::verify_secret`] a login goes through. It opens nothing.","example":"base64 of 32 bytes"},"claim_sha256":{"type":"string","description":"Which claim slot this is about: SHA-256 of the claim secret, base64.","example":"base64 of 32 bytes"},"keyring":{"$ref":"#/components/schemas/PutKeyringRequest","description":"The keyring this account has from now on, in exactly the shape\n`PUT /me/keyring` takes: a password slot, a recovery slot, the derived\ncredential and the two published keys. `current_auth_key` is absent and\nmust be — there is no credential to replace, because this is the\naccount's first."}}},"ClaimSlot":{"type":"object","description":"What `GET /auth/claim` answers with.\n\nDeliberately nothing about the person. No address, no name, no display name,\nno count of records, not even whether any have been written. What is here is\nthe account id — which the client needs as the HKDF `info` and as half of the\nwrap's associated data — and 72 bytes that open under one key in the world.\n\nThe expiry is here because the screen has to be able to say something true\nabout a link that is about to lapse, and because it is a fact about the link\nrather than about the person.","required":["user_id","claim_slot_id","claim_wrapped_dmk","claim_nonce","expires_at"],"properties":{"claim_nonce":{"type":"string","example":"base64 of 24 bytes"},"claim_slot_id":{"type":"string","format":"uuid"},"claim_wrapped_dmk":{"type":"string","example":"base64 of 48 bytes"},"expires_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid"}}},"Claimed":{"type":"object","description":"What a completed claim answers with: a session, and the account it belongs\nto.\n\nThe session is minted here rather than left to a `POST /auth/login` the client\nwould then have to make, and the reason is not convenience. The person has\njust proved possession of the claim secret *and* opened the master key with\nit; sending them round to the login screen would mean asking for the password\nthey typed thirty seconds ago before they have any reason to trust that it\nworked. `POST /auth/recover` mints one on the same argument.\n\nThe account row comes back with it because the client needs the address: the\nlocal marker that keeps this browser from ever sending this account's password\nis keyed by email, and until this moment nothing has told the client what the\naddress is. That is a disclosure to a caller who has just demonstrated they\nhold the key to the record, which is the one caller entitled to it.","required":["token","expires_at","user"],"properties":{"expires_at":{"type":"string","format":"date-time"},"token":{"type":"string","description":"The bearer token, on the same terms as a login's."},"user":{"$ref":"#/components/schemas/PublicUser"}}},"CodeRequest":{"type":"object","description":"A code on its way to be checked: a six-digit TOTP code, or a recovery code.\n\nOne field for both, because the caller has one thing to type and asking them\nwhich kind it is would be asking them to classify their own credential. Which\npath it takes is decided by its shape — see the module docs on why that is\nnot an oracle.\n\nNo `Debug`: a recovery code is a credential.","required":["code"],"properties":{"code":{"type":"string","description":"Six digits from an authenticator, or one of the recovery codes from\nenrolment. Whitespace and hyphens are ignored, and a recovery code is\nmatched case-insensitively.","example":"492039"}}},"CompleteVerificationRequest":{"type":"object","description":"Request body for `POST /me/verifications/{id}/complete`.","required":["status"],"properties":{"status":{"type":"string","description":"`verified`, `failed` or `expired`. Not `pending`: a completion back to\n\"nobody has said anything yet\" is not a completion.","example":"verified"}}},"ConfirmResetRequest":{"type":"object","description":"Request body for `POST /auth/account-reset/confirm`.\n\nNo `Debug`: the token is a live credential for the length of its life, and a\n`{:?}` on the way to a log line would put a scheduled account destruction in\nit.","required":["token","confirmation"],"properties":{"confirmation":{"type":"string","description":"[`CONFIRMATION`], typed. See that constant for why the server checks it\nas well as the screen.","example":"delete everything"},"token":{"type":"string","description":"The value of the `reset-account` query parameter in the emailed link.","example":"<43-character opaque token>"}}},"Connector":{"type":"object","description":"One entry in the catalogue.\n\nEvery field is either a label a screen shows or a fact about what the entry\nis for. There is deliberately no endpoint, no client id and no secret here:\na connector runs in the person's own client, so the credentials are theirs\nand this service never holds one.","required":["id","name","direction","status","schema","note"],"properties":{"direction":{"$ref":"#/components/schemas/Direction"},"id":{"type":"string","description":"The identifier that goes in `inbound_item.connector_id` and in the\nprovenance line. Lower-case, hyphenated, and stable: it is written into\nrows that are immutable afterwards.","example":"fhir-patient-access"},"name":{"type":"string","description":"What it is called on a screen.","example":"Patient-directed FHIR access"},"note":{"type":"string","description":"One sentence saying what this is and what it is not, in the words a\nperson reads. It is served rather than kept in a document because the\nscreen that offers a pull is where the distinction between an aggregator\nand a source has to be made, and a sentence in a document is a sentence\nnobody reads at the moment it matters."},"schema":{"type":"string","description":"Which of [`crate::inbound::INBOUND_SCHEMAS`] its documents arrive in.","example":"fhir"},"status":{"$ref":"#/components/schemas/ConnectorStatus"}}},"ConnectorStatus":{"type":"string","description":"Whether this deployment can actually run a connector today.","enum":["available","planned"]},"ConsentRecord":{"type":"object","description":"One consent record: what was consented to, whether it was granted, when,\nagainst which document, and how it was signed.\n\nNever the thing consented to. A row saying a background check was authorised\nis a consent record; the report that check produced is not stored by this\nservice at all.","required":["id","consent_type","granted","granted_at","created_at"],"properties":{"application_id":{"type":["string","null"],"format":"uuid","description":"The application this consent was given for, where it was given as part\nof one.\n\n`null` is the ordinary answer and is never a gap in the record: the\nterms of service a person agrees to at sign-up, and a standing\nauthorisation recorded from the consent screen, belong to no application\nand inventing one for them would be this service asserting a link\nnobody made. What the link buys where it *is* set is the question a\nregulated consent has to be able to answer years later — \"which\nbackground check was this authorising?\" — which a timestamp beside a\nlist of applications only approximates.\n\nSet from `POST /me/consents` and only ever to an application of the\ncaller's own; see [`record_consent`] for why the check is the route's\nand cannot be the foreign key's."},"consent_type":{"type":"string","description":"One of `background_check`, `drug_test`, `mvr`, `fcra_disclosure`,\n`electronic_signature`, `data_sharing`, `terms_of_service`.","example":"background_check"},"created_at":{"type":"string","format":"date-time"},"document_version":{"type":["string","null"],"description":"Which version of the document was shown. A consent to v1 is not a consent\nto v2, and without this the record cannot say which was signed.","example":"fcra-disclosure-v3"},"granted":{"type":"boolean","description":"`false` is a real record: a refusal, or a later withdrawal, is a fact\nworth keeping as firmly as an agreement."},"granted_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"ip":{"type":["string","null"],"description":"Observed by the server at the moment of the write, not claimed by the\nclient: it is evidence of the signing act.","example":"203.0.113.7"},"signature_method":{"type":["string","null"],"description":"One of `typed`, `checkbox`, `wet`, `e_signature`."},"signer_name":{"type":["string","null"],"description":"The name as the signer typed it — the signature itself, when the method\nis `typed`."},"user_agent":{"type":["string","null"],"description":"Observed the same way, and stored verbatim for the same reason."}}},"CreateAddressRequest":{"type":"object","properties":{"address_type":{"type":["string","null"],"description":"`current` (the default), `prior` or `mailing`."},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"from_date":{"type":["string","null"],"format":"date"},"is_current":{"type":["boolean","null"],"description":"Defaults to whether `address_type` is `current`, which is what a caller\nthat says nothing about it almost always means."},"line1":{"type":["string","null"]},"line2":{"type":["string","null"]},"postal_code":{"type":["string","null"]},"region":{"type":["string","null"]},"to_date":{"type":["string","null"],"format":"date"}}},"CreateCredentialRequest":{"type":"object","required":["name"],"properties":{"credential_number":{"type":["string","null"]},"expiration_date":{"type":["string","null"],"format":"date"},"is_cdl":{"type":["boolean","null"],"description":"Defaults to false. Writing the CDL detail sets it regardless."},"issue_date":{"type":["string","null"],"format":"date"},"issuing_org":{"type":["string","null"]},"jurisdiction":{"type":["string","null"]},"name":{"type":"string","example":"Commercial Driver's Licence"},"status":{"type":["string","null"],"description":"One of `active`, `expired`, `in_progress`."}}},"CreateDrivingEquipmentRequest":{"type":"object","required":["equipment_type"],"properties":{"approx_from":{"type":["string","null"],"format":"date"},"approx_to":{"type":["string","null"],"format":"date"},"class_operated":{"type":["string","null"]},"equipment_type":{"type":"string","example":"Tractor-semitrailer"},"total_time_months":{"type":["integer","null"],"format":"int32"}}},"CreateGrantRequest":{"type":"object","description":"Request body for `POST /grants`.","required":["scopes"],"properties":{"audience":{"type":["string","null"],"description":"Optional. The slug of the registered institution this delegation is\nbeing issued *to*.\n\nNaming one is the stronger form of the same grant. Reading a record\nalready requires the reader to be a registered relying party — see\n[`crate::relying_party`] — and this binds the delegation to one\nparticular institution on top of that, so a token that leaks from Acme\ncannot be redeemed by Initech even though Initech is registered too.\n\nLeaving it out is what an agent delegation is: the holder is the user's\nown agent, there is no institution to name, and the gate still refuses\nan unregistered reader. Every grant issued before this existed is in\nthat state, which is why the column is nullable and this field is\noptional rather than required.\n\nA slug that is not a registered relying party is a `400`. Silently\nissuing an unbound grant instead would be the one failure mode this\nfield exists to prevent.","example":"acme-university"},"expires_at":{"type":["string","null"],"format":"date-time","description":"Must be in the future. Defaults to 90 days out — a grant is a standing\ndelegation, but never an unbounded one."},"label":{"type":["string","null"],"description":"Optional, for the owner's own benefit — it names the delegation in\n`GET /grants`. Not part of the token and not shown to the holder.","example":"Recruiter at Example Corp"},"max_lookups":{"type":["integer","null"],"format":"int32","description":"Optional. How many times this delegation may be read before it is spent.\n\nLeft out, the delegation is uncapped and only its expiry and its owner\ncan retire it — which is what every grant issued before this field\nexisted is. An auto mode preset always sets it (about three, per\n`product-model.md`), and a user issuing by hand may.\n\nCounted over *completed* uses, so a refused request never spends one, and\nenforced in the one place every acceptance decision is made. A spent\ndelegation is refused as `exhausted`. The check and the increment happen\nunder a row lock [`authorize`] takes for the life of the request, so two\nholders presenting the token at the same moment cannot both spend the\nlast read.","example":3},"scopes":{"type":"array","items":{"type":"string"},"description":"Non-empty, at most 32 entries of at most 64 characters, each one drawn\nfrom [`KNOWN_SCOPES`] — a scope this service does not enforce would\ndelegate nothing, so it is refused here rather than minted. Scopes are\nsymbols matched exactly: `profile:read:public` does not satisfy a check\nfor `profile:read`.","example":["profile:read"]}}},"CreateGrantResponse":{"type":"object","description":"Response body for `POST /grants`. Not `Debug`: `token` is a credential and\nthis is the only moment it exists on our side.","required":["grant_id","token","scopes","expires_at"],"properties":{"audience_org_id":{"type":["string","null"],"format":"uuid","description":"The institution this delegation was bound to, if the request named one.\n`null` means the delegation names no institution — see\n[`CreateGrantRequest::audience`]."},"expires_at":{"type":"string","format":"date-time"},"grant_id":{"type":"string","format":"uuid"},"max_lookups":{"type":["integer","null"],"format":"int32","description":"The read cap, if this delegation has one."},"scopes":{"type":"array","items":{"type":"string"}},"token":{"type":"string","description":"The portable half of the grant: `v1.<payload>.<signature>`, an ed25519\nsignature over the token's own prefix. Returned here and nowhere else —\nwe keep no copy, and no later endpoint can reissue it.","example":"v1.<payload-base64url>.<signature-base64url>"}}},"CreateLanguageRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","example":"Spanish"},"proficiency":{"type":["string","null"],"description":"`native`, `fluent`, `professional` or `limited`."}}},"CreateLinkRequest":{"type":"object","required":["url"],"properties":{"label":{"type":["string","null"],"description":"`linkedin`, `github`, `portfolio`, `website`, or `other` (the default)."},"url":{"type":"string","description":"Must be `http://` or `https://`; see [`crate::validate::url`].","example":"https://example.com/ada"}}},"CreateOrgRequest":{"type":"object","description":"Request body for `POST /orgs`.","required":["name"],"properties":{"name":{"type":"string","example":"Aperture Robotics"},"slug":{"type":["string","null"],"description":"Optional. Derived from the name when absent, and normalised either way —\nso `\"Aperture Robotics!\"` and `\"aperture-robotics\"` are one handle.","example":"aperture-robotics"},"website":{"type":["string","null"],"example":"https://aperture.example.com"}}},"CreatePhoneRequest":{"type":"object","required":["phone"],"properties":{"country_code":{"type":["string","null"],"example":"+44"},"is_primary":{"type":"boolean","description":"Marking a number primary demotes whichever number was primary before —\nthere is at most one, and the database enforces it."},"phone":{"type":"string","example":"07700 900123"},"phone_type":{"type":["string","null"],"description":"`mobile` (the default), `home` or `work`."}}},"CreatePostingRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/postings`.","required":["title","description"],"properties":{"description":{"type":"string"},"employment_type":{"type":["string","null"],"example":"full_time"},"location":{"type":["string","null"],"example":"Leeds, UK"},"remote":{"type":["boolean","null"]},"required_scopes":{"type":["array","null"],"items":{"type":"string"},"description":"Defaults to `[\"profile:read\", \"history:read\"]`. Every entry must be a\nscope this service actually enforces.","example":["profile:read","history:read"]},"screening_questions":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ScreeningQuestion"},"description":"Defaults to an empty array."},"status":{"type":["string","null"],"description":"Defaults to `draft`. A posting may be created `open`, which publishes it\nimmediately.","example":"draft"},"title":{"type":"string","example":"Class 1 Driver — Leeds"}}},"CreateProvisionalRequest":{"type":"object","description":"Request body for `POST /agent/provisional`.\n\nDeliberately no `Debug`, like every other type in this service that holds a\ncredential: `claim_auth` is one.\n\nRead the field list as the specification of what an agent may ask this\nservice to hold on a stranger's behalf. An address, an optional name, one wrap\nslot, one digest and one verifier — and **not the claim secret**, which is the\npoint of the whole arrangement and is argued at the top of this module.","required":["user_id","email","claim_slot_id","claim_wrapped_dmk","claim_nonce","claim_sha256","claim_auth"],"properties":{"claim_auth":{"type":"string","description":"The 32 bytes a claimant presents where an ordinary account sends\n`auth_key`. Hashed here and dropped; the hash is what is stored, and no\nroute returns it.","example":"base64 of 32 bytes"},"claim_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"claim_sha256":{"type":"string","description":"SHA-256 of the claim secret, base64. How the claim routes find this row.","example":"base64 of 32 bytes"},"claim_slot_id":{"type":"string","format":"uuid","description":"The claim slot's id, minted by the agent. Part of the wrap's associated\ndata, which is why it is stored rather than implied."},"claim_wrapped_dmk":{"type":"string","description":"The claim wrap slot: the DMK sealed under `KEK_claim`, 48 bytes.","example":"base64 of 48 bytes"},"display_name":{"type":["string","null"],"description":"Optional, trimmed, dropped if blank.","example":"Ada Lovelace"},"email":{"type":"string","description":"Where the person will sign in from once they have claimed it. Held in the\nclear, exactly as every other account's address is.","example":"ada@example.com"},"expires_in_days":{"type":["integer","null"],"format":"int64","description":"How long the link should stand, in days. Absent takes 30; more than 90 is\nrefused.","example":30},"user_id":{"type":"string","format":"uuid","description":"The account's id, **minted by the agent**.\n\nThe same ordering argument `PUT /me/records/{id}` makes: the id is the\nHKDF `info` of the claim key and half of the wrap's associated data, so a\nservice that allocated it would be asking the client to encrypt against a\nvalue it does not yet know. Section 2.3's two-stage registration is the\nother answer to the same problem; this is the one that costs one round\ntrip rather than two."}}},"CreateReferenceRequest":{"type":"object","required":["name"],"properties":{"company":{"type":["string","null"]},"email":{"type":["string","null"]},"name":{"type":"string","example":"Dana Whitfield"},"phone":{"type":["string","null"]},"relationship":{"type":["string","null"],"example":"Former manager"},"title":{"type":["string","null"]},"years_known":{"type":["integer","null"],"format":"int32"}}},"CreateUserRequest":{"type":"object","description":"Request body for `POST /users`.\n\nDeliberately does not derive `Debug`: it holds a plaintext password, and an\naccidental `{:?}` anywhere would put that password in the logs.","required":["email","password"],"properties":{"display_name":{"type":["string","null"],"description":"Optional. Trimmed, and dropped entirely if it is blank.","example":"Ada Lovelace"},"email":{"type":"string","description":"Must contain an `@`. Compared case-insensitively, so an address that\ndiffers only in case is the same account.","example":"ada@example.com"},"password":{"type":"string","description":"At least 12 characters. Stored only as an argon2id hash. The web client\nasks for more than that before it will use one: see\n[`MIN_PASSWORD_LEN`].","example":"a long passphrase nobody has written down"}}},"CreateVerificationRequest":{"type":"object","description":"Request body for `POST /me/verifications`.","required":["kind"],"properties":{"kind":{"type":"string","description":"`identity`, `employment`, `education` or `income`.","example":"identity"},"provider":{"type":["string","null"],"description":"Which checker to use. Omit it for the manual one, which is the only\nimplementation in this build.","example":"manual"}}},"CredentialEntry":{"type":"object","description":"One licence, certification, registration or ticket, with its commercial\ndriving detail attached when it has one.","required":["id","name","is_cdl","created_at"],"properties":{"cdl":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CdlDetail","description":"The 1:1 commercial-licence extension, or `null`. Not a column on\n`credential` and not decoded from one — `#[sqlx(skip)]` leaves it `None`\nfor every statement here, and [`load_credentials`] is the one place that\nfills it in."}]},"created_at":{"type":"string","format":"date-time"},"credential_number":{"type":["string","null"],"description":"The number on the document. Sensitive: with a name and a jurisdiction it\nis frequently enough to look the holder up in a registry, so it travels\nto a delegate only on a grant carrying [`REQUIRED_SCOPE`], along with the\nrest of the credential."},"expiration_date":{"type":["string","null"],"format":"date"},"id":{"type":"string","format":"uuid"},"is_cdl":{"type":"boolean","description":"Whether this is a commercial driving licence. Set by\n`PUT /me/credentials/{id}/cdl`, and settable directly for a driver who\nhas not filled in the class breakdown yet."},"issue_date":{"type":["string","null"],"format":"date"},"issuing_org":{"type":["string","null"],"example":"Illinois Secretary of State"},"jurisdiction":{"type":["string","null"],"description":"Where it was issued — the field an employer checks first.","example":"IL"},"name":{"type":"string","example":"Commercial Driver's Licence"},"status":{"type":["string","null"],"description":"One of `active`, `expired`, `in_progress`. What the *holder* says; the\nexpiry date is the fact, and this is the claim about it."}}},"CredentialRefused":{"type":"object","description":"The non-`200` body of [`verify`], in the shape `POST /grants/verify` uses.\n\nA refusal is the endpoint working, not an error, so it keeps its own shape\nrather than the standard error envelope and the reason is meant to be\nbranched on.","required":["valid","reason"],"properties":{"reason":{"type":"string","description":"`malformed`, `invalid_signature`, or `expired`.","example":"invalid_signature"},"valid":{"type":"boolean","description":"Always `false`."}}},"CredentialVerified":{"type":"object","description":"The `200` body of [`verify`]: the signed claim, decoded.\n\nEvery field here comes out of the payload the signature covers. Nothing is\nlooked up, so nothing here can disagree with what the holder's own offline\ncheck would have told them.","required":["valid","subject","kind","status","issued_at"],"properties":{"expires_at":{"type":["string","null"],"format":"date-time","description":"When the underlying check stops meaning anything, if it does. Absent\nwhen the credential has no expiry."},"issued_at":{"type":"string","format":"date-time","description":"When the credential was minted. The token carries this as unix seconds;\nit is rendered here as RFC 3339, which is what every other timestamp in\nthis API is."},"kind":{"type":"string","description":"`identity`, `employment`, `education` or `income`.","example":"identity"},"provider":{"type":["string","null"],"description":"Who did the checking, when the row named one.","example":"manual"},"status":{"type":"string","description":"What was asserted. `verified` for anything this build minted.","example":"verified"},"subject":{"type":"string","format":"uuid","description":"The user the attestation is about."},"valid":{"type":"boolean","description":"Always `true` here, and narrower than it looks: it means *we signed this\nand it has not expired*. Whether the subject is verified is `status`."}}},"Delegated":{"type":"object","description":"The delegation an application issued, as the applicant should see it.\n\nNo token, because none was minted: see\n[`crate::grants::issue_without_token`]. What is here is what a person needs\nto understand and, if they change their mind, to revoke — the id\n`DELETE /me/grants/{id}` takes, exactly what was delegated, and until when.\n\n# And what the client needs in order to seal\n\nThe rest of it is the sealing instruction. A delegation is a *permission*;\nunder [`crate::encrypted`] the records it permits are ciphertext this service\ncannot open, so the disclosure is not finished until the applicant's own\nclient has sealed the record keys to the employer — which it can only do if\nit is told which institution to seal to and which scopes to cover. That is\n[`Delegated::audience`], [`Delegated::relying_party_registered`] and\n[`Delegated::scopes`], and they are on this struct rather than behind a\nsecond call because the moment to seal is the moment the grant is minted:\nthe person is here, their keys are unlocked, and a client that had to go and\nfetch the audience would be a client that could skip the step.\n\nThe sealing itself stays in the client. This service holds no record key and\nhas no route that would take one; see [`crate::bundles`].","required":["grant_id","scopes","expires_at","audience","relying_party_registered"],"properties":{"audience":{"type":"string","description":"The employer this delegation is for, by the name an institution is named\nby everywhere a client has to name one: `GET /rp/{slug}/keys` is keyed on\nit, so it is the only handle a browser can seal *to*, and the client\nchecks what that returns against the institution's own domain before it\nseals one byte.\n\nAlways the employer's slug, whether or not the grant names them as its\naudience — a seeker is entitled to be told who cannot receive their\nrecord as precisely as who can. Whether to seal is\n[`Delegated::relying_party_registered`]'s question and not this field's.","example":"acme-university"},"expires_at":{"type":"string","format":"date-time"},"grant_id":{"type":"string","format":"uuid"},"relying_party_registered":{"type":"boolean","description":"Whether that employer can actually receive a sealed record.\n\n`true` says two things that are the same thing: the organization is a\nregistered relying party with a live X25519 key, and **this grant names\nit** as its `audience_org_id` — so `POST /me/grants/{id}/bundle` will\naccept a bundle sealed to it and the institution's own read will serve it\nback.\n\n`false` is not an error and not a smaller disclosure: the grant was\nminted exactly as it always has been, naming no audience, and the\nemployer reads the plaintext shared profile through it as before. What it\nmeans is that there is nothing to seal *to* — the employer has published\nno key — and the honest thing for a client to do is to tell the seeker\nso, rather than to start a sealing that will fail at the last step. The\nregistry is the authority either way and the seal step asks it again;\nthis field is what lets a screen say it *before* a spinner."},"scopes":{"type":"array","items":{"type":"string"},"description":"The posting's `required_scopes`, and never anything wider. These are the\nscopes to seal: a bundle entry outside them is refused by\n[`crate::bundles::store_bundle`], whatever the statement claims.","example":["profile:read","history:read"]}}},"DelegatedApplyRequest":{"type":"object","description":"Request body for the delegated submission. Every field optional, and `{}` is\na perfectly good body: everything an application is made of is already on\nfile, and what a delegate may add is the one thing that is not.","properties":{"draft":{"type":["string","null"],"description":"The covering letter to send with it — the draft\n`POST /shared/agent/draft-application` returned, edited or not."}}},"Delegation":{"type":"object","description":"The state of one application's delegation, from the employer's side.\n\nNo token and no key material, because none was ever minted: an employer\nfollows a delegation rather than presenting one — see\n[`crate::grants::issue_without_token`].","required":["grant_id","scopes","status"],"properties":{"expires_at":{"type":["string","null"],"format":"date-time"},"grant_id":{"type":"string","format":"uuid"},"last_used_at":{"type":["string","null"],"format":"date-time","description":"When somebody at this organization last read the profile through it."},"scopes":{"type":"array","items":{"type":"string"},"description":"Exactly the posting's `required_scopes`, and never anything wider.","example":["profile:read","history:read"]},"status":{"$ref":"#/components/schemas/GrantStatus","description":"`active`, `expired` or `revoked` — the same verdict `GET /me/grants`\nshows the applicant and `POST /grants/verify` enforces, from the same\nfunction. **`revoked` means the candidate has withdrawn their record**,\nand the profile read will answer `403`."}}},"Direction":{"type":"string","description":"Which way a partner moves data, which decides which half of this product\nthey are.\n\nSerialised as a lower-case word, because it goes on the wire and into the\nenvelope's label rather than only into a screen.","enum":["pull","push"]},"DirectoryUser":{"type":"object","description":"What one account may learn about another.\n\nTwo fields, and the absence of the rest is the point. [`PublicUser`] is\ncalled public because it carries nothing secret — no password hash, no\nstatus — but it carries the account's *email address*, and an email address\nis not a thing strangers are entitled to read off an id. `created_at` is\ngone for a weaker version of the same reason: it is account metadata, it\ntells a caller nothing they asked a directory for, and the way to keep it\nfrom leaking is not to select it.\n\nThe owner's own view is unaffected — `GET /auth/me` still answers with the\nfull [`PublicUser`], to the one caller who already knows every field in it.\nThis type is deliberately the same shape whoever asks, including a caller\nlooking themselves up: a response that grew a field when the id happened to\nbe your own would be a response shape clients have to branch on, and a\nsecond, subtler oracle besides.","required":["id"],"properties":{"display_name":{"type":["string","null"],"description":"What this person chose to be called. `null` if they never set one.","example":"Ada Lovelace"},"id":{"type":"string","format":"uuid"}}},"DocumentMetrics":{"type":"object","description":"The documents directory, as of the last time anything looked at it.\n\nEvery other figure on this page is counted now, from a table. These three are\nnot, and cannot be: whether a file is on a disk is not a question SQL\nanswers, and a route that stat-ed every stored document while an\nadministrator waited would be a page whose load time grew with the store. So\n[`crate::sweeps::reconcile_documents`] does the looking, hourly, and this is\nthe row it left behind.\n\nAll three fields are `null` until that sweep has run once. That is the point\nof the shape: a `missing_files` of zero beside a `checked_at` of null would\nread as \"nothing is missing\" when what it means is \"nobody has looked\", and\nthose are the two states this whole feature exists to tell apart. The\nproduction failure it was written for, which was seven rows, no files and\nevery read a 500, lasted as long as it did because the second state was\nindistinguishable from the first.","properties":{"checked_at":{"type":["string","null"],"format":"date-time","description":"When that pass ran. A `checked_at` that stops moving is a worker that has\nstopped, which is worth as much attention as a missing file."},"missing_files":{"type":["integer","null"],"format":"int64","description":"How many of those named a file the filesystem said was not there.\nAnything above zero is somebody's upload that this service can no longer\nhand back, and the sweep's `warn` lines in the journal name which\ndocuments they were."},"stored":{"type":["integer","null"],"format":"int64","description":"`document` rows carrying a `storage_key` that the sweep got an answer\nabout when it last ran.\n\nRows the store could not be asked about — an I/O error that is not\n`NotFound` — are in neither this figure nor the next, and the sweep's\n`error` lines name them. See [`crate::documents::Presence`] for why they\nare kept out of both."}}},"DocumentRecord":{"type":"object","description":"One document: what it is, and everything a client needs to open it that this\nservice is allowed to hold.\n\nThe bytes are not in here and never will be. They are fetched from a route\nof their own, so a listing of thirty documents is a listing and not thirty\nfiles, and so the one place that serves content is the one place that has to\nget the authorisation right.\n\n**Read the field list as what an operator learns about a sealed document.**\nA kind, an id, a size to within a padding class, a nonce, a digest of the\nciphertext and two timestamps. Three of the fields below are `null` on every\ndocument written since blocker 9 — `filename`, `mime_type` and `label` — and\na check constraint rather than this file is what keeps them that way.","required":["id","doc_type","crypto_version","is_default","has_legacy_bytes","created_at","updated_at"],"properties":{"byte_class":{"type":["integer","null"],"format":"int64","description":"The padding class the plaintext was padded to before it was sealed, and\n`null` on a version 1 row. A component of the file's associated data, so\na reader rebuilds it from here — or, better, from the copy inside the\ndocument's own record, which this service cannot have edited."},"content_sha256":{"type":["string","null"],"description":"SHA-256 of the stored bytes, lower-case hex, and it is also the `ETag` on\nthe content routes.\n\nOn a sealed document this is the digest **of the ciphertext**, for the\nreason `inbound_item.payload_hash` is: a hash of the plaintext would let\nanybody holding the row confirm a guessed document, and the row is held by\nthis service, by an operator with a dump and by every later delegate.\nOver the ciphertext it is checkable by all of them and informative to\nnone.","example":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},"created_at":{"type":"string","format":"date-time"},"crypto_version":{"type":"integer","format":"int32","description":"`1` for a document uploaded before the bytes became ciphertext — the file\nat `storage_key` is the plaintext as it was sent — and `2` for one sealed\nin its owner's client.\n\nThe one field on this type a client branches on. A `1` is what\n`PUT /me/documents/{id}/sealed` exists to turn into a `2`.","example":2},"ct_nonce":{"type":["string","null"],"description":"The 24-byte nonce the bytes were sealed under, base64. `null` on a\nversion 1 row.","example":"base64 of 24 bytes"},"doc_type":{"type":"string","description":"One of `resume`, `cover_letter`, `transcript`, `certificate`, `license`,\n`portfolio`, `reference_letter`, `writing_sample`, `other`.\n\nPlaintext, and on section 6's list for the same reason `record_type` is:\nit names the *shape* of a document and never its content, and a\nform-filler has to know which of a person's files is the résumé.","example":"resume"},"filename":{"type":["string","null"],"description":"The name the file had. **`null` on every sealed document**: it is a field\nvalue, usually the most identifying one on the record, and it lives\ninside the ciphertext of the document's own record.","example":"null on a sealed document"},"has_legacy_bytes":{"type":"boolean","description":"Whether this document's **superseded plaintext** is still on disk.\n\nTrue exactly between a re-seal and the owner spending\n`DELETE /me/documents/{id}/legacy`. No route serves those bytes; this\nfield is how the owner's client knows there is still something to ask to\nhave destroyed, and how they can be told so plainly rather than having it\nhappen behind them."},"id":{"type":"string","format":"uuid"},"is_default":{"type":"boolean","description":"Whether this is the one to attach by default. At most one per\n`doc_type`, enforced by a partial unique index."},"label":{"type":["string","null"],"description":"What the user calls it, as distinct from the filename their word\nprocessor chose. `null` on a sealed document; it is in the record.","example":"null on a sealed document"},"mime_type":{"type":["string","null"],"description":"The media type. `null` on a sealed document, for the reason `filename`\nis, and because this service can no longer form an opinion about it.","example":"null on a sealed document"},"resealed_at":{"type":["string","null"],"format":"date-time","description":"When this document was re-sealed, or `null` for one that was born sealed\nor has not been re-sealed yet."},"size_bytes":{"type":["integer","null"],"format":"int64","description":"What was actually stored, in bytes — counted by this service rather than\nclaimed by the client. On a sealed document that is the length of the\nciphertext, which is `byte_class` plus one tag and says nothing the class\ndid not already say."},"storage_key":{"type":["string","null"],"description":"Where the bytes are, relative to the service's document root. Returned\nbecause the owner is entitled to know their file is held and where, and\nit is not a capability: fetching it goes through the content routes,\nwhich authorise by document id and never by path.\n\n`null` on a row written before this service stored bytes at all, which\ndescribed a file the user held themselves. Those have no content to\nfetch.","example":"8c8f…/3d2b….vgs"},"updated_at":{"type":"string","format":"date-time"}}},"DraftRequest":{"type":"object","description":"Request body for `POST /me/agent/draft-application`.","required":["job_title","company","job_description"],"properties":{"company":{"type":"string","description":"Required, at most 200 characters. The one field recorded in the audit\ntrail, so a user can see which applications the agent drafted for them.","example":"Example Corp"},"job_description":{"type":"string","description":"Required, at most 10,000 characters — the posting, pasted in.","example":"We are looking for an engineer to …"},"job_title":{"type":"string","description":"Required, at most 200 characters.","example":"Senior Backend Engineer"}}},"DraftResponse":{"type":"object","description":"Response body: the draft, and an honest account of what produced it.\n\n`provider` is always present so a caller can tell a mock draft from a real\none without reading the text. `model` is omitted entirely by the mock,\nbecause there was no model.","required":["draft","provider"],"properties":{"draft":{"type":"string","description":"The application text, drafted only from facts in the caller's profile."},"model":{"type":["string","null"],"description":"Omitted when the provider named no model: by the mock, because there was\nnone, and by `claude-cli`, because the CLI picked it and does not say\nwhich in text mode."},"provider":{"type":"string","description":"`mock`, `anthropic` or `claude-cli`. Always present, so a caller can\ntell a mock draft from a real one without reading the text.","example":"mock"}}},"DrivingEquipmentEntry":{"type":"object","description":"One kind of equipment the user has operated, and for how long.","required":["id","equipment_type","created_at"],"properties":{"approx_from":{"type":["string","null"],"format":"date"},"approx_to":{"type":["string","null"],"format":"date"},"class_operated":{"type":["string","null"],"description":"The licence class it was operated under, when the user recorded one."},"created_at":{"type":"string","format":"date-time"},"equipment_type":{"type":"string","example":"Tractor-semitrailer"},"id":{"type":"string","format":"uuid"},"total_time_months":{"type":["integer","null"],"format":"int32","description":"Total months at the wheel of it, which is not the span between the\ndates: a driver runs two kinds of equipment in the same years."}}},"EeoSelfIdentification":{"type":"object","description":"The voluntary self-identification block. Every field is nullable, and the\nabsence of a row is a different state from a row of nulls: one is \"never\nasked\", the other is \"asked, and this is the answer\".\n\nNothing in this service reads this type except the two owner endpoints below\nand the `eeo:read` branch of [`attach_for_share`]. It is never a matching\ninput — see the module documentation.","required":["updated_at"],"properties":{"disability_form_version":{"type":["string","null"],"description":"Which revision of CC-305 the answer was given against. The form is\nreissued with an expiry, and an answer to the old wording is not an\nanswer to the new one.","example":"CC-305 (rev. 04/2024)"},"disability_status":{"type":["string","null"],"description":"The CC-305 answer: `yes`, `no`, or `prefer_not_to_say`."},"gender":{"type":["string","null"],"description":"One of `male`, `female`, `non_binary`, `prefer_not_to_say`."},"race_ethnicity":{"type":["array","null"],"items":{"type":"string"},"description":"The EEO-1 categories, as a set."},"updated_at":{"type":"string","format":"date-time"},"veteran_status":{"type":["string","null"],"description":"One of `protected_veteran`, `not_protected_veteran`, `prefer_not_to_say`."},"veteran_subcategories":{"type":["array","null"],"items":{"type":"string"},"description":"Which VEVRAA classifications apply, asked only when `veteran_status` is\n`protected_veteran`."}}},"EmailRequest":{"type":"object","description":"Request body for `POST /auth/kdf-params`, `POST /auth/recovery-challenge`\nand the first field of `POST /auth/recover`.\n\nAn address and nothing else. No `Debug` for the reason every request type in\nthis module lacks one, even though an email is not a credential: the habit is\nwhat keeps a field that *is* one from being added to a `Debug` type later.","required":["email"],"properties":{"email":{"type":"string","example":"ada@example.com"}}},"Employer":{"type":"object","description":"The employer behind a public posting: what `organization` discloses to a\nreader who has no credential.\n\nThree columns and no id-shaped handle onto anything else. There is\ndeliberately no `org_id` here — an organization id is what the member routes\ntake, and a public advertisement has no business handing a stranger the\nargument to `GET /orgs/{id}/members`, even though that route would answer it\nwith a `404`.","required":["name","slug"],"properties":{"name":{"type":"string","example":"Aperture Robotics"},"slug":{"type":"string","example":"aperture-robotics"},"website":{"type":["string","null"]}}},"EncryptedRecord":{"type":"object","description":"One record version, as the server holds it.\n\nEvery component of `AAD_rec` comes back with the row, which is what makes a\nrecord self-describing: a client that has never seen this row before can\nrebuild the exact associated data it was sealed under from the response\nalone, and therefore can tell an honest row from a substituted one.","required":["id","scope","record_type","record_version","write_epoch","byte_class","ciphertext","ct_nonce","wrapped_dek","dek_nonce","created_at","updated_at"],"properties":{"byte_class":{"type":"integer","format":"int32"},"ciphertext":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"ct_nonce":{"type":"string"},"dek_nonce":{"type":"string"},"id":{"type":"string","format":"uuid"},"record_type":{"type":"string"},"record_version":{"type":"integer","format":"int64"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"wrapped_dek":{"type":"string"},"write_epoch":{"type":"integer","format":"int32"}}},"Enrolment":{"type":"object","description":"What `POST /me/totp/enrol` answers with, once.\n\nEvery field is key material and none of it can be read back: the secret is\nsealed before the row is written and the recovery codes are hashed before\nthey are. A client that loses this response has lost the enrolment, and the\nway forward is `DELETE /me/totp` and a second [`enrol`] — which is the honest\noutcome, and much better than a route that would serve a shared secret to\nwhoever held a session.\n\nDeliberately no `Debug`, like [`crate::llm_keys::SetKeyRequest`] and\n[`crate::users::CreateUserRequest`]: one stray `{:?}` would put a second\nfactor in the logs.","required":["secret","otpauth_uri","recovery_codes"],"properties":{"otpauth_uri":{"type":"string","description":"The same secret as an `otpauth://totp/…` URI, for the applications that\ntake a scan. **Text, not an image** — see the module docs.","example":"otpauth://totp/Verigrant:ada@example.com?secret=…&issuer=Verigrant&algorithm=SHA1&digits=6&period=30"},"recovery_codes":{"type":"array","items":{"type":"string"},"description":"Ten one-time codes, shown here and nowhere else again. Each is ten\ncharacters grouped as `XXXXX-XXXXX`; either spelling is accepted back,\nand so is any casing.","example":["4F7KQ-2XAB3"]},"secret":{"type":"string","description":"The shared secret, RFC 4648 base32 with no padding, for the authenticator\napplications that take a typed key. Thirty-two characters.","example":"JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"}}},"EraseAccountRequest":{"type":"object","description":"Request body for `DELETE /me/account`.\n\nDeliberately no `Debug`, like every other type in this service that holds a\nplaintext password: one stray `{:?}` would put it in a log line.\n\n**Two credentials, exactly as `POST /auth/login` takes two.** A version 1\naccount confirms with `password`; a version 2 account confirms with\n`auth_key`, because it *has* no password — signup mints a bootstrap\ncredential, uses it once and forgets it, so the hash in `users.password_hash`\nis a hash of 32 bytes nobody knows. Before this field existed, an encrypted\naccount could not be erased at all: the route asked for a password that\ncould not be produced and answered `403 confirmation_failed` forever, which\nis an erasure right that exists on paper.\n\n**Which one confirms is decided by the account, not by the caller.** An\naccount with a keyring is confirmed by `auth_key` and by nothing else — a\npassword sent for one is refused even if it happens to match the bootstrap\nhash left over from before enrolment, because that hash is not the account's\ncredential any more and honouring it would let somebody who knew the old\npassword destroy the record. An account with no keyring is confirmed by its\npassword. Sending the wrong one of the two is a refusal like any other, and\ndeliberately not a different one: both are checked against something that\nwill not match, at the same cost, and both get the same `403`.","properties":{"auth_key":{"type":["string","null"],"description":"Base64 of the 32-byte `auth_key`, for an account that has registered a\nkeyring — the same credential it signs in with, and the same verifier it\nis checked against. Version 2 accounts only.","example":"base64 of 32 bytes"},"password":{"type":["string","null"],"description":"The account's own password, re-entered. The session is not enough on its\nown: a borrowed laptop with an open tab should not be able to destroy\nsomebody's record. Version 1 accounts only.","example":"the-account-password"}}},"ErasureReceipt":{"type":"object","description":"What the caller is handed back for an account that no longer exists.\n\nA receipt rather than a bare `204`, because this is the one operation whose\neffect the caller can never go and check: every route that could confirm it\nnow refuses their token. The counts are the ones taken inside the\ntransaction, immediately before the delete, and the same map is retained in\n`erasure_log` so an operator can reproduce this answer afterwards.","required":["user_id","erased_at","deleted_rows","document_files_removed"],"properties":{"deleted_rows":{"type":"object","description":"Table name to rows destroyed, for every table in the inventory —\nincluding the zeros, which are as much a part of the account of what\nhappened as the rest.","additionalProperties":{"type":"integer","format":"int64"},"propertyNames":{"type":"string"}},"document_files_removed":{"type":"integer","description":"How many stored document files were removed from disk. Postgres cannot\ncascade to a filesystem, so this is done by hand after the commit.","minimum":0},"erased_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid","description":"The id the account had."}}},"ErrorResponse":{"type":"object","description":"The error envelope every failing route answers with.\n\nA documentation type: [`crate::error::AppError`] builds this body itself,\nfrom a `serde_json` literal, and is an enum rather than a struct — so this\nis what that literal renders to, named so operations can point at it.\n\n`error` is the stable, machine-readable half. `message` is prose for a human\nand may change; branching on it is a mistake the docs should not encourage.","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable code: `bad_request`, `unauthorized`, `not_found`, `conflict`,\n`unavailable`, `internal`, or one of the grant-specific refusals\n(`revoked`, `expired`, `insufficient_scope`, `no_llm_key`, …).","example":"unauthorized"},"message":{"type":"string","description":"Human-readable detail. Never carries key material, a query fragment, or\na database error — those go to the server log instead.","example":"valid bearer token required"}}},"Evidence":{"type":"object","description":"Everything a client needs to refuse a key the operator substituted.\n\nMirrors `vg_crypto::attest::LogInclusion` and\n`vg_crypto::attest::DomainAttestation` together, for the same reason\n[`PublicKeyView`] mirrors `RegisteredKey`.","required":["signed_tree_head","inclusion_proof","tree_size","root_hash","attestation_fetched_from","attestation_document","attestation_signature","verification_state"],"properties":{"attestation_document":{"type":"string","description":"The attestation document as the institution registered it, base64. The\nclient fetches its own copy from the domain and compares; this is what it\ncompares against, not what it trusts."},"attestation_fetched_from":{"type":"string","description":"The host the client must fetch the live attestation from.","example":"acme-university.edu"},"attestation_issued_at":{"type":["string","null"],"format":"date-time","description":"When the institution issued the announcement. Inside the signature, so a\nregistry that edited it would produce a statement that does not verify."},"attestation_issued_at_unix":{"type":["integer","null"],"format":"int64","description":"The same instant as unix seconds, for the reason\n[`PublicKeyView::not_before_unix`] gives."},"attestation_key_id":{"type":["string","null"],"description":"Which of the institution's Ed25519 keys signed it. The client looks that\nkey up among the keys in this same response and checks *its* inclusion\nproof before it will believe the signature.","example":"attest-2026"},"attestation_signature":{"type":"string","description":"Ed25519 over the canonical attestation statement, base64."},"inclusion_proof":{"type":"string","description":"Concatenated 32-byte Merkle nodes, leaf-ward first, base64."},"root_hash":{"type":"string","description":"The root the proof recomputes, base64."},"signed_tree_head":{"type":"string","description":"The signed tree head the inclusion proof is against, base64.\n\nThe **current** head, and the same head for every key in one response, so\nthat a client checks one thing and pins one thing. A proof against the\nhead as it stood when the key was registered would be a proof against a\ntree nobody else is looking at any more; the registration-time head is\nkept in `relying_party_key_attestation` as the record of what was\npublished then, and `relying_party_log_root` holds every root this\nservice has ever signed."},"tree_size":{"type":"integer","format":"int64","description":"The number of leaves in the tree the proof was cut against."},"verification_state":{"type":"string","description":"`unverified`, `structural`, `proven` or `failed`.\n\n`proven` for anything registered since the proofs landed: this service\nverified the institution's signature before storing the row. `structural`\nis what rows registered before that are, and a client will refuse to seal\nto one, which is correct — it was never proved.","example":"proven"},"verified_at":{"type":["string","null"],"format":"date-time"}}},"ExportBundle":{"type":"object","description":"Everything this service holds about one person.\n\nEach section is the same shape the endpoint that owns it returns — the\ngrants are what `GET /me/grants` answers, the notifications what\n`GET /me/notifications` answers, and so on down — because a bundle with\nshapes of its own would be a second contract to keep in step, and the first\ndivergence would be silent. The `profile` section is the one this service\nhas no owner-facing endpoint for since the cutover, so it is the shared\nread's [`FullProfile`] with nothing redacted out of it.","required":["format_version","exported_at","tables","row_counts","user","profile","consents","documents","answers","applications","matches","agent_suggestions","access_requests","vera_ids","capabilities","application_requests","notifications","grants","verifications","sessions","organizations","audit_log"],"properties":{"access_requests":{"type":"array","items":{"$ref":"#/components/schemas/AccessRequest"},"description":"Every access request an employer has made of the owner, pending first,\nwith what was asked for and what they answered. The delegations these\nproduced are in `grants` beside every other one."},"agent_suggestions":{"type":"array","items":{"$ref":"#/components/schemas/ExportedSuggestion"},"description":"Every application the agent has drafted for the owner, newest first, each\nwith the screening answers it resolved from their record and the answer\nbank, and the gaps it could not. Including the ones about postings that\nhave since closed, for the reason the matches are: the bundle is what is\nheld, and `GET /me/agent/suggestions` — which shows only what can still be\nacted on — is a different question."},"answers":{"type":"array","items":{"$ref":"#/components/schemas/StoredAnswer"}},"application_requests":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationRequest"},"description":"Every application an agent has made in the owner's name, pending first,\nwith the institution, the scopes, and the delegation that answered it if\none did. The other direction from `access_requests`, and the reason both\nare here: one is what was asked *of* this person and the other is what\nwas asked *for* them."},"applications":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationRecord"}},"audit_log":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"},"description":"The custody trail: every recorded act on this record, newest first,\nincluding the failed logins against this address."},"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/CapabilitySummary"},"description":"The mandate behind each auto mode Vera ID: which scopes, which limits, what\nit has spent, and whether its revocation has finished. No key material —\n`agent_capability.sealed_payload` is opaque to this service and stays a\ncount in `row_counts`, like every other blob here."},"consents":{"type":"array","items":{"$ref":"#/components/schemas/ConsentRecord"}},"documents":{"type":"array","items":{"$ref":"#/components/schemas/DocumentRecord"},"description":"Document *metadata*. The bytes are not in this document — a JSON bundle\nwith ten megabytes of base64 in it is not a portable record — and are\ndownloaded one at a time from `GET /me/documents/{id}/content`, which\nevery row here carries the id for."},"eeo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EeoSelfIdentification"}]},"exported_at":{"type":"string","format":"date-time"},"format_version":{"type":"integer","format":"int32","description":"The shape of this document. See [`FORMAT_VERSION`].","minimum":0},"grants":{"type":"array","items":{"$ref":"#/components/schemas/GrantSummary"},"description":"Every delegation ever minted, including the revoked and expired ones.\nNo tokens: they were returned once, at creation, and this service kept\nno copy to put here."},"llm_credential":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LlmCredentialRecord","description":"That a key is held, and for whom — never the key. See the module docs."}]},"matches":{"type":"array","items":{"$ref":"#/components/schemas/ExportedMatch"},"description":"Every match this service has computed for the owner, best first, each\nwith the score and the factors behind it. Including the ones for postings\nthat have since closed: the bundle is what is held, and the seeker's\n`GET /me/matches` — which shows only open vacancies — is a different\nquestion."},"notifications":{"type":"array","items":{"$ref":"#/components/schemas/Notification"},"description":"Every notice this service has sent the owner, unread first, with the\ntimestamp at which each was read. The other half of the record: what they\nwere told, beside what they said."},"organizations":{"type":"array","items":{"$ref":"#/components/schemas/Membership"},"description":"The organizations this person may act for, and the role they hold in\neach. Their *colleagues* are not here: a roster belongs to the\norganization, not to one member's record, and an export is the answer to\n\"what do you hold about me\"."},"profile":{"$ref":"#/components/schemas/FullProfile","description":"The part of the record this service can still read: the contact detail,\nthe languages, the references, the credentials, the availability and the\nverification summary.\n\n**Not the whole record, and the difference is the point.** The narrative\ncore, the positions held, the qualifications and the skills are\nciphertext since `20260828000002_retire_plaintext_profile.sql`, and a\nbundle assembled by this service cannot contain what this service cannot\nopen. Section 8.5 says where the complete export comes from instead: the\nserver streams the ciphertext and the keyring, and the client assembles\nthe readable bundle. Until it does, the honest description of this field\nis what is written above it.\n\nOne thing in it is here rather than in a delegate's copy, and on purpose:\nwhile a Verigrant-signed check note is the person's alone,\n`verification_credentials` is attached to *this* document. The person who\nasked for the check is handed the signed statement of it and decides for\nthemselves who ever sees it. See [`crate::verifications::ChecksPolicy`]\nand [`crate::profile::owned`]."},"row_counts":{"type":"object","description":"How many rows were read from each of those tables. The same census the\nerasure receipt reports, so the two can be compared.","additionalProperties":{"type":"integer","format":"int64"},"propertyNames":{"type":"string"}},"sensitive_id":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SensitiveId"}]},"sessions":{"type":"array","items":{"$ref":"#/components/schemas/SessionRecord"},"description":"Session history, without `token_hash`. See the module docs."},"tables":{"type":"array","items":{"type":"string"},"description":"Every physical table this bundle drew from, sorted. Not decoration: it is\nthe claim of completeness, and the end-to-end test holds it against\n`information_schema` so that a table added later cannot go unexported."},"user":{"$ref":"#/components/schemas/ExportedUser"},"vera_ids":{"type":"array","items":{"$ref":"#/components/schemas/VeraIdSummary"},"description":"Every Vera ID this account has handed to an agent, newest first, with\nwhat each has been used for. **Never an id**: they are shown once at\ncreation and stored as a hash, so there is none here to carry.\n\nA section rather than only a row count, on the same argument\n`access_requests` is: the record of which agents were given standing in\nthe owner's name is theirs, and a bundle that counted these rows and\nshowed none of them would be reporting a number about the person instead\nof telling them what is held."},"verifications":{"type":"array","items":{"$ref":"#/components/schemas/Verification"}},"work_eligibility":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WorkEligibility","description":"The regulated islands, each a section of its own here exactly as it is a\nresource of its own in the API. `null` where the owner has recorded\nnothing."}]}}},"ExportedMatch":{"type":"object","description":"One match as the export bundle carries it: the row, without the posting it\njoins to.\n\nA posting is the employer's, and an export of the seeker's record should not\ngrow a copy of somebody else's advertisement. The `posting_id` is enough to\nfetch it from `GET /postings/{id}` while it is open.","required":["posting_id","score","reasons","matched_at","scored_at"],"properties":{"matched_at":{"type":"string","format":"date-time"},"posting_id":{"type":"string","format":"uuid"},"reasons":{"type":"array","items":{"type":"object"}},"score":{"type":"number","format":"double"},"scored_at":{"type":"string","format":"date-time"}}},"ExportedSuggestion":{"type":"object","description":"One suggestion as the export bundle carries it: the row, without the posting\nit joins to.\n\nA posting is the employer's, and an export of one person's record should not\ngrow a copy of somebody else's advertisement — the same decision\n[`crate::matching::ExportedMatch`] makes. The `posting_id` is enough to line\nthis up with the match beside it in the same bundle.\n\nUnlike [`Suggestion`], this reads the row alone, so a suggestion about a\nvacancy that has since been withdrawn is still in the export. The bundle is\nwhat is held; the list route is what can still be acted on.","required":["id","posting_id","draft","filled_answers","gaps","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"draft":{"type":"string"},"filled_answers":{"type":"array","items":{"type":"object"}},"gaps":{"type":"array","items":{"type":"object"}},"id":{"type":"string","format":"uuid"},"posting_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"ExportedUser":{"type":"object","description":"The account row, with the two fields [`crate::users::PublicUser`] withholds\nfrom strangers. Its own type rather than that one, because an export is the\nowner reading their own row: `status` is why a disabled account cannot log\nin, and it would be strange to answer \"everything you hold about me\" with\neverything but that.","required":["id","email","status","created_at","updated_at","notification_email"],"properties":{"created_at":{"type":"string","format":"date-time"},"display_name":{"type":["string","null"]},"email":{"type":"string"},"email_verified_at":{"type":["string","null"],"format":"date-time","description":"When this address was proved, or `null` if it never was. In the bundle\nfor the reason `status` is: it is a fact this service holds about the\naccount and acts on, and the trail below carries the\n`auth.email_verified` entry that says how it got here."},"id":{"type":"string","format":"uuid"},"notification_email":{"type":"boolean","description":"Whether this account asked for its notices to be mailed as well as held."},"status":{"type":"string","description":"`active` or `suspended`. Every account is created `active`; the second\nvalue is written by `POST /admin/users/{id}/suspend`, and the trail below\ncarries the `account.suspended` entry that says when and by whom — which\nis the point of exporting this field rather than withholding it."},"updated_at":{"type":"string","format":"date-time"}}},"FillGapsRequest":{"type":"object","description":"Request body for `POST /me/agent/suggestions/{id}/answers`.","required":["answers"],"properties":{"answers":{"type":"array","items":{"$ref":"#/components/schemas/StoreAnswerRequest"},"description":"One entry per gap being answered, in exactly the body `PUT /me/answers`\ntakes: the question as the posting asked it, and the one value that\nanswers it."}}},"FilledAnswer":{"type":"object","description":"One question and what the engine could do with it.\n\n`Deserialize` as well as `Serialize`, because these are stored: an\n`agent_suggestion` holds a run's worth of them and [`crate::submit`] reads\nthem back to send them. Round-tripping through the column rather than\nre-resolving is what makes a submitted application carry the answers the\nperson actually read and approved.","required":["question","source","confidence"],"properties":{"answer":{"type":["object","null"],"description":"The answer, in the type `answer_type` names, or `null` for a gap."},"answer_type":{"type":["string","null"],"description":"The type of `answer`: what the value *is*, which is the declared type\nwhen the record could be presented in it. `null` for a gap."},"confidence":{"type":"number","format":"float","description":"Between 0 and 1 — see the confidence constants in this module. Exactly 0\nfor a gap.","example":0.95},"field":{"type":["string","null"],"description":"Which canonical field answered, for a `structured` answer: `email`,\n`desired_pay`, `work_authorized`. `null` otherwise.","example":"email"},"fingerprint":{"type":["string","null"],"description":"The identity the answer bank would file this question under, or `null`\nfor a question with nothing to identify it by. Hand it to\n[`learn`]'s `question` field — or rather, hand back the text: the\nfingerprint is derived, and the text is what gets stored."},"question":{"type":"string","description":"The question as it was asked, echoed back so a caller can pair the\nresponse with its own fields."},"source":{"$ref":"#/components/schemas/Source"}}},"FullProfile":{"type":"object","description":"The whole document, as returned by `GET /shared/profile`. Every section the\npresented grant does not reach is emptied by\n[`FullProfile::redacted_for_share`], in one place, rather than by two queries\nthat could disagree.\n\nThere is no longer an owner's copy of this shape: the owner reads their\nrecord through `GET /me/records`, decrypts it in their browser, and never\nasks this service what it says. Each field below is therefore documented by\nthe scope a *delegate* needs for it.","required":["phones","links","addresses","languages","references","credentials","driving_equipment","availability_shifts","verifications"],"properties":{"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"description":"Current, prior and mailing addresses. The current one is on\n`profile:read`; the history is background-check material and needs\n`identity:read`."},"answers":{"type":["array","null"],"items":{"$ref":"#/components/schemas/StoredAnswer"},"description":"The answer bank, on `answers:read`. Each answer with the question it\nanswers, which is what makes it usable by a form-filler."},"availability":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AvailabilityPreference","description":"On `preferences:read`. `null` until the owner has written any\npreferences, and `null` for a delegate whose grant does not carry that\nscope."}]},"availability_shifts":{"type":"array","items":{"$ref":"#/components/schemas/ShiftEntry"},"description":"On `preferences:read`."},"consents":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ConsentRecord"},"description":"The consent trail, on `consents:read`. Records only: never the\nbackground check, drug test or motor-vehicle record they authorised."},"credentials":{"type":"array","items":{"$ref":"#/components/schemas/CredentialEntry"},"description":"Licences and certifications, each with its CDL detail when it has one, on\n`credentials:read`."},"documents":{"type":["array","null"],"items":{"$ref":"#/components/schemas/DocumentRecord"},"description":"Document metadata, on `documents:read`. Never any bytes — those come\nfrom `GET /shared/documents/{id}/content`, on the same scope: see\n[`crate::documents`]."},"driving_equipment":{"type":"array","items":{"$ref":"#/components/schemas/DrivingEquipmentEntry"},"description":"What the user has driven. Experience rather than a credential, so the\nspec files it with the history: on `history:read`."},"eeo":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/EeoSelfIdentification","description":"EEO self-identification, on `eeo:read`. Never an input to matching,\nsearch or ranking — see [`crate::regulated`]."}]},"languages":{"type":"array","items":{"$ref":"#/components/schemas/LanguageEntry"},"description":"On `history:read`."},"links":{"type":"array","items":{"$ref":"#/components/schemas/LinkEntry"},"description":"On `profile:read`."},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"description":"On `profile:read`: a way to contact somebody is the least a delegation\nto read their profile can be worth."},"references":{"type":"array","items":{"$ref":"#/components/schemas/ReferenceEntry"},"description":"People who will vouch for the owner. Always empty in the delegate's\nview, whatever the grant carries — see\n[`FullProfile::redacted_for_share`]."},"sensitive_id":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SensitiveId","description":"Date of birth and SSN last four, on `identity:read` — the same scope\nthat unlocks the address history above."}]},"verification_credentials":{"type":["object","null"],"description":"The same summary, signed — one `cred.v1` attestation per *verified*\nkind, keyed the same way.\n\nThe map above is a claim we are making on a connection the reader has to\ntrust; these are the ones they can keep. Whoever saves a credential can\nshow it onward, months later, to somebody who never held a grant and\nnever calls us — the signature is checkable from the platform public key\nalone. See [`crate::attest`].\n\n**Which party gets them is the whole of what\n[`crate::verifications::ChecksPolicy`] decides.** Under the default they\nare attached to the owner's own export and to nothing else, so the signed\nnote reaches the person who asked for the check and travels onward only\nif they carry it. Under [`crate::verifications::ChecksPolicy::Institutions`]\nthey are attached to a delegate's copy instead, on the same\n`profile:read` the summary used to ride on, which is the behaviour this\nfield had before the flag existed.\n\nOnly `verified` kinds appear either way, so this map is a subset of\n`verifications` and often a smaller one.","additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}},"verification_status":{"type":["string","null"],"description":"What a delegate is told about this record's checks instead of the two\nfields above.\n\nPresent only in a delegate's copy and only while a check note is the\nperson's alone, and always the one sentence\n[`crate::verifications::STATUS_FROM_THE_APPLICANT`] holds. A reader gets\na statement rather than an empty map because an empty map is read as\n\"never checked\" and sometimes as \"checked and failed\", and neither is\nwhat withholding a note means.\n\nAbsent from the owner's own copy, which carries the summary itself and\nhas nobody to be told about."},"verifications":{"type":"object","description":"Which of this record's claims somebody else has checked, as\nkind→status: `{\"identity\": \"verified\", \"employment\": \"pending\"}`.\n\nThis is now the only thing in the document that speaks to what the record\n*says*, and it does so without saying it: a badge is an assertion about a\nclaim, not a copy of it. A kind that has never been checked is absent —\nsee [`crate::verifications::summary`].\n\n**Empty in a delegate's copy under the default policy**, whatever the\ngrant carries, and `verification_status` below is what stands in its\nplace. See [`crate::verifications::ChecksPolicy`].","additionalProperties":{"$ref":"#/components/schemas/Status"},"propertyNames":{"type":"string"}},"work_eligibility":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WorkEligibility","description":"Work eligibility, on `eligibility:read`."}]}}},"GrantActivity":{"type":"object","description":"One entry of the custody trail, as `GET /me/grants/{id}/activity` returns it.\n\nThe columns of `audit_log`, and nothing derived: this endpoint reports what\nwas recorded rather than interpreting it, because the trail is append-only\nand a reader comparing two entries needs them in the shape they were\nwritten.","required":["id","action","detail","created_at"],"properties":{"action":{"type":"string","description":"What happened: `grant.created`, `grant.revoked`, `profile.shared`,\n`eligibility.shared`, `identity.shared`, `eeo.shared`,\n`consents.shared`, `agent.draft`.","example":"profile.shared"},"created_at":{"type":"string","format":"date-time"},"detail":{"type":"object","description":"The recorded detail. Symbolic throughout — grant ids, scope names, the\ncompany a draft was for — and never a value from the record the grant\nunlocked. A trail that copied the sensitive value into a second table\nwould have doubled the problem it exists to solve."},"id":{"type":"integer","format":"int64","description":"The trail's own sequence number. Monotonic, so a client polling this can\ntell \"nothing new\" from \"the same second, again\"."},"ip":{"type":["string","null"],"description":"The address the request came from, or `null` where none was recorded.","example":"203.0.113.7"}}},"GrantMetrics":{"type":"object","description":"The delegation counters. `active`, `revoked` and `expired` partition\n`issued`: a grant is withdrawn, or timed out, or good right now, and never\ntwo of those.","required":["issued","active","revoked","expired"],"properties":{"active":{"type":"integer","format":"int64"},"expired":{"type":"integer","format":"int64"},"issued":{"type":"integer","format":"int64"},"revoked":{"type":"integer","format":"int64"}}},"GrantRefused":{"type":"object","description":"The non-`200` body of [`verify`].\n\nA refusal is a normal answer here rather than an error — the endpoint exists\nto be told \"no\" — so it keeps its own shape instead of the standard error\nenvelope, and the reason is meant to be branched on.","required":["valid","reason"],"properties":{"reason":{"type":"string","description":"`malformed`, `invalid_signature`, `expired`, `unknown_grant`, `revoked`,\nor `unavailable`.","example":"revoked"},"valid":{"type":"boolean","description":"Always `false`."}}},"GrantStatus":{"type":"string","description":"Where a grant stands, as one word.\n\nThree states and no fourth: a grant is either usable, past its `expires_at`,\nor withdrawn by its owner. Computed by [`status_of`] rather than stored,\nbecause two of the three are a function of the clock and a stored copy would\nbe wrong for exactly as long as nobody rewrote it.","enum":["active","expired","revoked","exhausted"]},"GrantSummary":{"type":"object","description":"One row of `GET /grants`. No token and no key material: a listing is a\nmanagement view, and re-issuing a credential from it would defeat the\nonce-only rule in [`create`].","required":["grant_id","scopes","created_at","use_count","issued_by","status","bundle_kinds"],"properties":{"audience":{"type":["string","null"],"description":"The institution this delegation was issued to, by the slug the public\nregistry is keyed on, or `null` for one that names none.\n\nThe same value `POST /grants/verify` reports, resolved the same way. It\nis here because the two human paths now bind the grants they mint — see\n[`crate::applications::apply`] — so \"who may read this\" is a question the\nowner's own list has to be able to answer without a second call, and\nbecause a slug is what a client seals to."},"bundle_kinds":{"type":"array","items":{"type":"string"},"description":"Which stages have actually been sealed under it: `preview`, `full`, both,\nor none.\n\nThe pair of this and `audience` is the one thing about a delegation its\nowner can still put right. A grant naming an institution with nothing\nsealed under it is a **permission over ciphertext the holder cannot\nopen** — the state a failed seal leaves behind, and the state every grant\nminted before its employer registered a key is in — and the owner's\nclient offers to seal it. A grant that already carries a bundle of a kind\ncannot be sealed again under it, because the entries name record versions\nand a second bundle would retire a disclosure the institution may already\nhave pulled; so this is also what tells a client not to offer.\n\nNever a sealed key and never a record id: the kinds and nothing else. The\nmanifest is `GET /me/grants/{id}/bundles`."},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"grant_id":{"type":"string","format":"uuid"},"issued_by":{"type":"string","description":"Which Vera ID's agent issued this, if one did: `user` for a delegation\nthe account issued itself, `agent` for one auto-issued under a private\nVera ID's capability."},"label":{"type":["string","null"]},"last_used_at":{"type":["string","null"],"format":"date-time","description":"When this delegation was last accepted on a request that then succeeded,\nor `null` for one that has never been used. Minting a grant is not using\nit, so a freshly created grant reads `null` rather than its own\n`created_at`."},"max_lookups":{"type":["integer","null"],"format":"int32","description":"How many reads this delegation permits in total, or `null` for an\nuncapped one. A grant whose `use_count` has reached this is refused as\n`exhausted`, and its `status` says so."},"revoked_at":{"type":["string","null"],"format":"date-time"},"scopes":{"type":"array","items":{"type":"string"}},"status":{"$ref":"#/components/schemas/GrantStatus","description":"`active`, `expired` or `revoked`, from the same rule [`authorize`]\napplies, evaluated against the clock at the moment of the request. A\ngrant listed as anything but `active` is one every route refuses."},"use_count":{"type":"integer","format":"int64","description":"How many times that has happened, counting every accepted presentation\nof the token — a `POST /grants/verify` as well as a read through\n`/shared/*`."}}},"GrantVerified":{"type":"object","description":"The `200` body of [`verify`]: the delegation, as the *row* describes it.\n\nNot the token's copy of the claims. The two agree today, but the row is what\nrevocation acts on, so it is the one an authorisation decision may read.","required":["valid","user_id","scopes","grant_id","relying_party_required"],"properties":{"audience":{"type":["string","null"],"description":"The slug of the institution this delegation was issued to, when it names\none. `null` means it names none, and any registered institution may\nredeem it.\n\nDeliberately the *slug* and not the id: a slug is what the public\nregistry at `GET /rp/{slug}/keys` is keyed on, so a holder can look up\nwho they are meant to be, and it is already public."},"expires_at":{"type":["string","null"],"format":"date-time"},"grant_id":{"type":"string","format":"uuid"},"relying_party_required":{"type":"boolean","description":"**Always `true`, and the field a caller most needs to read.**\n\n`valid: true` above means this token is signed by us, on file, unexpired\nand unrevoked. It does **not** mean the caller may redeem it. Since the\nrelying-party gate, reading a record also requires the credential of a\nregistered institution — see `docs/product-model.md`, and\n[`crate::relying_party`].\n\nThis endpoint stays open, because the token in the body is the credential\nbeing asked about and the answer is the same whoever asks: it discloses\nno field of anybody's record, and the third-party agents that call it hold\nno other credential of ours. What would be dishonest is answering\n`valid: true` to somebody who cannot use it and letting them find out at\nthe read, so the constraint is reported here instead."},"scopes":{"type":"array","items":{"type":"string"}},"user_id":{"type":"string","format":"uuid","description":"The user the holder is now acting for."},"valid":{"type":"boolean","description":"Always `true` here. Present so a caller can branch on one field across\nboth shapes without first looking at the status code."}}},"InboundAddress":{"type":"object","description":"What a sender needs before it can seal anything: `GET /inbound/{user_id}`.\n\nEvery Verigrant ID resolves to this. The route *is* the resolution — there is\nno separate address format to allocate, publish and keep in step with the\naccount it names, because an account id already identifies exactly one inbox\nand already appears in the provenance statement.","required":["user_id","box_pub","key_epoch","key_fingerprint","schemas","max_byte_class","seal_overhead_bytes"],"properties":{"box_pub":{"type":"string","description":"The recipient's X25519 public key, base64. What the payload is sealed to,\nand what the provenance statement commits to, so that a push sealed under\na superseded keyring is refused rather than stored unopenable."},"key_epoch":{"type":"integer","format":"int32","description":"Which generation of that key this is, starting at 1.\n\nThe field a sender with a cache actually reads. Comparing an integer it\nstored beside a key against the integer this route serves is the whole of\n\"is what I hold still current\", and a sender may send it back on the push\nas [`PushRequest::recipient_key_epoch`] to have the question settled\nbefore it seals anything the wrong way.\n\nIt advances only when an account's published key actually changes, which\nthe three ordinary re-keying events do not do. See the module\ndocumentation, which is where that is argued, and\n[`crate::encrypted::write_keyring`], which is where it is enforced."},"key_fingerprint":{"type":"string","description":"SHA-256 of `box_pub`, base64.\n\nRedundant beside the key itself and deliberately so. It is the value a\nsender can write in a log line, quote in a support conversation and\ncompare by eye, none of which is true of 32 bytes of key material that\nlooks like every other 32 bytes of key material. It commits to nothing\nsecret: the key is public, and so is a digest of it."},"max_byte_class":{"type":"integer","format":"int32","description":"The largest padding class a payload may be padded to.","minimum":0},"schemas":{"type":"array","items":{"type":"string"},"description":"The schemas this deployment will tag. See [`INBOUND_SCHEMAS`]."},"seal_overhead_bytes":{"type":"integer","description":"What the seal adds on top of the padding class, so a sender can compute\nthe exact length this service will accept.","minimum":0},"user_id":{"type":"string","format":"uuid","description":"The account this address names, echoed back so a client that followed a\nredirect knows what it resolved to."}}},"InboxItem":{"type":"object","description":"One inbound item's envelope: everything about it except what it says.","required":["id","doc_type","schema","schema_version","byte_class","key_epoch","payload_hash","provenance","received_at","blocked"],"properties":{"blocked":{"type":"boolean"},"byte_class":{"type":"integer","format":"int32"},"doc_type":{"type":"string"},"id":{"type":"string","format":"uuid"},"key_epoch":{"type":"integer","format":"int32","description":"Which generation of the owner's X25519 key these bytes were sealed to.\n\nOn the item rather than only on the address because it is a fact about\n*these bytes* rather than about the account: after a key that genuinely\nmoved, this is what tells a client which of its items predate the move,\nwithout it having to try to open each one and read failure as an answer.\nIn the hierarchy this product ships it is 1 on every row, and the module\ndocumentation says why that is the design rather than a coincidence."},"payload":{"type":["string","null"],"description":"The sealed payload, base64 — **only** on the single-item read.\n\nAbsent from the list deliberately. The inbox list is a triage screen and\na page of fifty items at the largest padding class would be six megabytes\nof ciphertext nobody asked for; it is also the clearest demonstration of\nthe property this module claims, because the list is an envelope and\nnothing else."},"payload_hash":{"type":"string","description":"SHA-256 of the ciphertext, base64."},"provenance":{"$ref":"#/components/schemas/Provenance"},"read_at":{"type":["string","null"],"format":"date-time"},"received_at":{"type":"string","format":"date-time"},"schema":{"type":"string"},"schema_version":{"type":"string"}}},"InviteMemberRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/members`.","required":["email"],"properties":{"email":{"type":"string","description":"The address of an **existing** account. Matched case-insensitively,\nbecause `users.email` is `citext`.","example":"recruiter@example.com"},"role":{"type":["string","null"],"description":"Defaults to `recruiter`. Only an owner may grant `owner`.","example":"recruiter"}}},"Invoice":{"type":"object","description":"One invoice.","required":["id","org_id","period_start","period_end","total_cents","status","lines","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"lines":{"type":"integer","format":"int64","description":"How many pull-throughs are on it, counted from the rows themselves rather\nthan stored, so the number cannot drift from the line items\n`GET /orgs/{id}/billing/usage` serves."},"org_id":{"type":"string","format":"uuid"},"period_end":{"type":"string","format":"date-time"},"period_start":{"type":"string","format":"date-time"},"status":{"type":"string","description":"`draft`, `open`, `paid` or `void`. **Every invoice this build produces is\n`draft`**, because no payment gateway is configured — see the module\nheader.","example":"draft"},"total_cents":{"type":"integer","format":"int64","example":125000}}},"IssueRequest":{"type":"object","description":"Request body for `POST /agent/grants`.","required":["request_id","grant_id","kind","scopes","expires_at","max_lookups","rp_key_id","statement","statement_sig","entries"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/BundleEntry"}},"expires_at":{"type":"string","format":"date-time","description":"When the grant ends. Must be within `grant_ttl_days` of now."},"grant_id":{"type":"string","format":"uuid","description":"The id the grant will have, minted by the agent.\n\nThe agent has to know it before it can seal: a bundle entry is bound to\nits grant id in the associated data, exactly as a record's ciphertext is\nbound to its record id. See [`crate::grants::issue_agent_grant`]."},"kind":{"type":"string","description":"`preview` or `full`. Must be `preview` if the preset is `preview_only`."},"label":{"type":["string","null"]},"max_lookups":{"type":"integer","format":"int32","description":"The read cap. Must be at most `max_lookups_per_grant`."},"request_id":{"type":"string","format":"uuid","description":"The application this answers."},"rp_key_id":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"statement":{"type":"string","description":"The statement, signed by the **agent's** key. `issuer` must be `agent`\nand `capability_id` must name this capability."},"statement_sig":{"type":"string"}}},"Issued":{"type":"object","description":"The answer to `POST /agent/grants`.","required":["grant_id","token","expires_at","max_lookups","grants_remaining","bundle"],"properties":{"bundle":{"$ref":"#/components/schemas/BundleSummary"},"expires_at":{"type":"string","format":"date-time"},"grant_id":{"type":"string","format":"uuid"},"grants_remaining":{"type":"integer","format":"int32","description":"How many issuances this capability has left."},"max_lookups":{"type":"integer","format":"int32"},"token":{"type":"string","description":"**Shown once**, to the agent that issued it, which is the party that\napplied and therefore the party that has to deliver it to the\ninstitution. This service keeps no copy."}}},"IssuedCredential":{"type":"object","description":"The `200` body of [`credential`]: the signed attestation, and the row it was\nminted from.\n\nThe credential is repeated here rather than returned bare so the holder can\nsee what they are about to hand on — a `cred.v1.…` string is not readable by\neye, and a person forwarding one is entitled to know which claim it carries.","required":["verification_id","kind","status","credential","issued_at"],"properties":{"credential":{"type":"string","description":"`cred.v1.<payload>.<sig>`, checkable by `POST /credentials/verify` or by\nanyone holding the platform's public key, offline.","example":"cred.v1.eyJzdWJqZWN0X3VzZXJfaWQiOi…"},"expires_at":{"type":["string","null"],"format":"date-time"},"issued_at":{"type":"string","format":"date-time","description":"When this copy was minted. A credential is a statement as of a moment,\nand asking again tomorrow mints a fresh one saying the same thing."},"kind":{"$ref":"#/components/schemas/Kind"},"status":{"$ref":"#/components/schemas/Status","description":"Always `verified`: nothing else is minted. See [`crate::attest`]."},"verification_id":{"type":"string","format":"uuid"}}},"JobQueueMetrics":{"type":"object","description":"The queue by status, and the one figure that says whether anybody has\nnoticed.\n\n`failed` has been counted here since the queue existed, and a count on its\nown is a number an operator reads as \"some\" — it does not distinguish three\nfailures from this morning, which is a vendor having a bad hour, from one\nfailure from a fortnight ago, which is work this service silently dropped\nwhile everybody watched the number and saw it not move.\n[`JobQueueMetrics::oldest_failed_age_seconds`] is what tells those apart, and\nit is why this block leads a console rather than sitting under the\nverification counters.\n\n`cancelled` joins the four because\n`20260905000012_job_cancelled_status.sql` added the status: a block that\ncounted four of the five statuses would be a total nobody could reconcile\nwith `SELECT status, count(*) FROM job_queue`.","required":["queued","running","done","failed","cancelled"],"properties":{"cancelled":{"type":"integer","format":"int64","description":"Jobs an administrator stopped through `POST /admin/jobs/{id}/cancel`."},"done":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64"},"oldest_failed_age_seconds":{"type":["integer","null"],"format":"int64","description":"How long ago the *oldest* failure happened, in seconds, or null when\nnothing has failed.\n\nMeasured on `updated_at`, which for a failed row is the moment it reached\nthat status and does not move again — so this is the age of the failure\nrather than the age of the job, and a job that spent a day backing off\nbefore it gave up is not reported as a day-old problem."},"queued":{"type":"integer","format":"int64"},"running":{"type":"integer","format":"int64"}}},"KdfParamsResponse":{"type":"object","description":"Response body for `POST /auth/kdf-params`.\n\nEverything a client needs to derive `KEK_pw` and `auth_key` from a password,\nand nothing that would let anybody else derive them. In particular **never\nthe wrapped DMK**: that is served by `GET /me/keyring`, behind a session, to\nthe account itself. Handing the wrap to whoever can name an address would\nturn one stolen password into an offline attack the account never sees.","required":["user_id","crypto_version","salt","params","issued_at","expires_at","public_key","signature"],"properties":{"crypto_version":{"type":"integer","format":"int32","description":"Always 2. See [`CRYPTO_VERSION`]."},"expires_at":{"type":"string","format":"date-time","description":"When it stops being an answer: [`KDF_PARAMS_TTL_MINUTES`] after\n`issued_at`, and part of the signed bytes for the reason the whole\nmessage is signed. A client that finds this in the past derives nothing\nand refuses; see `web::client::kdf_params::verify`."},"issued_at":{"type":"string","format":"date-time","description":"When this answer was made, and part of the signed bytes."},"params":{"$ref":"#/components/schemas/KdfParamsWire"},"public_key":{"type":"string","description":"The Ed25519 key the signature below is checked against: the transparency\nlog's publication key, which is the key a client pins. Published here so\nthat a client which has one pinned can compare, and a client which does\nnot can at least tell two answers apart.","example":"base64 of 32 bytes"},"salt":{"type":"string","description":"The 16-byte Argon2id salt, base64.","example":"base64 of 16 bytes"},"signature":{"type":"string","description":"Ed25519 over `\"vgrp.kdf.v1.\" || canonical bytes of the fields above`.\nSee [`kdf_params_message`] for the exact encoding a client rebuilds.","example":"base64 of 64 bytes"},"user_id":{"type":"string","format":"uuid","description":"The account's id, which is the HKDF `info` for both derived keys. A\ndecoy answer carries a deterministic id shaped like a v4 UUID."}}},"KdfParamsWire":{"type":"object","description":"The Argon2id parameters a client needs before it can stretch a password.","required":["alg","m_kib","t","p"],"properties":{"alg":{"type":"string","example":"argon2id"},"m_kib":{"type":"integer","format":"int32","description":"Memory cost in KiB.","example":65536},"p":{"type":"integer","format":"int32","description":"Lanes. Always 1 in a browser, which has no threads without cross-origin\nisolation; see section 1.3.","example":1},"t":{"type":"integer","format":"int32","description":"Time cost, in passes.","example":3}}},"KeyStatus":{"type":"object","description":"What every endpoint here reports: which provider, whether a key is held,\nand — on `GET` — the hint.\n\n`present: false` serialises alone, with no `provider` and no `hint`, so a\ncaller with no key stored gets `{\"present\":false}` exactly.","required":["present"],"properties":{"hint":{"type":["string","null"],"description":"The last four characters of the stored key, so a user can tell which key\nthey stored. Returned by `GET` only, and only ever four characters.","example":"…7f2a"},"present":{"type":"boolean"},"provider":{"type":["string","null"],"example":"anthropic"}}},"Keyring":{"type":"object","description":"`GET /me/keyring` — everything a client needs to rebuild its key hierarchy,\nand nothing that would let anybody else rebuild it.\n\n`auth_verifier` is not a field of this type, so it cannot leak through it.","required":["kdf_alg","kdf_salt","kdf_m_kib","kdf_t","kdf_p","dmk_slot_id","wrapped_dmk","dmk_nonce","recovery_slot_id","recovery_wrapped_dmk","recovery_nonce","box_pub","sign_pub","box_epoch","scope_epochs","scopes_pending_rotation","created_at","updated_at"],"properties":{"box_epoch":{"type":"integer","format":"int32","description":"Which generation of `box_pub` this account is publishing, starting at 1.\n\nThe number `GET /inbound/{user_id}` serves to a sender, served back to\nthe account itself so that the two readings cannot drift: a person whose\ninbox shows an item sealed under generation 1 can see that their account\nis still on generation 1, which is the only way that item's\n`key_epoch` means anything to them.\n\nIt advances only when the published key actually changes, which a\npassword change, a recovery reset and a claim do not do. See\n[`write_keyring`], where the rule is a clause of the statement rather\nthan a convention."},"box_pub":{"type":"string"},"box_rotated_at":{"type":["string","null"],"format":"date-time","description":"When it last moved, or `null` for an account whose key has never moved."},"created_at":{"type":"string","format":"date-time"},"dmk_nonce":{"type":"string"},"dmk_slot_id":{"type":"string","format":"uuid"},"kdf_alg":{"type":"string"},"kdf_m_kib":{"type":"integer","format":"int32"},"kdf_p":{"type":"integer","format":"int32"},"kdf_salt":{"type":"string"},"kdf_t":{"type":"integer","format":"int32"},"recovery_nonce":{"type":"string"},"recovery_slot_id":{"type":"string","format":"uuid"},"recovery_wrapped_dmk":{"type":"string"},"scope_epochs":{"type":"array","items":{"$ref":"#/components/schemas/ScopeEpoch"},"description":"The current epoch of every scope this user has written a record in.\n\nReturned with the keyring rather than from a route of its own because a\nclient needs both together and always at the same moment: the DMK comes\nout of the wrap, and the epoch says which `SRK_{scope,epoch}` to derive\nfrom it. A client holding one without the other can do nothing."},"scopes_pending_rotation":{"type":"array","items":{"type":"string"},"description":"The scopes whose root key a revoked agent still holds, oldest debt first.\n\nServed here rather than from a route of its own for the reason\n`scope_epochs` is: a client reads this at sign-in and at every unlock, and\nboth answers are about the same key. An account owing nothing — which is\nalmost every account almost always — therefore learns so without one extra\nrequest, which is what lets the client's rotation be automatic rather than\na thing somebody has to press.\n\nA scope named here is one where a revoked agent's `SRK_{s,e}` is still\nlive, and forward-covering, so it opens everything its owner writes in\nthat scope until the client calls `POST /me/scopes/{scope}/rotate`. See\n[`crate::rotation`]."},"sign_pub":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"wrapped_dmk":{"type":"string"}}},"Kind":{"type":"string","description":"What can be checked.\n\nA closed set, and the same one the `verifications_kind_check` constraint\nenforces in the database. It is closed because each kind implies a different\nchecker and a different meaning downstream: `identity` is a document check,\n`employment` and `income` are payroll ones, `education` is a registry\nlookup. A free-text kind would be a string nobody could act on.","enum":["identity","employment","education","income"]},"LanguageEntry":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"proficiency":{"type":["string","null"],"description":"One of `native`, `fluent`, `professional`, `limited`, or absent."}}},"LinkEntry":{"type":"object","required":["id","label","url","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"label":{"type":"string","description":"One of `linkedin`, `github`, `portfolio`, `website`, `other`."},"url":{"type":"string"}}},"LlmCredentialRecord":{"type":"object","description":"That a provider key is in the vault, and since when.","required":["provider","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"provider":{"type":"string","example":"anthropic"},"updated_at":{"type":"string","format":"date-time"}}},"LogProof":{"type":"object","description":"The answer to `GET /rp/keys/log/proof`.\n\nOne shape for both proof kinds, because they are one question asked from two\ndirections — \"is this leaf in that tree\" and \"is that tree a prefix of this\none\" — and a client asks both of the same endpoint in the same round trip.","required":["kind","tree_size","root_hash","signed_tree_head","proof"],"properties":{"first_root_hash":{"type":["string","null"],"description":"For a consistency proof, that earlier tree's root, base64."},"first_tree_size":{"type":["integer","null"],"format":"int64","description":"For a consistency proof, the earlier tree size it starts from."},"kind":{"type":"string","description":"`inclusion` or `consistency`.","example":"inclusion"},"leaf_index":{"type":["integer","null"],"format":"int64","description":"For an inclusion proof, the leaf index it is for."},"proof":{"type":"string","description":"Concatenated 32-byte Merkle nodes, base64."},"root_hash":{"type":"string","description":"That tree's root, base64."},"signed_tree_head":{"type":"string","description":"The signed head over it, base64. Present so one request yields both the\nproof and the artifact to check it against."},"tree_size":{"type":"integer","format":"int64","description":"The tree the proof is against."}}},"LoginRequest":{"type":"object","description":"Request body for `POST /auth/login`.\n\nNo `Debug`, for the same reason as `CreateUserRequest`: it holds a\nplaintext password — or, for an encrypted account, something that is not a\npassword but is still a credential.\n\n# The two credentials, and why there are two\n\nA version 1 account sends `password` and is verified against\n`users.password_hash`, exactly as it always has been.\n\nA version 2 account sends `auth_key` and is verified against\n`user_keyring.auth_verifier`. Those 32 bytes are one of the two independent\nhalves of the client-side Argon2id stretch described in section 1.3 of\n`docs/zero-knowledge-architecture.md`; the other half never leaves the\nbrowser and is what wraps the account's master key. So an encrypted account\nauthenticates *without this service ever receiving the password*, which is\nthe point: a stolen `auth_verifier` is a login, and it is not a step towards\nreading a record.\n\nNothing in the verification path below changes between the two. The stored\nverifier is an ordinary argon2 PHC string in the same shape\n`users.password_hash` uses, which is why `user_keyring` stored it that way\nrather than inventing a second format. The cheap parameters it was written\nwith are correct for the reason that section gives: the input is already 256\nbits of material the client spent 64 MiB of Argon2id producing, so a second\nexpensive stretch here would cost a tenth of a second per login and buy\nnothing.","required":["email"],"properties":{"auth_key":{"type":["string","null"],"description":"Base64 of the 32-byte `auth_key`, for an account that has registered a\nkeyring. Version 2 accounts only.","example":"base64 of 32 bytes"},"email":{"type":"string","example":"ada@example.com"},"password":{"type":["string","null"],"description":"The account's password. Version 1 accounts only.","example":"choose-a-long-passphrase"}}},"LoginResponse":{"type":"object","description":"Response body for `POST /auth/login`. Also deliberately not `Debug`: the\nraw token is a bearer credential and must not reach a log line.","required":["token","expires_at"],"properties":{"expires_at":{"type":"string","format":"date-time","description":"30 days out. The session can also end sooner, by logout or by the\naccount being disabled."},"token":{"type":"string","description":"The session bearer token: 32 random bytes as unpadded URL-safe base64.\nReturned once, here, and never stored in a form that could reproduce it\n— only `sha256(token)` is kept.\n\n`pub(crate)` for one reader: [`crate::claim::claim`], which answers with\na session *and* the account row beside it, because a claimant needs the\naddress the local marker is keyed by and has never been told it. It mints\nthat session through [`issue_session`] like everything else, and this is\nhow it gets the two fields out of what that returns.","example":"<43-character opaque session token>"}}},"ManifestEntry":{"type":"object","description":"One line of the manifest.","required":["record_id","record_version","scope","record_type"],"properties":{"record_id":{"type":"string","format":"uuid"},"record_type":{"type":"string","example":"work_history"},"record_version":{"type":"integer","format":"int64"},"scope":{"type":"string","example":"history:read"}}},"MarkedRead":{"type":"object","description":"How many notices one call to `…/read-all` cleared.","required":["marked"],"properties":{"marked":{"type":"integer","format":"int64","description":"Rows that went from unread to read. Zero when there was nothing to clear,\nwhich is a success and not a 404.","example":3,"minimum":0}}},"Membership":{"type":"object","description":"An organization as one of its members sees it: the organization, plus the\ncaller's own place in it.\n\nOne type for `POST /orgs` and `GET /me/orgs` rather than an `Organization` and\na separate role field, because in this service there is no such thing as\nreading an organization you do not belong to — so a shape that could describe\none would be a shape no route can produce.","required":["member_id","org_id","name","slug","role","created_at","joined_at"],"properties":{"created_at":{"type":"string","format":"date-time","description":"When the organization was created."},"joined_at":{"type":"string","format":"date-time","description":"When the caller joined it. The same instant as `created_at` for whoever\ncreated it."},"member_id":{"type":"string","format":"uuid","description":"The caller's `org_member` row. This is the id `PATCH` and `DELETE` on\n`/orgs/{id}/members/{member_id}` take, which matters for the one case\nwhere a member acts on their own row."},"name":{"type":"string","example":"Aperture Robotics"},"org_id":{"type":"string","format":"uuid"},"role":{"type":"string","description":"`owner`, `admin` or `recruiter`.","example":"owner"},"slug":{"type":"string","example":"aperture-robotics"},"website":{"type":["string","null"]}}},"Metrics":{"type":"object","description":"What the service currently holds, counted.\n\nAlmost every number here is a `count(*)` over a whole table, taken in one\nstatement so that the figures are one snapshot rather than a series of reads\nthat disagree with each other. The exception is [`Metrics::documents`], which\nis a question about a filesystem rather than about rows and is therefore\n*reported* rather than counted: it is read, in the same statement, from the\nrow the hourly reconcile sweep last wrote. None of them names a person.","required":["users","grants","verifications","postings","applications","agent","job_queue","documents","generated_at"],"properties":{"agent":{"$ref":"#/components/schemas/AgentMetrics"},"applications":{"type":"integer","format":"int64","description":"Applications recorded by their applicants, whether they were sent through\nthis service or logged after the fact."},"documents":{"$ref":"#/components/schemas/DocumentMetrics","description":"What the last pass of `documents.reconcile` found. The one figure on this\npage that is not counted by the statement below, for the reason\n[`DocumentMetrics`] gives."},"generated_at":{"type":"string","format":"date-time","description":"When the snapshot was taken, so a dashboard can say how old it is."},"grants":{"$ref":"#/components/schemas/GrantMetrics"},"job_queue":{"$ref":"#/components/schemas/JobQueueMetrics","description":"The queue by status, and how old the oldest failure is. The block a\nconsole leads with, and the one whose rows are now reachable: see\n[`crate::jobs::list_jobs`]."},"postings":{"$ref":"#/components/schemas/PostingMetrics"},"users":{"$ref":"#/components/schemas/UserMetrics"},"verifications":{"$ref":"#/components/schemas/VerificationMetrics"},"watch":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/WatchStatus","description":"What the machine's own watchdog last found, or `null` where nothing is\nwatching.\n\nThe only block here that is neither counted nor written by this service:\n`deploy/verigrant-watch.sh` runs every five minutes as root and checks\nthe things a process cannot honestly check about itself — its own unit,\nthe certificate nginx serves, the disk, last night's backup — and leaves\na file behind. This is that file. `null` means it is not there, which is\nthe state \"nobody is watching\" and is deliberately not the same value as\na clean result. See [`crate::watch`]."}]}}},"MintApiKeyRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/relying-party/api-keys`.","required":["label"],"properties":{"expires_in_days":{"type":["integer","null"],"format":"int64","description":"Days until it expires. Defaults to [`DEFAULT_API_KEY_DAYS`].","example":365},"label":{"type":"string","description":"What this key is for, so a roster of five can be audited. Never the key.","example":"ats-production"}}},"MintVeraIdRequest":{"type":"object","description":"Request body for `POST /me/vera-ids`.","properties":{"label":{"type":["string","null"],"description":"What the user calls this agent in their own list.","example":"My job-hunting agent"}}},"MintedApiKey":{"type":"object","description":"A freshly minted API key. The only time the token itself is ever shown.","required":["id","label","token","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"id":{"type":"string","format":"uuid"},"label":{"type":"string","example":"ats-production"},"token":{"type":"string","description":"The credential, prefixed with [`API_KEY_PREFIX`]. **Returned once.** This\nservice stores only its SHA-256 and cannot show it again."}}},"MintedVeraId":{"type":"object","description":"The one moment an id exists on our side.","required":["id","kind","vera_id","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string","example":"review"},"label":{"type":["string","null"]},"vera_id":{"type":"string","description":"**Shown once.** Hand it to the agent now; this service keeps only a hash\nand no later route can reissue it.","example":"vera_<base64url>"}}},"ModeResponse":{"type":"object","description":"The answer to `GET /agent/mode`: which mode this deployment is in, and the\nallowance that comes with it.","required":["mode"],"properties":{"daily_cap":{"type":["integer","null"],"format":"int32","description":"Hosted drafts each account may make per UTC day, or `null` where this\ndeployment meters nothing — every mode but `hosted`. Never a count and\nnever anybody's: this route is answered without a session, so it says\nwhat the deployment is and nothing about who is asking. Today's use is at\n`GET /me/agent/quota`.","example":20},"mode":{"type":"string","description":"`mock`, `live`, `claude-cli` or `hosted`, spelled as\n`VERIGRANT_LLM_MODE` spells it.","example":"hosted"}}},"NewCustomerRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/billing/customer`. Optional in its\nentirety: an institution that has already named a billing contact, or one\nthat does not want to, posts nothing.","properties":{"email":{"type":["string","null"],"description":"Where this company wants its invoices sent. Stored on\n`org_billing.billing_email` and handed to the gateway.\n\nThe **company's** address and not any account's. Nothing here reads a\nuser's login address, which is the rule [`NewCustomer`] states and the\nreason this field exists at all rather than being inferred.","example":"accounts@aperture-robotics.test"}}},"Note":{"type":"object","description":"One internal note on an application.\n\nThe employer's own working record. Never served to the applicant — see this\nmodule's documentation and the migration, where the absence of an owner\npolicy on `application_note` is what enforces it.","required":["id","application_id","body","created_at"],"properties":{"application_id":{"type":"string","format":"uuid"},"author_email":{"type":["string","null"],"description":"The address that account is registered under, on exactly the terms\n`GET /orgs/{id}/members` already discloses it to a fellow member.","example":"recruiter@example.com"},"author_user_id":{"type":["string","null"],"format":"uuid","description":"The member who wrote it, or `null` if they have since erased their\naccount. The note stays: it was the company's work product, and what is\nlost with the author is the attribution, which is the part that was\npersonal."},"body":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"}}},"Notification":{"type":"object","description":"One notice, as its owner sees it.","required":["id","kind","title","data","created_at"],"properties":{"body":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object","description":"Identifiers the client needs to act on this notice. Always an object,\npossibly empty."},"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"The machine-readable event name. Branch on it, and fall back to `title`.","example":"verification.completed"},"read_at":{"type":["string","null"],"format":"date-time","description":"`null` while it is unread. Set once, by the first\n`POST /me/notifications/{id}/read` or `…/read-all` to reach it."},"title":{"type":"string","example":"Your identity check came back"}}},"OrgBillingSummary":{"type":"object","description":"One institution's line in the operator's billing summary.\n\nNamed, unlike almost everything else an administrator sees: an organization\nis not a person, `GET /admin/users` already discloses far more about\naccounts, and an operator working out who to invoice cannot do it from a\ncolumn of UUIDs.","required":["org_id","name","slug","plan","status","currency","unit_price_cents","monthly_fee_cents","annual_fee_cents","included_free_pulls","allowance_used","unbilled","advances","full_reads","previews","lifetime","amount_cents","invoices"],"properties":{"advances":{"type":"integer","format":"int64"},"allowance_used":{"type":"integer","format":"int64","description":"Billable pull-throughs this institution has ever recorded — the number\nthe free allowance is spent against, so an operator can see who is about\nto run out before they are told by a `402`."},"amount_cents":{"type":"integer","format":"int64","description":"What the unbilled rows come to at the current price, plus the plan's\nstanding fee: the number on the next bill, rather than a count multiplied\nby a price. The billable kinds only — see [`BILLABLE_KINDS`]."},"annual_fee_cents":{"type":"integer","format":"int32","description":"What a `contract` institution pays a year, recorded and charged nowhere."},"currency":{"type":"string"},"full_reads":{"type":"integer","format":"int64"},"included_free_pulls":{"type":"integer","format":"int32","description":"How many pull-throughs a free institution gets. Consulted on `free` and\non no other plan."},"invoices":{"type":"integer","format":"int64","description":"How many invoices have been drawn for it."},"last_pull_through_at":{"type":["string","null"],"format":"date-time","description":"The most recent act, of any kind. `null` for an institution that has\nnever taken anything in."},"lifetime":{"type":"integer","format":"int64","description":"Every pull-through this institution has ever run, billed or not."},"monthly_fee_cents":{"type":"integer","format":"int32","description":"The plan's standing fee. Non-zero only on `platform`."},"name":{"type":"string","example":"Aperture Robotics"},"note":{"type":["string","null"],"description":"The operator's own sentence about this arrangement, if there is one. The\ninstitution's owner reads it too — see [`Billing::note`]."},"org_id":{"type":"string","format":"uuid"},"plan":{"type":"string","description":"One of [`PLANS`]. `free` for an organization with no billing row."},"previews":{"type":"integer","format":"int64","description":"Unbilled preview reads, free on every plan. Beside the two above because\nan operator deciding who to move onto a paid plan wants to see who is\ntriaging heavily and ingesting nothing yet. See [`KIND_PREVIEW_READ`]."},"slug":{"type":"string","example":"aperture-robotics"},"status":{"type":"string","description":"One of [`BILLING_STATUSES`]."},"unbilled":{"type":"integer","format":"int64","description":"Unbilled metered acts: what the next invoice will contain, every kind,\nincluding the previews it will not charge for."},"unit_price_cents":{"type":"integer","format":"int32"}}},"OrgMember":{"type":"object","description":"One row of an organization's roster.\n\nIt carries the member's email address and display name, which is a deliberate\ndisclosure and the narrowest one that makes the resource useful: a roster\nnobody can read is a roster nobody can correct, and the address is what the\ninvite route already takes as input. Nothing else about the person travels —\nno profile, no documents, no verification, nothing a grant would be needed\nfor.","required":["id","org_id","user_id","email","role","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"display_name":{"type":["string","null"]},"email":{"type":"string","example":"recruiter@example.com"},"id":{"type":"string","format":"uuid","description":"The membership row's id, which is what the role and removal routes name."},"org_id":{"type":"string","format":"uuid"},"role":{"type":"string","description":"`owner`, `admin` or `recruiter`.","example":"recruiter"},"user_id":{"type":"string","format":"uuid","description":"The account behind the membership. The same id `GET /users/{id}` answers\nto."}}},"PairRequest":{"type":"object","description":"Request body for `POST /me/capabilities`: pairing an agent.","required":["capability_id","agent_box_pub","agent_sign_pub","scopes","preview_only","grant_ttl_days","max_lookups_per_grant","max_grants","not_before","not_after","statement","statement_sig","sealed_payload"],"properties":{"agent_box_pub":{"type":"string","description":"The agent's X25519 public key, base64. What the payload is sealed to."},"agent_sign_pub":{"type":"string","description":"The agent's Ed25519 public key, base64. What every request it makes is\nsigned with."},"capability_id":{"type":"string","format":"uuid","description":"Minted by the client, because it is bound into the sealed payload's\nassociated data before this service sees any of it."},"grant_ttl_days":{"type":"integer","format":"int64"},"label":{"type":["string","null"]},"max_grants":{"type":"integer","format":"int32"},"max_grants_per_day":{"type":["integer","null"],"format":"int32","description":"The rate, where `max_grants` is the lifetime budget: how many grants this\ncapability may issue in any trailing 24 hours.\n\nOptional because a mandate signed before this field existed states no\nsuch limit, and `None` means exactly that rather than \"assume a number\".\n[`check_mandate`] refuses a request that states one where the signed\nstatement does not, or the other way round."},"max_lookups_per_grant":{"type":"integer","format":"int32"},"not_after":{"type":"string","format":"date-time"},"not_before":{"type":"string","format":"date-time"},"payload_version":{"type":"integer","format":"int32"},"preview_only":{"type":"boolean"},"rp_policy":{"type":"object","description":"Which institutions this capability may issue to, as the signed statement\ncarries it. `{\"mode\":\"any\"}` or\n`{\"mode\":\"class\",\"classes\":[…],\"min_registry_age_days\":n}`; see\n[`check_rp_policy_shape`], which refuses anything else at pairing so that\nno policy is ever stored that could not be evaluated."},"scopes":{"type":"array","items":{"type":"string"}},"sealed_payload":{"type":"string","description":"`seal(agent_box_pub, JCS({srk, k_idx}), AAD_cap)`, base64. Opaque here."},"statement":{"type":"string","description":"The exact RFC 8785 bytes the account signed, base64."},"statement_sig":{"type":"string"}}},"PairedAgent":{"allOf":[{"$ref":"#/components/schemas/CapabilitySummary"},{"type":"object","required":["vera_id_secret"],"properties":{"vera_id_secret":{"type":"string","description":"**Shown once.** The auto mode Vera ID itself: hand it to the agent now.","example":"vera_<base64url>"}}}],"description":"The answer to `POST /me/capabilities`."},"PatchAddressRequest":{"type":"object","description":"Request body for `PATCH /me/addresses/{id}`.","properties":{"address_type":{"type":["string","null"]},"city":{"type":["string","null"]},"country":{"type":["string","null"]},"from_date":{"type":["string","null"],"format":"date"},"is_current":{"type":["boolean","null"]},"line1":{"type":["string","null"]},"line2":{"type":["string","null"]},"postal_code":{"type":["string","null"]},"region":{"type":["string","null"]},"to_date":{"type":["string","null"],"format":"date"}}},"PatchCredentialRequest":{"type":"object","description":"Request body for `PATCH /me/credentials/{id}`. An absent field is left\nalone; an explicit `null` clears it.","properties":{"credential_number":{"type":["string","null"]},"expiration_date":{"type":["string","null"],"format":"date"},"is_cdl":{"type":["boolean","null"],"description":"The column is `NOT NULL`, so this one cannot be cleared — only set."},"issue_date":{"type":["string","null"],"format":"date"},"issuing_org":{"type":["string","null"]},"jurisdiction":{"type":["string","null"]},"name":{"type":["string","null"]},"status":{"type":["string","null"]}}},"PatchDocumentRequest":{"type":"object","description":"Request body for `PATCH /me/documents/{id}`. An absent field is left alone;\nan explicit `null` clears it.\n\nThe descriptive columns are missing from here on purpose. Before documents\nwere stored at all, a filename, a media type and a size were the user's\nclaims about a file they held themselves, and editing them was the only way\nto correct one. Then they became observations of a file this service holds,\nand a route that let a client relabel a PDF as a PNG would have been a route\nthat decides what `Content-Type` the next reader's browser sees. Now they are\nciphertext, and editing one is editing the document's record.\n\n`label` is the one that moved rather than being removed, and it is still\nhere for the documents it is still on: a version 1 row carries a readable\nlabel, and until its owner re-seals it, relabelling it is this route. On a\nsealed document a `label` is refused — the label is in the record, and a\ncolumn that silently accepted one would be a second, readable copy of it.","properties":{"doc_type":{"type":["string","null"]},"is_default":{"type":["boolean","null"],"description":"`true` promotes this document and demotes the previous default of its\nkind; `false` demotes it and leaves the kind with no default. There is no\nnull: a flag is set or it is not."},"label":{"type":["string","null"],"description":"Only on a version 1 document. See the type documentation."}}},"PatchDrivingEquipmentRequest":{"type":"object","description":"Request body for `PATCH /me/driving-equipment/{id}`.","properties":{"approx_from":{"type":["string","null"],"format":"date"},"approx_to":{"type":["string","null"],"format":"date"},"class_operated":{"type":["string","null"]},"equipment_type":{"type":["string","null"]},"total_time_months":{"type":["integer","null"],"format":"int32"}}},"PatchLanguageRequest":{"type":"object","description":"Request body for `PATCH /me/languages/{id}`. An absent field is left alone;\nan explicit `null` clears it.","properties":{"name":{"type":["string","null"]},"proficiency":{"type":["string","null"]}}},"PatchLinkRequest":{"type":"object","description":"Request body for `PATCH /me/links/{id}`.","properties":{"label":{"type":["string","null"]},"url":{"type":["string","null"]}}},"PatchPhoneRequest":{"type":"object","description":"Request body for `PATCH /me/phones/{id}`. An absent field is left alone; an\nexplicit `null` clears it.","properties":{"country_code":{"type":["string","null"]},"is_primary":{"type":["boolean","null"]},"phone":{"type":["string","null"]},"phone_type":{"type":["string","null"]}}},"PatchPostingRequest":{"type":"object","description":"Request body for `PATCH /orgs/{id}/postings/{posting_id}`.\n\nEvery field is optional and the nullable ones tell an absent key from an\nexplicit `null` — see [`crate::validate::patched`]. The three fields that\ncannot be cleared say so: a posting with no title, no description or no\nstatus is not a posting.","properties":{"description":{"type":["string","null"]},"employment_type":{"type":["string","null"]},"location":{"type":["string","null"]},"remote":{"type":["boolean","null"]},"required_scopes":{"type":["array","null"],"items":{"type":"string"}},"screening_questions":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ScreeningQuestion"}},"status":{"type":["string","null"],"description":"`draft`, `open` or `closed`. This is the field that publishes and\nwithdraws a posting.","example":"open"},"title":{"type":["string","null"]}}},"PatchPreferenceRequest":{"type":"object","description":"Request body for `PATCH /me/availability`. An absent field is left alone; an\nexplicit `null` clears it.","properties":{"currently_employed":{"type":["boolean","null"]},"desired_locations":{"type":["array","null"],"items":{"type":"string"}},"desired_pay":{"type":["string","null"],"example":"72150.00"},"desired_pay_currency":{"type":["string","null"]},"desired_pay_period":{"type":["string","null"]},"earliest_start_date":{"type":["string","null"],"format":"date"},"employment_type_sought":{"type":["string","null"]},"max_travel_pct":{"type":["integer","null"],"format":"int32"},"remote_preference":{"type":["string","null"]},"willing_relocate":{"type":["boolean","null"]},"willing_travel":{"type":["boolean","null"]}}},"PatchReferenceRequest":{"type":"object","description":"Request body for `PATCH /me/references/{id}`.","properties":{"company":{"type":["string","null"]},"email":{"type":["string","null"]},"name":{"type":["string","null"]},"phone":{"type":["string","null"]},"relationship":{"type":["string","null"]},"title":{"type":["string","null"]},"years_known":{"type":["integer","null"],"format":"int32"}}},"PaymentEvent":{"type":"object","description":"One payment result, in the shape [`crate::payments`] says the gateway sends\nit.\n\nDeliberately the *same* fields a charge reply carries, because the assumed\ncontract says a callback and a read-back describe a charge the same way — see\nthat module's header. A gateway that disagrees is one struct to change, in\nthe one file that holds every other assumption about its API.","required":["charge_id","status"],"properties":{"charge_id":{"type":"string","description":"The gateway's own id for the charge, matched against `invoice.external_id`.\nThis is the whole basis on which a delivery finds a bill: a payment\ncallback names a charge, never an organization and never a person.","example":"ch_3PfQ2mG"},"failure_code":{"type":["string","null"],"description":"The gateway's own code for a failure, if it gave one. It goes into\n`invoice.last_error` so an operator can look `card_declined` up in the\ngateway's documentation rather than in ours.","example":"card_declined"},"status":{"type":"string","description":"`paid` or `failed`. `pending` is a status a charge can *have* rather than\na result worth delivering, so a callback carrying it is a permanent job\nfailure rather than a silent no-op — the same judgement\n`Status::parse_terminal` makes about a verification webhook that would\nmove a check back to `pending`.","example":"paid"}}},"PendingReset":{"type":"object","description":"A destruction that has been scheduled and has not run.","required":["requested_at","due_at","deadline"],"properties":{"deadline":{"type":"string","description":"The deadline as the messages about it are worded, so a screen says the\nsame thing the mail said rather than formatting it a second way.","example":"17 September 2026 at 09:30 UTC"},"due_at":{"type":"string","format":"date-time","description":"When this account will be destroyed unless it is cancelled first."},"requested_at":{"type":"string","format":"date-time","description":"When the mailed confirmation was spent."}}},"PhoneEntry":{"type":"object","required":["id","phone","phone_type","is_primary","created_at"],"properties":{"country_code":{"type":["string","null"],"description":"Dialling code, kept apart from the number because forms ask for them in\nseparate boxes far more often than they ask for one combined string."},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"is_primary":{"type":"boolean"},"phone":{"type":"string"},"phone_type":{"type":"string","description":"One of `mobile`, `home`, `work`."}}},"PlatformPublicKey":{"type":"object","description":"The response body of [`pubkey`]. Every field is a public fact; there is\nnothing here to redact and nothing to authenticate.","required":["algorithm","public_key","encoding","verifies"],"properties":{"algorithm":{"type":"string","description":"Always `ed25519`. Present so a verifier picks its library from the\ndocument rather than from documentation that could drift from it.","example":"ed25519"},"encoding":{"type":"string","description":"How to decode `public_key`.","example":"base64url-nopad"},"public_key":{"type":"string","description":"The 32-byte verifying key, [`KEY_ENCODING`].","example":"3q2-7_8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},"verifies":{"type":"array","items":{"type":"string"},"description":"The token formats this key verifies, by their version prefix: `v1` for a\ngrant token and `cred.v1` for a verification credential.\n\nWorth publishing beside the key because the prefix is *inside* the signed\nbytes — a verifier reconstructs the signed message as\n`<prefix>.<payload>`, not as the payload alone, and that is exactly the\nproperty that keeps the two formats from being confused for each other.","example":["v1","cred.v1"]}}},"Posting":{"type":"object","description":"A posting as one of its organization's members sees it: every column,\nincluding the ones that are nobody else's business until it is published.","required":["id","org_id","title","description","remote","required_scopes","screening_questions","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"description":{"type":"string"},"employment_type":{"type":["string","null"],"description":"One of [`crate::validate::EMPLOYMENT_TYPES`], or `null` for a vacancy\nthat has not decided.","example":"full_time"},"id":{"type":"string","format":"uuid"},"location":{"type":["string","null"],"example":"Leeds, UK"},"org_id":{"type":"string","format":"uuid"},"remote":{"type":"boolean"},"required_scopes":{"type":"array","items":{"type":"string"},"description":"What an applicant is being asked to delegate. A request, not a\npermission — see this module's documentation.","example":["profile:read","history:read"]},"screening_questions":{"type":"array","items":{"$ref":"#/components/schemas/ScreeningQuestion"},"description":"The screening tail, in `POST /me/autofill`'s `questions` shape."},"status":{"type":"string","description":"`draft`, `open` or `closed`. Only `open` is readable by anybody outside\nthe organization.","example":"draft"},"title":{"type":"string","example":"Class 1 Driver — Leeds"},"updated_at":{"type":"string","format":"date-time"}}},"PostingMetrics":{"type":"object","required":["total","draft","open","closed"],"properties":{"closed":{"type":"integer","format":"int64"},"draft":{"type":"integer","format":"int64"},"open":{"type":"integer","format":"int64","description":"Published: the ones `GET /postings` serves to anybody at all."},"total":{"type":"integer","format":"int64"}}},"Provenance":{"type":"object","description":"The source of one item, as its owner and any later delegate read it.\n\nThe fields are what a verifier needs to rebuild [`provenance_statement`] for\nitself and check the signature against the sender's published key. That is\nwhy the whole statement's inputs travel rather than a verdict: a verdict\nwould be this platform's opinion, and the point of the construction is that\nnobody has to take it.","required":["origin"],"properties":{"connector":{"type":["string","null"],"description":"Which connector fetched it, for [`ORIGIN_CONNECTOR`] and `null`\notherwise.\n\n**The aggregator, never the source.** It is beside `sender_org_id` in\nthis struct and it is emphatically not a substitute for one: a sender\nsigned a statement and a connector signed nothing, so a reader that\ntreated the two fields alike would be reading a fetch as evidence. The\npair is mutually exclusive by CHECK constraint, which is what stops one\never being presented as the other."},"origin":{"type":"string","description":"[`ORIGIN_INSTITUTION`], [`ORIGIN_SELF`] or [`ORIGIN_CONNECTOR`]. An item\nsays which it is and can never say anything else."},"sender_domain":{"type":["string","null"],"description":"The host that speaks for the sender, which the signed statement binds."},"sender_kind":{"type":["string","null"],"description":"What kind of institution it is, as the registration names it."},"sender_org_id":{"type":["string","null"],"format":"uuid","description":"The sender, where there is one. `null` for anything but a push."},"sender_org_slug":{"type":["string","null"],"description":"Its handle in the public registry, so a verifier knows where to fetch the\nkey: `GET /rp/{slug}/keys`, which needs no credential."},"sent_at":{"type":["string","null"],"format":"date-time","description":"When the sender says it signed, as the statement binds it."},"signature":{"type":["string","null"],"description":"The 64 bytes, base64."},"signing_key_id":{"type":["string","null"],"description":"Which registered `ed25519` key signed."}}},"ProvisionalCreated":{"type":"object","description":"What `POST /agent/provisional` answers with, once.\n\nThere is no claim secret in it and there could not be: this service has never\nheld one. `claim_url_prefix` is the deployment's own public origin with the\nquery parameter already on it, so an agent does not carry a second copy of the\norigin in its configuration and cannot send somebody to the wrong deployment.","required":["user_id","claim_url_prefix","expires_at"],"properties":{"claim_url_prefix":{"type":"string","description":"`{origin}/app/?claim=`. Append the claim secret to make the link.","example":"https://verigrant.com/app/?claim="},"expires_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid","description":"The id the agent minted, echoed so that a client which lost it mid-call\ndoes not have to guess whether the account exists."}}},"PublicKeyView":{"type":"object","description":"One registered public key, as a client that is about to seal sees it.\n\nMirrors `vg_crypto::attest::RegisteredKey` field for field, because that is\nthe struct a client feeds this into. A field here that the crate's checker\ndoes not take is a field the client would have to decide what to do with.","required":["key_id","alg","public_key","domain","not_before","not_after","not_before_unix","not_after_unix","log_seq","evidence"],"properties":{"alg":{"type":"string","example":"x25519"},"domain":{"type":"string","description":"Where the attestation must be fetched from. Compared by the client\nagainst the host it actually connected to.","example":"acme-university.edu"},"evidence":{"$ref":"#/components/schemas/Evidence","description":"The evidence a client checks before it will seal to the bytes above."},"key_id":{"type":"string","example":"2026-q3"},"log_seq":{"type":"integer","format":"int64","description":"This key's leaf index in the transparency log. Zero-based, because a\nMerkle leaf index is an offset into a tree of `tree_size` leaves and\n`vg_crypto::attest` refuses `leaf_index >= tree_size`.\n\nThe leaf's **position** in the ordered log, which is not the same number\nas the `seq` the row is stored under: a Postgres sequence burns a value\non a rolled-back registration, and an offset into a tree has to count\nleaves rather than sequence values. See [`Log`]."},"not_after":{"type":"string","format":"date-time"},"not_after_unix":{"type":"integer","format":"int64"},"not_before":{"type":"string","format":"date-time"},"not_before_unix":{"type":"integer","format":"int64","description":"The same two instants as unix seconds.\n\nRedundant on purpose. These are the representation the log leaf and the\ninstitution's signed statement commit to, and a client has to rebuild\nboth to check either. Serving the integers means a browser client never\nhas to carry a date parser into the one code path where a differing\ninterpretation of a timestamp would be a proof that mysteriously fails to\nverify. Nothing is trusted here that is not also inside the signature."},"public_key":{"type":"string","description":"The 32 bytes, base64."},"revoked_at":{"type":["string","null"],"format":"date-time","description":"When this key was withdrawn, if it was.\n\n`null` everywhere a client that is about to seal can see it: [`registry`]\nserves live keys only, so this field is `null` in every response that\nfeeds `vg_crypto::attest`. It is here for the *other* reader of this\nshape, [`list_keys`], which is deliberately wider — an institution\nauditing its own rotation needs to see the generation it retired, and\n\"see\" is not something a listing can do while the only difference between\na live key and a withdrawn one is a column it does not serve. Without it\nthe institution's own screen would offer to revoke a key that was revoked\nlast quarter and call the answer a success.\n\nAdditive on the wire: `PublicKeyView` otherwise mirrors\n`vg_crypto::attest::RegisteredKey`, and this is a field that crate's\nchecker does not take and never sees a value for."}}},"PublicPosting":{"type":"object","description":"One row of the public feed: the advertisement, the employer, and — on the\nsingle-posting route — the structured data for it.","required":["id","organization","title","description","remote","required_scopes","screening_questions","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"description":{"type":"string"},"employment_type":{"type":["string","null"]},"id":{"type":"string","format":"uuid"},"json_ld":{"type":["object","null"],"description":"schema.org `JobPosting`, as Google for Jobs reads it. Present on\n`GET /postings/{id}` and absent from the feed — a crawler reads the\nstructured data from the page for one job, and repeating it fifty times\nin a list would be fifty times the bytes for nothing.\n\nEvery string in it is HTML-escaped, because the object exists to be\nembedded in a `<script type=\"application/ld+json\">` element. See this\nmodule's documentation."},"location":{"type":["string","null"]},"organization":{"$ref":"#/components/schemas/Employer"},"remote":{"type":"boolean"},"required_scopes":{"type":"array","items":{"type":"string"},"description":"What applying will ask the candidate to delegate, so a client can show it\n*before* anybody mints anything.","example":["profile:read","history:read"]},"screening_questions":{"type":"array","items":{"$ref":"#/components/schemas/ScreeningQuestion"},"description":"Hand this array to `POST /me/autofill` as its `questions` field."},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"PublicUser":{"type":"object","description":"The only shape of a user we ever hand back. `password_hash` and `status`\nare not part of it, so they cannot leak through this type.","required":["id","email","created_at","notification_email"],"properties":{"created_at":{"type":"string","format":"date-time"},"display_name":{"type":["string","null"]},"email":{"type":"string","example":"ada@example.com"},"email_verified_at":{"type":["string","null"],"format":"date-time","description":"When this address was proved, or `null` if it has not been.\n\nA moment rather than a flag, because \"when\" is a fact somebody may want\nto produce later and \"whether\" is derivable from it. It is on the account\nshape rather than behind a settings route of its own so that the one\ncaller who needs it — the account's own client, which has already fetched\nthis — learns it without a second request. See [`crate::mail`] for what\nan unproved address costs, which is deliberately almost nothing: the two\ninstitution routes in [`crate::orgs`] and the outbound mail channel, and\nnot one route over the caller's own record."},"id":{"type":"string","format":"uuid"},"notification_email":{"type":"boolean","description":"Whether this account wants its notices mailed as well as held.\n\nOnly ever consulted for a verified address, so `true` on an unverified\naccount means \"would, if asked\" rather than \"is being\". Set through\n`PATCH /me/settings`; see [`crate::notifications::patch_settings`]."}}},"PullThrough":{"type":"object","description":"One billable act, as the institution being charged reads it back.\n\nIt names the application and the delegation and nothing about the person\nbehind either — which is the same rule every other employer-facing list in\nthis service follows, and here it is also all an invoice needs. A usage\nreport that identified candidates would be a disclosure with a spreadsheet\nattached.","required":["id","org_id","kind","occurred_at"],"properties":{"application_id":{"type":["string","null"],"format":"uuid","description":"The application taken in, or `null` if it has since been erased — the\ncolumn is `ON DELETE SET NULL`, because a candidate closing their account\nmust not be obstructed by an institution's bill."},"billed_at":{"type":["string","null"],"format":"date-time","description":"When this act was drawn onto an invoice. `null` means it is in the\ncurrent period — the one [`overview`] reports and the next invoice will\ncontain."},"grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation the act was performed through, on the same terms."},"id":{"type":"string","format":"uuid"},"invoice_id":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string","description":"`advance` or `full_read`.","example":"advance"},"occurred_at":{"type":"string","format":"date-time"},"org_id":{"type":"string","format":"uuid"}}},"PushReceipt":{"type":"object","description":"What a sender gets back: the item's id and when it landed.\n\nDeliberately nothing else. A receipt that echoed the envelope would be this\nservice telling an institution what it had just been told, and a receipt that\ncarried anything about the recipient would be the push route answering a\nquestion nobody asked it.","required":["item_id","received_at"],"properties":{"item_id":{"type":"string","format":"uuid"},"received_at":{"type":"string","format":"date-time"}}},"PushRequest":{"type":"object","description":"Request body for `POST /inbound/{user_id}`.\n\nOne opaque byte string and the labels that name it. Every field here is\neither ciphertext, a commitment to ciphertext, a label an operator can\nalready see, or the signature that binds the three together.","required":["doc_type","schema","schema_version","byte_class","payload","payload_hash","recipient_box_pub","sent_at","signing_key_id","provenance_signature"],"properties":{"byte_class":{"type":"integer","format":"int32","description":"The padding class the plaintext was padded to before it was sealed, from\nsection 1.6 of the architecture — the same classes a record uses.","example":4096},"doc_type":{"type":"string","description":"The label, never the content: `pay_statement`, `w2`, `explanation_of_benefits`.","example":"pay_statement"},"payload":{"type":"string","description":"The payload, sealed to the recipient's `box_pub`. Exactly\n`byte_class + 48` bytes: a 32-byte ephemeral public key, the padded\nplaintext, and a 16-byte tag.","example":"base64 of byte_class + 48 bytes"},"payload_hash":{"type":"string","description":"SHA-256 of those exact bytes, base64. Over the **ciphertext**; see\n[`provenance_statement`] for why it is not over the plaintext.","example":"base64 of 32 bytes"},"provenance_signature":{"type":"string","description":"The 64 bytes, base64, over [`provenance_statement`].","example":"base64 of 64 bytes"},"recipient_box_pub":{"type":"string","description":"The recipient's key as the sender resolved it. Checked against the\nkeyring this service holds, so a push sealed under a superseded key is\nrefused rather than stored unopenable.","example":"base64 of 32 bytes"},"recipient_key_epoch":{"type":["integer","null"],"format":"int32","description":"Which generation of that key the sender believes it holds, from\n[`InboundAddress::key_epoch`].\n\nOptional, because it adds nothing a sender that re-read the address a\nmoment ago does not already have, and because an integer is not what\nmakes this safe: `recipient_box_pub` above is the check that cannot be\ngot around, and it is made against the 32 bytes this service holds rather\nthan against anything the body asserts.\n\nWhat sending it buys is a **better refusal**. A sender that offers the\nepoch it cached is told `stale_recipient_key` with both numbers in the\nmessage, so its operator reads \"you hold generation 1 and the account is\non 2\" instead of \"these 32 bytes are not those 32 bytes\". Both are the\nsame `409` and both are fixed the same way, which is to resolve\n`GET /inbound/{user_id}` again and re-seal.","example":1},"schema":{"type":"string","description":"One of [`INBOUND_SCHEMAS`].","example":"payroll"},"schema_version":{"type":"string","description":"Which revision of that schema. Mandatory whatever the schema is.","example":"2026-01"},"sent_at":{"type":"string","format":"date-time","description":"When the sender signed. Refused if it is more than\n[`MAX_CLOCK_SKEW_SECONDS`] ahead of this service's clock."},"signing_key_id":{"type":"string","description":"Which of the sender's registered `ed25519` keys made the signature.","example":"attest-2026"}}},"PutCdlRequest":{"type":"object","description":"Request body for `PUT /me/credentials/{id}/cdl`. A `PUT` replaces: an\nomitted field clears the stored value.","required":["cdl_class"],"properties":{"cdl_class":{"type":"string","description":"`A`, `B` or `C`. The one field a CDL cannot be without.","example":"A"},"cmv_years":{"type":["integer","null"],"format":"int32"},"endorsements":{"type":["array","null"],"items":{"type":"string"},"example":["H","N"]},"med_card_expiration":{"type":["string","null"],"format":"date"},"restrictions":{"type":["array","null"],"items":{"type":"string"}}}},"PutEeoRequest":{"type":"object","description":"Request body for `PUT /me/eeo`. Every field is optional, in both senses: the\nkey may be omitted, and the answer may be `prefer_not_to_say`.","properties":{"disability_form_version":{"type":["string","null"],"example":"CC-305 (rev. 04/2024)"},"disability_status":{"type":["string","null"]},"gender":{"type":["string","null"]},"race_ethnicity":{"type":["array","null"],"items":{"type":"string"}},"veteran_status":{"type":["string","null"]},"veteran_subcategories":{"type":["array","null"],"items":{"type":"string"}}}},"PutEligibilityRequest":{"type":"object","description":"Request body for `PUT /me/work-eligibility`. A `PUT` replaces: an omitted\nfield clears the stored value.","properties":{"auth_type":{"type":["string","null"]},"authorized_us":{"type":["boolean","null"]},"citizenship_status":{"type":["string","null"]},"clearance_level":{"type":["string","null"]},"clearance_status":{"type":["string","null"]},"is_over_18":{"type":["boolean","null"]},"requires_sponsorship":{"type":["boolean","null"]},"veterans_preference":{"type":["string","null"]}}},"PutKeyringRequest":{"type":"object","description":"Request body for `PUT /me/keyring`.\n\nDeliberately no `Debug`, like every other type in this service that holds a\ncredential: `auth_key` is one, and one stray `{:?}` would put it in the log.\n\nEvery byte string is standard base64. The server decodes each one, checks\nits length against the construction that produced it, and stores the bytes\nwithout looking inside them — which it could not do in any case.\n\nThe type is visible outside this module for one reader — [`crate::claim`],\nwhere the same body arrives nested inside a claim request, because claiming a\nprovisional account *is* enrolling a keyring — and its **fields are not**.\nThat caller goes through [`check_keyring`] and [`write_keyring`] rather than\nreading a field of its own, so there is exactly one implementation of what\nthis body is allowed to contain and a rule added to it cannot be missing from\none of the two paths.","required":["kdf_salt","kdf_m_kib","kdf_t","kdf_p","dmk_slot_id","wrapped_dmk","dmk_nonce","recovery_slot_id","recovery_wrapped_dmk","recovery_nonce","auth_key","box_pub","sign_pub"],"properties":{"auth_key":{"type":"string","description":"The 32 bytes a v2 client sends where a v1 client sends a password. It is\nhashed here and dropped; the hash is what is stored, and no route\nreturns it.","example":"base64 of 32 bytes"},"box_pub":{"type":"string","description":"X25519 public key, 32 bytes: how somebody seals to this user offline.","example":"base64 of 32 bytes"},"current_auth_key":{"type":["string","null"],"description":"The `auth_key` the account authenticates with **today**, base64 of 32\nbytes. Required when a keyring already exists, refused as unnecessary by\nnothing when it does not.\n\nThis is the field that turns a password change from an account takeover\ninto a password change; see [`put_keyring`].","example":"base64 of 32 bytes"},"current_password":{"type":["string","null"],"description":"The account's password, as `POST /auth/login` would take it. Required on\na **first** enrolment, where there is no keyring to prove and the\nsession alone is therefore not enough.\n\nIt is checked against `users.password_hash` through the same\n[`crate::auth::verify_secret`] a login goes through, and it is the one\nvalue on this route that is a plaintext password. It is read here,\nverified, and dropped; nothing downstream sees it and no response shape\nhas a field it could travel in.\n\nFor an account created the way `web::vault::sign_up` creates one this is\nthe bootstrap credential the client minted a moment earlier, which that\nclient still holds and nobody else ever will. For a version 1 account\nadopting a keyring it is the password the person has been signing in\nwith. See [`put_keyring`] for why a bare session is not enough.","example":"the account's password"},"dmk_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"dmk_slot_id":{"type":"string","format":"uuid","description":"The password slot's id, minted by the client. Part of the wrap's\nassociated data, which is why it is stored rather than implied."},"kdf_alg":{"type":"string","description":"Currently always `argon2id`. Stored per user so the algorithm can move\nwithout a migration; see section 1.7.","example":"argon2id"},"kdf_m_kib":{"type":"integer","format":"int32","description":"Argon2id memory cost in KiB. The client's default is 65536.","example":65536},"kdf_p":{"type":"integer","format":"int32","description":"Argon2id parallelism. Always 1 in a browser, which has no threads\nwithout cross-origin isolation; see section 1.3.","example":1},"kdf_salt":{"type":"string","description":"16 random bytes, minted by the client at signup. Not a secret: the\nclient needs it before it can log in, so it is served to whoever can\nname the account.","example":"base64 of 16 bytes"},"kdf_t":{"type":"integer","format":"int32","description":"Argon2id time cost. The client's default is 3.","example":3},"recovery_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"recovery_slot_id":{"type":"string","format":"uuid","description":"The recovery slot's id. Must differ from `dmk_slot_id`."},"recovery_wrapped_dmk":{"type":"string","description":"The recovery wrap slot: the same DMK under a key derived from the\nrecovery phrase the user was shown once. 48 bytes.","example":"base64 of 48 bytes"},"sign_pub":{"type":"string","description":"Ed25519 public key, 32 bytes: what a grant statement is verified\nagainst.","example":"base64 of 32 bytes"},"wrapped_dmk":{"type":"string","description":"The password wrap slot: the DMK sealed under `KEK_pw`, 48 bytes.","example":"base64 of 48 bytes"}}},"PutPreferenceRequest":{"type":"object","description":"Request body for `PUT /me/availability`. A `PUT` replaces: an omitted field\nclears the stored value.","properties":{"currently_employed":{"type":["boolean","null"]},"desired_locations":{"type":["array","null"],"items":{"type":"string"}},"desired_pay":{"type":["string","null"],"example":"72150.00"},"desired_pay_currency":{"type":["string","null"]},"desired_pay_period":{"type":["string","null"]},"earliest_start_date":{"type":["string","null"],"format":"date"},"employment_type_sought":{"type":["string","null"]},"max_travel_pct":{"type":["integer","null"],"format":"int32"},"remote_preference":{"type":["string","null"]},"willing_relocate":{"type":["boolean","null"]},"willing_travel":{"type":["boolean","null"]}}},"PutRecordRequest":{"type":"object","description":"Request body for `PUT /me/records/{id}`.\n\nFour opaque byte strings and the labels that name them. Every field here is\neither ciphertext, a wrapped key, or a value the operator can already see.","required":["scope","record_type","write_epoch","byte_class","ciphertext","ct_nonce","wrapped_dek","dek_nonce"],"properties":{"byte_class":{"type":"integer","format":"int32","description":"The padding class the plaintext was padded to.","example":256},"ciphertext":{"type":"string","description":"The record, sealed under a DEK minted for this version. Exactly\n`byte_class + 16` bytes.","example":"base64 of byte_class + 16 bytes"},"ct_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"dek_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"record_type":{"type":"string","description":"The label — `work_history`, `education`, `skill` — never the content.","example":"work_history"},"scope":{"type":"string","description":"One of the grant catalogue's scopes, which is also the record\npartition: it decides which scope root key wrapped this row's DEK.","example":"history:read"},"wrapped_dek":{"type":"string","description":"That DEK, sealed under `SRK_{scope,epoch}`. 48 bytes.","example":"base64 of 48 bytes"},"write_epoch":{"type":"integer","format":"int32","description":"The scope's current epoch, which this write pins into the row forever.\nRefused if it is not current; see the module documentation.","example":1}}},"PutSensitiveIdRequest":{"type":"object","description":"Request body for `PUT /me/sensitive-id`.\n\nThere is no `ssn` field and no `ssn_full` field. A body carrying one is not\nrejected with a special message — it is simply not read, because there is\nnowhere for it to go.","properties":{"date_of_birth":{"type":["string","null"],"format":"date","example":"1988-04-12"},"ssn_last4":{"type":["string","null"],"description":"Exactly four digits.","example":"0042"}}},"PutShiftRequest":{"type":"object","description":"Request body for `PUT /me/availability-shifts`.","required":["day_of_week"],"properties":{"available":{"type":["boolean","null"],"description":"Defaults to `true`: a day written into the grid is normally a day the\nuser can work."},"day_of_week":{"type":"integer","format":"int32","description":"0 (Sunday) to 6 (Saturday)."},"end_time":{"type":["string","null"],"example":"17:30:00"},"overnight":{"type":["boolean","null"]},"start_time":{"type":["string","null"],"example":"09:00:00"}}},"QueuedJob":{"type":"object","description":"One queue row, as an administrator sees it.\n\nEvery column of `job_queue` except one, and the exception is the whole\nargument: **`payload` does not travel.** It is the field that names people —\n`notify` carries a user id, `agent.run` carries whose run it is — and this\nmodule's own rule for it is that it holds identifiers rather than data. An\nidentifier is still a person. What travels instead is\n[`QueuedJob::payload_keys`]: the shape of the arguments without their values,\nwhich is what tells an operator that a stuck `mail.send` job is missing its\n`notification_id` rather than which mailbox it was for.\n\nThe rest is the queue's own bookkeeping and belongs to nobody: what kind of\nwork it is, where it got to, how many attempts it has spent, when it may run\nnext, and what went wrong last time.","required":["id","kind","status","attempts","max_attempts","run_at","payload_keys","created_at","updated_at"],"properties":{"attempts":{"type":"integer","format":"int32","description":"How many times a worker has *started* this job. Counted at claim time —\nsee this module's header — so a job that took its worker down with it has\nstill spent every attempt it looks like it spent."},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"The registry key this row dispatches on — `notify`, `agent.run`,\n`queue.retention`. Free text in the schema, so a kind no handler knows is\na real thing to find here: it is the typo that leaves a job queued\nforever, and this list is where it becomes visible.","example":"notify"},"last_error":{"type":["string","null"],"description":"Why the last attempt failed, in the handler's own words, truncated to\n[`MAX_ERROR_CHARS`] with a trailing ellipsis. Null on a row that has\nnever failed, and on a `done` row, which the worker clears."},"max_attempts":{"type":"integer","format":"int32"},"payload_keys":{"type":"array","items":{"type":"string"},"description":"The payload's keys, sorted, and never its values. Empty for the many\njobs whose payload is `{}`.","example":["notification_id"]},"run_at":{"type":"string","format":"date-time","description":"`job_queue.run_after`: the moment this row becomes eligible. Named for\nwhat an operator reads it as — *when does this run* — and in the past for\neverything that is not deliberately delayed or backing off."},"status":{"type":"string","description":"One of [`JOB_STATUSES`].","example":"failed"},"updated_at":{"type":"string","format":"date-time","description":"When the row last moved. For a terminal row that is when it reached that\nstatus, which is what makes \"the oldest failure is four days old\" a\nquestion [`crate::admin::metrics`] can answer."}}},"QueuedVerification":{"type":"object","description":"One check, as the operator deciding it sees it.\n\nThe verification row and nothing beside it. `user_id` is here because an\noperator has to be able to tell two checks apart and to cross-reference\n`GET /admin/users/{id}`; it is an id and not a name, an address or a field of\nanybody's profile, and that is the whole of what this route discloses about\nthe person.","required":["id","user_id","kind","status","detail","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"detail":{"type":"object","description":"What the checker recorded. Never the evidence — see the module docs."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the underlying check stops meaning anything, if it says."},"id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/Kind"},"provider":{"type":["string","null"],"description":"Who is doing the checking — `manual` for everything this build opens.","example":"manual"},"reference":{"type":["string","null"],"description":"The checker's own identifier, when they gave one. Absent on a manual\ncheck: there is no vendor holding a copy of it."},"review_reason":{"type":["string","null"],"description":"Why it was rejected, when it was."},"reviewed_at":{"type":["string","null"],"format":"date-time"},"reviewed_by":{"type":["string","null"],"format":"uuid","description":"The operator who last decided this check, and when. Absent on everything\nstill waiting — which is what makes `reviewed_at` the queue's real\n\"untouched\" flag, distinct from `status`, since a check can be `pending`\nagain only by being a new row."},"status":{"$ref":"#/components/schemas/Status"},"user_id":{"type":"string","format":"uuid","description":"Whose check this is. The same id `GET /admin/users/{id}` takes."},"verified_at":{"type":["string","null"],"format":"date-time","description":"When this check became `verified`, if it currently is."}}},"Quota":{"type":"object","description":"One person's day, as `GET /me/agent/quota` reports it.","required":["day","drafts","tokens_in","tokens_out","resets_at"],"properties":{"daily_cap":{"type":["integer","null"],"format":"int32","description":"The allowance, or `null` where this deployment meters nothing. Every mode\nbut `hosted` reports `null` here, which is the honest answer: there is no\nlimit because there is no platform bill.","example":20},"day":{"type":"string","format":"date","description":"The UTC day these figures are for.","example":"2026-09-09"},"drafts":{"type":"integer","format":"int32","description":"Hosted drafts made today — the number the cap is compared with. Drafts\npaid for with the caller's own stored key are not among them."},"remaining":{"type":["integer","null"],"format":"int32","description":"What is left of the allowance, or `null` where there is none."},"resets_at":{"type":"string","format":"date-time","description":"When the allowance is restored: midnight UTC at the end of `day`."},"tokens_in":{"type":"integer","format":"int64","description":"What the provider reported today's drafts consumed. Zero where it\nreported nothing — the mock and the CLI name no usage."},"tokens_out":{"type":"integer","format":"int64"}}},"Received":{"type":"object","description":"What the endpoint answers with.","required":["status"],"properties":{"job_id":{"type":["string","null"],"format":"uuid","description":"The `job_queue` row this delivery became, for the events that become one.\nIt names a queue row and nothing about anybody."},"status":{"type":"string","description":"`queued` for an event this service acts on, `ignored` for one it does\nnot. Both are a `200`: Stripe reads the status code and nothing else, and\nthis field is for a person reading a delivery in Stripe's dashboard.","example":"queued"}}},"RecordApplicationRequest":{"type":"object","description":"Request body for `POST /me/applications`.","required":["company"],"properties":{"ats_vendor":{"type":["string","null"],"example":"greenhouse"},"company":{"type":"string","example":"Aperture Robotics"},"job_title":{"type":["string","null"],"example":"Senior Backend Engineer"},"job_url":{"type":["string","null"]},"status":{"type":["string","null"],"description":"Defaults to `draft`."},"submitted_at":{"type":["string","null"],"format":"date-time","description":"Defaults to now for any status other than `draft`, and to nothing for a\ndraft."}}},"RecordConsentRequest":{"type":"object","description":"Request body for `POST /me/consents`.\n\nThere is no `ip` or `user_agent` field: those are observed, not claimed. And\nthere is no field for the *result* of whatever was consented to, because this\nservice records the consent and never the report.","required":["consent_type","granted"],"properties":{"application_id":{"type":["string","null"],"format":"uuid","description":"The application this consent is being given for, where it is being given\nas part of one.\n\nOptional, and absent is the ordinary case rather than a client that\nforgot: the sign-up agreement and a standing authorisation are consents\nto nothing in particular. Where it *is* sent it must name an application\nof the caller's own — see [`record_consent`]."},"consent_type":{"type":"string","example":"background_check"},"document_version":{"type":["string","null"],"example":"fcra-disclosure-v3"},"granted":{"type":"boolean"},"granted_at":{"type":["string","null"],"format":"date-time","description":"Defaults to now. Present so a consent gathered a moment earlier in a form\nflow is recorded at the time it was given."},"signature_method":{"type":["string","null"]},"signer_name":{"type":["string","null"]}}},"RecoverRequest":{"type":"object","description":"Request body for `POST /auth/recover`.\n\nNo `Debug`: the signature is not a long-lived credential, but it is the one\nthing standing between this body and a session.","required":["email","challenge","signature"],"properties":{"challenge":{"type":"string","description":"The challenge from `POST /auth/recovery-challenge`, base64, returned\nverbatim.","example":"base64 of 32 bytes"},"email":{"type":"string","example":"ada@example.com"},"signature":{"type":"string","description":"Ed25519 over `\"vgrp.recover.v1.\" || challenge`, under the account's\nkeyring `sign_pub` — which is derived from the DMK, which comes out of\nthe recovery wrap slot. base64 of 64 bytes.","example":"base64 of 64 bytes"}}},"RecoveryChallengeResponse":{"type":"object","description":"Response body for `POST /auth/recovery-challenge`.\n\nThe challenge, and the recovery wrap slot the client needs in order to be\nable to sign it. Serving that slot to whoever can name an address is safe for\nthe reason the whole design rests on: it is the DMK sealed under a key\nderived from 32 bytes of uniform entropy that exist only on a piece of paper\nthe user was shown once. There is no dictionary to run against it.\n\nSection 7.1's derivation needs no salt and no cost parameters — the input is\nalready uniform, so there is no Argon2id in front of it — so the parameters\nthis answer has to carry are `user_id` and `recovery_slot_id`, which are the\nHKDF `info` and the wrap's associated data.","required":["user_id","challenge","expires_at","recovery_slot_id","recovery_wrapped_dmk","recovery_nonce"],"properties":{"challenge":{"type":"string","description":"32 random bytes to sign, base64.","example":"base64 of 32 bytes"},"expires_at":{"type":"string","format":"date-time","description":"Five minutes out."},"recovery_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"recovery_slot_id":{"type":"string","format":"uuid","description":"The recovery slot's id, the other half of the associated data."},"recovery_wrapped_dmk":{"type":"string","description":"The DMK sealed under `KEK_rec`, 48 bytes.","example":"base64 of 48 bytes"},"user_id":{"type":"string","format":"uuid","description":"The account's id: the HKDF `info` for `KEK_rec`, and half of the wrap's\nassociated data."}}},"ReferenceEntry":{"type":"object","description":"Somebody who will vouch for the user. Mostly *their* personal data, which is\nwhy it never leaves through a grant in this phase.","required":["id","name","created_at"],"properties":{"company":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"email":{"type":["string","null"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"phone":{"type":["string","null"]},"relationship":{"type":["string","null"],"description":"How the referee knows the user: \"former manager\", \"colleague\"."},"title":{"type":["string","null"]},"years_known":{"type":["integer","null"],"format":"int32"}}},"RegisterKeyRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/relying-party/keys`.\n\nEverything here is the *institution's* material. This service generates none\nof it and holds no private half of any of it.","required":["key_id","alg","public_key","not_before","not_after","attestation"],"properties":{"alg":{"type":"string","description":"`x25519` to receive sealed record keys, `ed25519` to sign announcements.","example":"x25519"},"attestation":{"$ref":"#/components/schemas/AttestationInput","description":"The institution's own announcement of this key, as it publishes it at\n`/.well-known/verigrant-relying-party.json`."},"key_id":{"type":"string","description":"The institution's own label for this key generation, e.g. `2026-q3`.\n\nChosen by the institution rather than by us, because the institution has\nto find the matching private key in its own KMS from what a grant bundle\nnames, and a uuid we invented would not be in there.","example":"2026-q3"},"not_after":{"type":"string","format":"date-time"},"not_before":{"type":"string","format":"date-time"},"public_key":{"type":"string","description":"The 32-byte public key, base64.","example":"TFNSc2VjcmV0S2V5Qnl0ZXNHb0hlcmVBQUFBQUFBQUE="}}},"RegisterRequest":{"type":"object","description":"Request body for `POST /orgs/{id}/relying-party`.\n\nEverything after `domain` is the vetting the founder handoff asks for, and\nevery field of it is required. A registration that named half of it would be\na queue entry an administrator has to bounce, which is worse for the\ninstitution than a `400` naming the field they left out.","required":["kind","domain","legal_name","jurisdiction","registration_id","responsible_person_name","responsible_person_title","responsible_person_email","agreement_accepted","agreement_version"],"properties":{"agreement_accepted":{"type":"boolean","description":"**The acceptance.** `true`, or this is a `400` and nothing is written.\n\nA separate field from the version rather than a version implying\nacceptance, because the two say different things: the version is what was\nshown, and this is that somebody pressed it. A client that sent a version\nwith no press would be recording an agreement nobody made."},"agreement_version":{"type":"string","description":"Which wording was accepted, as the client rendered it.\n\nCarried from the client rather than stamped here with\n[`AGREEMENT_VERSION`] for the reason that constant's own documentation\ngives: the client is what showed the text, and a service that overwrote\nthis would be recording what it meant to show.","example":"2026-09-13"},"domain":{"type":"string","description":"The host that speaks for this institution: where a client goes to fetch\n`/.well-known/verigrant-relying-party.json` and check our answer about\nits keys. No scheme, no path, no port — see the migration's note on why\nthis is a string comparison rather than a parsing exercise.","example":"acme-university.edu"},"jurisdiction":{"type":"string","description":"Which register holds that file, as the institution writes it.\n\nFree text and not a country code. The register an operator has to open\nbelongs to a jurisdiction rather than to a country, and a two-letter code\nwould send them to the wrong one in every federal system.","example":"England and Wales"},"kind":{"type":"string","description":"One of [`RP_KINDS`]. What kind of institution this is, from the product\nmodel's first-group verticals.","example":"employer"},"legal_name":{"type":"string","description":"The name this institution is **registered** under, which is often not the\nname it trades as.\n\nThe organization's own name is the trading name: it is what a candidate\nreads on a posting. This is what a public register has a file on, and the\ndiligence is a person checking the two against each other.","example":"Aperture Robotics Holdings Limited"},"registration_id":{"type":"string","description":"The company number, registration number or tax identifier that\njurisdiction files this institution under.\n\nFree text for the same reason, and one field rather than a choice between\nthree, because a sole trader with a tax identifier and a chartered\nuniversity with a charter number are answering the same question.\n\nNot a secret and not treated as one. It is the handle on a public filing\nand it is printed on the institution's own invoices.","example":"09876543"},"responsible_person_email":{"type":"string","description":"Their address at the institution, which an operator compares against the\ndomain above. A responsible person reachable only at a free mailbox is\nthe cheapest signal this diligence has.","example":"dana.okafor@acme-university.edu"},"responsible_person_name":{"type":"string","description":"Who at this institution is answerable for what it reads.","example":"Dana Okafor"},"responsible_person_title":{"type":"string","description":"What they do there. A title is what makes \"answerable\" checkable: an\noperator can tell a head of talent from a summer intern, and a name on\nits own does not let them.","example":"Head of Talent"}}},"Registration":{"type":"object","description":"An organization's standing as an institution this service will disclose to,\nand the vetting behind it.\n\n**Not what the public registry serves.** Half of this is diligence material —\na legal name, a register entry, a named member of staff with an address — and\nan open route that published an institution's keys should not also publish a\nperson. [`registry`] selects [`REGISTRY_COLUMNS`] into its own shape for that\nreason, and the two statements are deliberately separate rather than one\nstatement with a filter somebody could forget.","required":["org_id","org_slug","org_name","kind","domain","status","created_at"],"properties":{"agreement_accepted_at":{"type":["string","null"],"format":"date-time"},"agreement_version":{"type":["string","null"],"description":"The version of the relying-party agreement most recently accepted, and\nwhen. From `relying_party_agreement`, newest first.","example":"2026-09-13"},"created_at":{"type":"string","format":"date-time"},"domain":{"type":"string","example":"acme-university.edu"},"jurisdiction":{"type":["string","null"],"example":"England and Wales"},"kind":{"type":"string","description":"One of [`RP_KINDS`].","example":"employer"},"legal_name":{"type":["string","null"],"description":"The name on the register, the register, and the number. `null` on a\nregistration made before vetting existed.","example":"Aperture Robotics Holdings Limited"},"org_id":{"type":"string","format":"uuid"},"org_name":{"type":"string","example":"Aperture Robotics"},"org_slug":{"type":"string","example":"aperture-robotics"},"registered_at":{"type":["string","null"],"format":"date-time"},"registration_id":{"type":["string","null"],"example":"09876543"},"rejected_at":{"type":["string","null"],"format":"date-time","description":"When the registration was refused, as distinct from when a good one was\nwithdrawn. Both are `suspended`; only one of them has this."},"responsible_person_email":{"type":["string","null"],"example":"dana.okafor@acme-university.edu"},"responsible_person_name":{"type":["string","null"],"description":"The named responsible person.\n\nRead by this institution's own members, through\n[`get_registration`], and by an administrator reviewing it. Not by\nanybody else, and in particular not by [`registry`], which selects\n[`REGISTRY_COLUMNS`] for exactly this reason: who is answerable at an\ninstitution is its own staff's business and the platform's, and an open\nroute that publishes key material should not also publish a person.","example":"Dana Okafor"},"responsible_person_title":{"type":["string","null"],"example":"Head of Talent"},"review_note":{"type":["string","null"],"description":"What the reviewer wrote: the grounds for a refusal, or a note on what was\nchecked.\n\nRead by the institution as well as by the operator, which is the same\nchoice `org_billing.note` makes and for the same reason: a fact recorded\nabout somebody that they may not see is a worse artifact than no note."},"reviewed_at":{"type":["string","null"],"format":"date-time","description":"When a person decided this, and who. Both `null` on a registration that\nnobody has reached yet, and on one written before vetting existed."},"reviewed_by":{"type":["string","null"],"format":"uuid"},"status":{"type":"string","description":"`pending`, `registered` or `suspended`. Only `registered` opens the gate.\n\nA new registration is `pending` and waits for an administrator. See the\nmodule documentation.","example":"pending"},"suspended_at":{"type":["string","null"],"format":"date-time"}}},"RegistryResponse":{"type":"object","description":"The answer to `GET /rp/{slug}/keys`: an institution, and the keys a client\nmay seal to it.","required":["org_id","org_slug","org_name","kind","domain","status","keys","proof_verification_implemented"],"properties":{"domain":{"type":"string","example":"acme-university.edu"},"keys":{"type":"array","items":{"$ref":"#/components/schemas/PublicKeyView"},"description":"Live keys only, revoked ones omitted. Several may be active at once,\nwhich is what makes rotation not break an outstanding grant.\n\nThe institution's Ed25519 attestation key is one of them, with its own\ninclusion proof against the same head. A client needs it: the signature\non an X25519 key's attestation is only worth anything if the key that\nmade it is itself in the log."},"kind":{"type":"string","example":"school"},"org_id":{"type":"string","format":"uuid"},"org_name":{"type":"string","example":"Acme University"},"org_slug":{"type":"string","example":"acme-university"},"proof_verification_implemented":{"type":"boolean","description":"**Whether the cryptographic half of key verification is implemented.**\n\nReported on the wire, and not only in a document, because it is the\ndifference between a client that can prove who it is sealing to and one\nthat is taking this service's word for it. `true` since the proofs\nlanded. See the module documentation."},"status":{"type":"string","description":"Always `registered` here: a pending or suspended institution is not in\nthe registry at all, and answers `404` like one that does not exist.","example":"registered"}}},"RejectRegistrationRequest":{"type":"object","description":"Request body for `POST /admin/relying-parties/{id}/reject`.","required":["reason"],"properties":{"reason":{"type":"string","description":"Why. Required, for the reason [`crate::review::reject`] requires one:\nthis is the sentence the institution reads on its own screen, and a\nrefusal with no grounds is one it can only respond to by writing to us.","example":"The company number does not match the legal name on the register."}}},"RejectVerificationRequest":{"type":"object","description":"Request body for `POST /admin/verifications/{id}/reject`.","required":["reason"],"properties":{"reason":{"type":"string","description":"Why. Required, and required for a reason: this is the sentence the person\nwhose claim was refused reads in their own `GET /me/verifications`, and a\nrefusal with no grounds is one they can only respond to by asking.","example":"The document supplied had expired."}}},"RequestResetRequest":{"type":"object","description":"Request body for `POST /auth/account-reset`: an address and nothing else.\n\nNo `Debug`, like every other request type on the unauthenticated surface.\nAn email is not a credential; the habit is what keeps a field that *is* one\nfrom being added to a `Debug` type later.","required":["email"],"properties":{"email":{"type":"string","example":"ada@example.com"}}},"ResealedCapability":{"type":"object","description":"The new scope key, sealed to one capability the user did not revoke.","required":["capability_id","payload_version","sealed_key"],"properties":{"capability_id":{"type":"string","format":"uuid"},"payload_version":{"type":"integer","format":"int32","description":"The version this blob was sealed under, which is bound into its\nassociated data. Must be exactly one past what this service holds, so\nthat an agent refusing a payload below the highest it has seen is\nrefusing a rollback rather than a re-seal.","example":2},"sealed_key":{"type":"string","description":"`seal(agent_box_pub, SRK_{scope,epoch+1}, AAD_cap)`, 80 bytes, base64.","example":"base64 of 80 bytes"}}},"ResetRequest":{"type":"object","description":"Request body for `POST /auth/reset`.\n\nNo `Debug`: it holds a plaintext password *and* a live credential, which is\ntwo reasons where the rest of this module needs one.","required":["token","password"],"properties":{"password":{"type":"string","description":"The new password. At least 12 characters, exactly as `POST /users` asks.","example":"a long passphrase nobody has written down"},"token":{"type":"string","description":"The value of the `reset` query parameter in the emailed link.","example":"<43-character opaque token>"}}},"RevokedOthers":{"type":"object","description":"What `POST /me/sessions/revoke-others` did.\n\nA count rather than the list of ids: the sessions it names are gone by the\ntime the answer is written, so a client's only correct next act is to read\nthe list again, and a number is what turns this into a sentence a person can\nbe shown.","required":["revoked"],"properties":{"revoked":{"type":"integer","format":"int64","description":"How many live sessions this call ended. Zero when the caller was already\nsigned in nowhere else, which is a successful answer and not an error.","example":2}}},"RotateScopeRequest":{"type":"object","description":"Request body for `POST /me/scopes/{scope}/rotate`.","required":["from_epoch","records"],"properties":{"capabilities":{"type":"array","items":{"$ref":"#/components/schemas/ResealedCapability"},"description":"Every capability that is live over this scope, re-sealed to. Not the one\nbeing revoked — that omission is the revocation."},"from_epoch":{"type":"integer","format":"int32","description":"The epoch the client derived the *old* key at, and therefore the epoch\nevery wrap in this body was unwrapped under. Compare-and-set: an epoch\nthat has moved is a `409` rather than a second bump.","example":1},"records":{"type":"array","items":{"$ref":"#/components/schemas/RotatedRecord"},"description":"Every live record in the scope, re-wrapped. Not some of them; see the\nmodule documentation on why a partial rotation is the one outcome this\nroute must not produce."}}},"RotatedRecord":{"type":"object","description":"One record's DEK, re-wrapped under the scope's new root key.\n\nThe ciphertext is not here and never will be: a rotation re-wraps 48 bytes\nper record and re-encrypts nothing, which is what makes cryptographic\nrevocation cheap enough that this service will actually do it.","required":["record_id","record_version","wrapped_dek","dek_nonce"],"properties":{"dek_nonce":{"type":"string","description":"Its 24-byte nonce.","example":"base64 of 24 bytes"},"record_id":{"type":"string","format":"uuid","description":"The record whose wrap is being replaced. `record_id` rather than `id`,\nlike `grant_bundle_entry`, because the thing this object identifies is not\nitself a resource with an id of its own."},"record_version":{"type":"integer","format":"int64","description":"The version the client unwrapped. A version that has moved since is a\n`409`: the row the client re-wrapped is not the row that is there.","example":3},"wrapped_dek":{"type":"string","description":"`XChaCha20Poly1305(SRK_{scope,epoch+1})(DEK)` with the row's own\nunchanged associated data. 48 bytes, base64.","example":"base64 of 48 bytes"}}},"RotatedScope":{"type":"object","description":"The answer: what the scope now is.","required":["scope","epoch","records_rewrapped","capabilities_resealed","rotation_pending","rotated_at"],"properties":{"capabilities_resealed":{"type":"integer","format":"int64"},"epoch":{"type":"integer","format":"int32","description":"The scope's epoch after the bump, which is what the client writes into\nthe next record it seals in this scope."},"records_rewrapped":{"type":"integer","format":"int64"},"rotated_at":{"type":"string","format":"date-time"},"rotation_pending":{"type":"boolean","description":"`false` on every successful rotation, and present so that the state the\nclient was acting on is answered rather than assumed."},"scope":{"type":"string"}}},"RunAccepted":{"type":"object","description":"The answer to `POST /me/agent/run`.\n\nA job id rather than a result: the work outlives the request by design, and a\nroute that waited for three provider calls would be a route that times out.","required":["job_id","already_queued"],"properties":{"already_queued":{"type":"boolean","description":"`true` when the caller already had a run queued or in flight and this\nrequest enqueued nothing. Pressing the button twice is one run, not two\n— which matters most in live mode, where a run spends the caller's key."},"job_id":{"type":"string","format":"uuid","description":"The queued job. It reaches `done` or `failed` in `job_queue`; what it\nproduced arrives at `GET /me/agent/suggestions`."},"submit_grant_id":{"type":["string","null"],"format":"uuid","description":"The delegation this run may submit under, where the caller authorised\none. It is an ordinary grant carrying `applications:submit`: it is in\n`GET /me/grants`, and `DELETE /me/grants/{id}` stops the run — including\nwhile it is in flight, because the authority is re-read at the moment of\neach submission. `null` for a run that will only draft."}}},"ScheduledReset":{"type":"object","description":"What a confirmed reset answers with.\n\nThe address is carried for the reason [`crate::auth::VerifiedEmail`] carries\none: this is opened in whatever browser a mail client handed the link to,\nwhich is very often not one that is signed in to anything, and a screen that\ncould not say *which* account it had just scheduled the destruction of would\nbe a screen nobody should press a button on. It discloses nothing — whoever\nholds the token was reading the mailbox it was sent to.","required":["email","requested_at","due_at","deadline"],"properties":{"deadline":{"type":"string","description":"The same instant, worded as the messages about it word it.\n\nCarried rather than left to the client for the reason [`PendingReset`]\ncarries one: this screen and the mail that arrives a second later are\nnaming the same hour, and a second formatting of one instant is how they\nend up naming two.","example":"17 September 2026 at 09:30 UTC"},"due_at":{"type":"string","format":"date-time","description":"When the account will be destroyed: [`DELAY_HOURS`] later. Anybody who\ncan sign in may cancel until then."},"email":{"type":"string","example":"ada@example.com"},"requested_at":{"type":"string","format":"date-time","description":"When this confirmation was taken."}}},"ScopeEpoch":{"type":"object","description":"One `(scope, epoch)` pair: which scope root key is current.","required":["scope","epoch","rotated_at"],"properties":{"epoch":{"type":"integer","format":"int32","example":1},"rotated_at":{"type":"string","format":"date-time"},"scope":{"type":"string","example":"history:read"}}},"ScreeningQuestion":{"type":"object","description":"One screening question, in the shape [`crate::autofill`] answers.\n\n`Deserialize` for the write routes and `Serialize` for the read ones, over\nthe same type, so what a client sends back is what it was given. The stored\nJSON is this type re-serialised after validation rather than the caller's\nbytes: text trimmed, `answer_type` in the spelling the closed set names, and\n`options` absent rather than `null` when there are none.","required":["text","answer_type"],"properties":{"answer_type":{"type":"string","description":"One of `boolean`, `single`, `multi`, `text`, `numeric`, `date`, `file` —\nthe same set the answer bank files an answer under, so an answer to this\nquestion is storable by `PUT /me/answers` without translation.","example":"boolean"},"options":{"type":["array","null"],"items":{"type":"string"},"description":"The choices offered, for a `single` or `multi` question. Absent, not\n`null`, when the question offers none.","example":["Yes","No"]},"text":{"type":"string","description":"The question as the employer would put it on the page.","example":"Do you hold a current Class 1 licence?"}}},"SealBundleRequest":{"type":"object","description":"Request body for `POST /me/grants/{id}/bundle`.\n\nEverything here was produced in the caller's own browser. This service\nreceives ciphered key material, a signed statement about it, and labels.","required":["kind","rp_key_id","statement","statement_sig","entries"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/BundleEntry"},"description":"One entry per record, at most 200, with no record named twice."},"kind":{"type":"string","description":"`preview` or `full`.","example":"preview"},"rp_key_id":{"type":"string","description":"The institution's own label for the key generation sealed to, e.g.\n`2026-q3`. It must name a live, registered X25519 key of the institution\nthis grant was issued to.","example":"2026-q3"},"statement":{"type":"string","description":"The exact RFC 8785 bytes the user's signing key signed, base64.\n\nHeld verbatim and never re-serialised: a signature is over bytes, and a\nround trip through a JSON library produces a document the signature no\nlonger covers.","example":"base64 of the canonical statement"},"statement_sig":{"type":"string","description":"Ed25519 over `statement` by the account's `sign_pub`, base64.","example":"base64 of 64 bytes"}}},"SeekerMatch":{"type":"object","description":"One of the seeker's matches: the score, the account of it, and enough of the\nposting to decide whether to open it.\n\nThe posting's columns are the public ones — this is what `GET /postings/{id}`\nserves a stranger — joined in rather than stored on the match, so a renamed\njob or a renamed company is right on the next read.","required":["posting_id","score","reasons","matched_at","scored_at","title","organization","remote"],"properties":{"employment_type":{"type":["string","null"]},"location":{"type":["string","null"]},"matched_at":{"type":"string","format":"date-time","description":"When this posting first became a match."},"organization":{"type":"string","example":"Aperture Robotics"},"posting_id":{"type":"string","format":"uuid"},"reasons":{"type":"array","items":{"type":"object"},"description":"One entry per factor: `{factor, weight, ratio, points, detail}`. **This is\nthe explanation**, and it is what was concluded at the time it was\nconcluded rather than something recomputed on read."},"remote":{"type":"boolean"},"score":{"type":"number","format":"double","description":"0 to 100, out of the factors that applied to this vacancy.","example":78.5},"scored_at":{"type":"string","format":"date-time","description":"When the score was last recomputed, which is what the ranking breaks ties\non."},"title":{"type":"string","example":"Class 1 Driver — Leeds"}}},"SenderPreference":{"type":"object","description":"One sender, and what its recipient has decided about it.","required":["sender_org_id","auto_include","updated_at"],"properties":{"auto_include":{"type":"boolean","description":"Whether items from this sender may be included in a standing grant\nwithout being asked each time.\n\n**Stored and not yet consumed.** See the module documentation: the grant\nmachinery reads this in the stage that adds standing grants, and until\nthen it is a recorded intention rather than a behaviour."},"blocked_at":{"type":["string","null"],"format":"date-time","description":"When this sender was blocked, or `null`. A blocked sender is refused at\nthe push route and its earlier items leave the inbox."},"sender_kind":{"type":["string","null"]},"sender_org_id":{"type":"string","format":"uuid"},"sender_org_slug":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},"SensitiveId":{"type":"object","description":"The sensitive-identifier singleton. Two fields, and there will never be a\nthird that is a full social security number — see the migration.","required":["updated_at"],"properties":{"date_of_birth":{"type":["string","null"],"format":"date","description":"Isolated here rather than on `profiles` precisely because age\ndiscrimination is the risk: a profile read cannot carry it, because it is\nnot in the profile.","example":"1988-04-12"},"ssn_last4":{"type":["string","null"],"description":"The last four digits, as text: leading zeros are meaningful, and `\"0042\"`\nis not `42`. The full number is not stored, anywhere, in any form.","example":"0042"},"updated_at":{"type":"string","format":"date-time"}}},"SessionRecord":{"type":"object","description":"One session, as history. `token_hash` is not a field of this type, so it\ncannot leak through it.","required":["id","created_at","expires_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_seen_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time","description":"Set by a logout, and `null` for a session that simply lapsed."}}},"SessionSummary":{"type":"object","description":"One live session, as its owner reads it back.\n\nNo token and no `token_hash`: the fields are the ones a person needs in\norder to recognise a session and decide about it, and a credential is not\none of them.","required":["id","created_at","expires_at","current"],"properties":{"created_at":{"type":"string","format":"date-time","description":"When this session was minted, which is when somebody signed in."},"current":{"type":"boolean","description":"True for the one session this very request authenticated with, so a\nclient can mark it and can warn before ending it. Decided by the server\nfrom the session the request arrived on, because the client has only a\ntoken and the list deliberately never carries one to compare against."},"expires_at":{"type":"string","format":"date-time","description":"Thirty days after it was minted. The session can also end sooner, by\nrevocation here, by logout, or by the account being disabled."},"id":{"type":"string","format":"uuid"},"ip":{"type":["string","null"],"description":"The address the login that minted this session came from, or `null` if\nno `auth.login` entry for it is still in the trail. A string rather than\na typed address: it is v4 or v6 depending on how the client reached us,\nand it exists here to be shown to a person rather than matched on.","example":"203.0.113.7"},"last_seen_at":{"type":["string","null"],"format":"date-time","description":"The last request this session authenticated, or `null` for one that has\nmade none since it was issued."}}},"SetBillingRequest":{"type":"object","description":"Request body for `PATCH /admin/orgs/{id}/billing`.\n\nEvery field optional, and at least one required: this is a `PATCH`, so\nnaming only the status leaves the price alone, and a body naming nothing is a\nrequest that means nothing rather than a `200` that changed nothing.","properties":{"annual_fee_cents":{"type":["integer","null"],"format":"int32","description":"What a `contract` institution pays a year, as agreed with a person.\n**Recorded, never charged** — see [`Billing::annual_fee_cents`]. Must not\nbe negative; zero means none recorded.","example":1200000},"included_free_pulls":{"type":["integer","null"],"format":"int32","description":"How many pull-throughs this institution's plan includes before the unit\nprice applies — and, on `free` alone, before the next one is refused.\nMust not be negative.\n\nMoved by a plan change like the two prices are, because it is part of\nwhat a plan costs: forty included at 500 is `platform`'s offer and ten at\n1200 is the other two's. Naming it here is how an operator agrees\nsomething else.","example":10},"monthly_fee_cents":{"type":["integer","null"],"format":"int32","description":"What this institution pays a month before anything is metered, in cents.\nCharged on `platform` and nowhere else. Must not be negative.","example":20000},"note":{"type":["string","null"],"description":"The operator's sentence about this arrangement. An empty string clears\nit; a body that does not name it leaves it alone.\n\n**The institution's owner can read this**, on `GET /orgs/{id}/billing`.\nSee [`Billing::note`] for why that is the right way round.","example":"12-month contract, three campuses, renews in September"},"plan":{"type":["string","null"],"description":"One of `free`, `per_pull`, `platform`, `contract`.\n\nNaming a plan the institution is not already on applies that plan's\n[`list_price`] to whichever of the three priced fields this request does\nnot name — so moving somebody onto `platform` and saying nothing else\nputs them on 20000 a month, 500 a pull and 40 included. Naming the plan\nthey already hold changes no price at all.","example":"platform"},"status":{"type":["string","null"],"description":"One of `active`, `past_due`, `suspended`.","example":"active"},"unit_price_cents":{"type":["integer","null"],"format":"int32","description":"What one pull-through costs, in cents. Charged on `per_pull` and\n`platform`; see [`Billing::unit_price`]. Must not be negative.\n\nOn `contract` it is recorded rather than charged — what the annual works\nout at per candidate, which at volume is six to eight hundred — and the\nplan's zero is what an invoice uses.","example":1000}}},"SetKeyRequest":{"type":"object","description":"Request body for `PUT /me/llm-key`.\n\nDeliberately no `Debug`, like [`crate::users::CreateUserRequest`]: it holds\na live API key, and one stray `{:?}` would put it in the logs.","required":["provider","api_key"],"properties":{"api_key":{"type":"string","description":"The key itself, at most 512 characters. Trimmed, encrypted, and dropped\n— it is never returned by this endpoint or any other, and never reaches\na log line or an audit row.","example":"<your provider API key>"},"provider":{"type":"string","description":"`anthropic`, which is the whole of [`SUPPORTED_PROVIDERS`] today. A\nclosed set, checked before anything is encrypted: an unrecognised\nprovider is a row the agent cannot route.","example":"anthropic"}}},"SetRoleRequest":{"type":"object","description":"Request body for `PATCH /orgs/{id}/members/{member_id}`.\n\nOne field, and it is required: this route exists to change a role, so an empty\nbody is a request that means nothing rather than a no-op worth answering\n`200`.","required":["role"],"properties":{"role":{"type":"string","description":"`owner`, `admin` or `recruiter`. Only an owner may set `owner`, and only\nan owner may change the role of an owner.","example":"admin"}}},"SetSenderRequest":{"type":"object","description":"Request body for `PUT /me/inbox/senders/{org_id}`.\n\nMerge semantics, like `PATCH /me/settings`: an omitted field leaves the\nstored decision alone, so a client that only wants to block does not have to\nknow what the auto-include setting currently is and cannot clear it by\naccident. An empty body reads the current decision back.","properties":{"auto_include":{"type":["boolean","null"],"description":"Whether items from this sender may be included in a standing grant.\nStored; see [`SenderPreference::auto_include`]."},"blocked":{"type":["boolean","null"],"description":"Refuse this sender's pushes, and take the items it has already sent out\nof the inbox. `false` lets it send again and puts those items back."}}},"SetStageRequest":{"type":"object","description":"Request body for `PATCH /orgs/{id}/applications/{application_id}`.\n\nOne field, and it is required. `stage` is the only column of an application\nan employer may write — the trigger in\n`20260819000011_applicant_pipeline.sql` says so to Postgres — so a body with\nanything else in it would be describing a request this service cannot make.","required":["stage"],"properties":{"stage":{"type":"string","description":"One of `applied`, `screening`, `interview`, `offer`, `hired`, `rejected`,\n`withdrawn`.","example":"interview"}}},"Settings":{"type":"object","description":"What `PATCH /me/settings` answers with: the settings as they now stand.","required":["notification_email","email_verified"],"properties":{"email_verified":{"type":"boolean","description":"Repeated from the account so a client can render the switch and the\nreason it is doing nothing in one pass. An account with no confirmed\naddress has a switch that is set and a channel that is off, and a screen\nthat could not say so would be a switch that appears broken."},"notification_email":{"type":"boolean","example":true}}},"SettingsPatch":{"type":"object","description":"Request body for `PATCH /me/settings`.\n\nOne field today, and `PATCH` rather than `PUT` so that the second one — if\nthere is ever a second one — does not silently clear the first for every\nclient written against this version. An absent field means \"leave it alone\",\nwhich is what merge semantics mean everywhere else in this service.","properties":{"notification_email":{"type":["boolean","null"],"description":"Whether this account's notices are also mailed to its confirmed address.\n\nOnly ever consulted for a verified address, so setting it to `true` on an\nunverified account means \"would, if asked\" rather than \"is being\" — which\nis why the route accepts it either way rather than refusing on behalf of\nsomebody who has not got round to opening a link yet.","example":true}}},"SharedDraftResponse":{"type":"object","description":"Response body for the grant-authenticated path.\n\nThe same three fields, plus the one thing a delegate does not otherwise know\nfor certain: whose profile it just drafted from. The holder presented a\ntoken, not a user id, so `on_behalf_of` is the server telling it which user\nthe grant resolved to — and it comes from the grant row, not from anything\nthe caller sent.","required":["draft","provider","on_behalf_of"],"properties":{"draft":{"type":"string","description":"The application text, drafted only from facts in the grant's user's\nprofile."},"model":{"type":["string","null"],"description":"Omitted when the provider named no model."},"on_behalf_of":{"type":"string","format":"uuid","description":"The user this draft was made for: the grant's owner, from the row."},"provider":{"type":"string","description":"`mock`, `anthropic` or `claude-cli`.","example":"mock"}}},"SharedRecord":{"type":"object","description":"One encrypted record, as an institution receives it.\n\nEvery component of `AAD_rec` travels with the row, exactly as it does on the\nowner's own read: a reader that has never seen this row can rebuild the\nassociated data it was sealed under, and can therefore tell an honest row\nfrom a substituted one.","required":["id","scope","record_type","record_version","write_epoch","byte_class","ciphertext","ct_nonce","created_at","updated_at"],"properties":{"byte_class":{"type":"integer","format":"int32"},"ciphertext":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"ct_nonce":{"type":"string"},"id":{"type":"string","format":"uuid"},"record_type":{"type":"string","example":"work_history"},"record_version":{"type":"integer","format":"int64"},"scope":{"type":"string","example":"history:read"},"updated_at":{"type":"string","format":"date-time"},"write_epoch":{"type":"integer","format":"int32"}}},"SharedRecords":{"type":"object","description":"The answer to `GET /shared/records`: everything an institution needs to\ndecrypt, and nothing this service could have used to.","required":["grant_id","user_id","kind","rp_key_id","rp_key_fingerprint","statement","statement_sig","user_sign_pub","issued_by","sealed_at","entries","records","stale"],"properties":{"agent_sign_pub":{"type":["string","null"]},"capability_statement":{"type":["string","null"],"description":"The exact bytes of the mandate, base64."},"capability_statement_sig":{"type":["string","null"]},"entries":{"type":"array","items":{"$ref":"#/components/schemas/BundleEntry"},"description":"One sealed key per record, opening exactly the version it names."},"grant_id":{"type":"string","format":"uuid"},"issued_by":{"type":"string","description":"`user` or `agent`: which of the two modes produced this disclosure.\n\nOn `agent` the three fields below carry the second link of the chain —\nthe mandate the account signed, and the agent key it authorises — and\n`vg_rp::open` refuses the disclosure unless both signatures hold. An\ninstitution reading this is being told, verifiably, that a machine issued\nthis under a standing authority rather than that a person clicked\napprove, which is a distinction worth having in a consent artifact.","example":"user"},"kind":{"type":"string","example":"preview"},"records":{"type":"array","items":{"$ref":"#/components/schemas/SharedRecord"},"description":"The ciphertext rows, with every component of their associated data."},"rp_key_fingerprint":{"type":"string"},"rp_key_id":{"type":"string","example":"2026-q3"},"sealed_at":{"type":"string","format":"date-time"},"stale":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Records in the bundle that this read does not carry.\n\nEither the version has moved — an edit mints a fresh DEK and replaces the\nrow's ciphertext, so what the entry's key opens is gone rather than\nsuperseded — or the record has since been rewritten into a scope this\ndelegation does not carry, which this service refuses to serve however it\nwas sealed. Reported rather than silently omitted, because an institution\nthat asked for five records and received four should be told which one to\nask for again under a new disclosure."},"statement":{"type":"string","description":"The consent artifact: the exact bytes the user signed."},"statement_sig":{"type":"string"},"user_id":{"type":"string","format":"uuid","description":"The account whose record this is. Part of every record's associated\ndata, so an institution rebuilding `AAD_rec` needs it — and it is in the\nsigned statement too, which is the copy to trust."},"user_sign_pub":{"type":"string","description":"The account's published Ed25519 key, base64, so the statement can be\nverified without a second call. An institution that wants to check this\nagainst another source is right to, and `GET /rp/...` is not it — the\nkey is the account's, published in its keyring."}}},"ShiftEntry":{"type":"object","required":["id","day_of_week","available","overnight"],"properties":{"available":{"type":"boolean","description":"`false` records a definite \"not this day\", which is a different answer\nfrom having said nothing about it."},"day_of_week":{"type":"integer","format":"int32","description":"0 is Sunday, 6 is Saturday."},"end_time":{"type":["string","null"],"example":"17:30:00"},"id":{"type":"string","format":"uuid"},"overnight":{"type":"boolean","description":"Whether the shift runs past midnight, which is what makes an end time\nearlier than the start time legible rather than inverted."},"start_time":{"type":["string","null"],"description":"`HH:MM:SS`.","example":"09:00:00"}}},"SignedTreeHead":{"type":"object","description":"The answer to `GET /rp/keys/log/sth`.","required":["tree_size","root_hash","signed_tree_head","public_key"],"properties":{"public_key":{"type":"string","description":"The log's Ed25519 publication key, base64.\n\nPublished so a client or a mirror can be *provisioned*, and it is worth\nbeing blunt about what that is not: a client that takes this key from\nthis response and then uses it to check this response has verified that\nwe can sign our own bytes. The key has to be pinned out of band, and the\nvalue of publishing it here is that somebody comparing a pinned key\nagainst what this endpoint says can tell when they differ."},"root_hash":{"type":"string","description":"The Merkle root over every leaf, base64."},"signed_tree_head":{"type":"string","description":"The self-contained signed artifact clients pin and gossip, base64:\n`\"vgrp.sth.v1.\" || tree_size || root || signature`."},"tree_size":{"type":"integer","format":"int64"}}},"Source":{"type":"string","description":"Where an answer came from.","enum":["structured","answer_bank","gap"]},"StartRunRequest":{"type":"object","description":"Request body for `POST /me/agent/run`. Optional in every sense: a request\nwith no body at all is a run that drafts and submits nothing, which is what\nthis route has always done.","properties":{"submit":{"type":"boolean","description":"Authorise this run to submit the applications it drafts, for the\nsuggestions that come back with no unanswered questions.\n\nWhat it does is mint a short-lived `applications:submit` grant and hand\nthe run its id. The permission is therefore a delegation like any other\nrather than a flag: the caller can see it, and withdraw it, and the run\nchecks that it still stands before every submission. A caller who already\nholds a standing `applications:submit` grant does not need this — the run\nfinds it — and sending it anyway mints nothing extra."}}},"Status":{"type":"string","description":"Where a check has got to.\n\nAlso a closed set with a matching `CHECK` constraint. `expired` is distinct\nfrom `failed` on purpose: a check that failed says something about the\nclaim, and one that expired says only that it is old.","enum":["pending","verified","failed","expired"]},"StoreAnswerRequest":{"type":"object","description":"Request body for `PUT /me/answers`.\n\nThe question text is the key — there is no id to know in advance, which is\nexactly the situation a form-filler is in — so this is a `PUT` to the\ncollection, upserting on the fingerprint. `PUT /me/availability-shifts` is\nkeyed by day for the same reason.\n\n**There is no value on this type.** The value is in the record `record_id`\nnames, which the client wrote a moment earlier through\n`PUT /me/records/{id}`. What is left here is the question, the labels that\ndescribe it, and two ids.","required":["question"],"properties":{"answer_type":{"type":["string","null"],"description":"Required, and this is the one field sealing made compulsory. A service\nthat cannot read a value cannot work out what kind of value it is, so the\nclient says — and the declaration is still held to the type the question\nis already on file as.\n\n`Option` for [`Self::record_id`]'s reason, and it was optional before\nthis change as well: it used to be inferred from the value, and a value\nis no longer there to infer it from.","example":"boolean"},"confidence":{"type":["string","null"],"description":"`user_confirmed` (the default) or `inferred`."},"options":{"type":["object","null"],"description":"The choices the form offered, for a `single` or `multi` question."},"question":{"type":"string","description":"The question exactly as the form asked it.","example":"Have you ever been employed by us before?"},"record_id":{"type":["string","null"],"format":"uuid","description":"The `encrypted_record` holding the answer, which must be one of the\ncaller's own, live, in scope `answers:read` and of type\n`screening_answer`.\n\nMinted by the client, because the id is authenticated into the\nciphertext: see `PUT /me/records/{id}`, which says why a server that\nallocated it would be asking the client to encrypt against a value it\ndoes not yet know.\n\nRequired, and `Option` only so that a body without it reaches\n[`store_answer`] and is refused in words rather than as a deserialiser's\n\"missing field\". That distinction is the whole of what a client written\nagainst the old route sees when it first meets this one, and \"missing\nfield `record_id`\" tells such a client nothing about what changed."},"topic":{"type":["string","null"],"example":"employment_history"},"value_bool":{"type":["object","null"],"description":"The five columns a value used to arrive in. Accepted by the deserialiser\nonly so that [`store_answer`] can refuse them **by name**; see\n[`RETIRED_VALUE_FIELDS`]."},"value_date":{"type":["object","null"]},"value_doc_id":{"type":["string","null"],"format":"uuid","description":"The id of one of the caller's own documents, on a `file` answer and only\non one.\n\nA label rather than a value, which is why it is still here in the clear\nwhile the answer itself is not: it is the column `ON DELETE SET NULL`\nacts on when the file it names is forgotten, and a client that sealed it\nand nothing else would have an answer this service could not truthfully\nempty. The record carries it too, so a reader need not trust this copy."},"value_multi":{"type":["object","null"]},"value_num":{"type":["object","null"]},"value_text":{"type":["object","null"]}}},"StoredAnswer":{"type":"object","description":"One answer, with the question it answers.\n\nThe question travels with the answer on every read: an answer bank that\nreturned values without the questions they belong to would be a column of\n\"yes\"es nobody could use.\n\n**Read `crypto_version` first.** On a `2` the five `value_*` fields below are\n`null` and [`StoredAnswer::record_id`] names the record holding the value; on\na `1` they are the plaintext this service still holds for an answer written\nbefore blocker 9. `value_doc_id` is on neither side of that line: it is an\nid, it is on section 6's list of what stays plaintext, and it is set on both.","required":["id","question_id","fingerprint","question","answer_type","crypto_version","confidence","created_at","updated_at"],"properties":{"answer_type":{"type":"string","description":"One of `boolean`, `single`, `multi`, `text`, `numeric`, `date`, `file`."},"confidence":{"type":"string","description":"`user_confirmed` or `inferred`."},"created_at":{"type":"string","format":"date-time"},"crypto_version":{"type":"integer","format":"int32","description":"`1` for an answer written before the value became ciphertext — the five\n`value_*` fields below carry it — and `2` for one sealed in its owner's\nclient, where they are all `null` and `record_id` is what to open.","example":2},"fingerprint":{"type":"string","description":"`v1:` and sixty-four hex digits. Stable for a given question text under\nthis version of the normalisation.","example":"v1:3f9c…"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":["string","null"],"format":"date-time","description":"When this answer was last handed to a form by [`lookup_answer`]. Storing\nan answer is not using it, so a write does not touch this."},"options":{"type":["object","null"],"description":"The choices the form offered, if it offered any."},"question":{"type":"string","description":"The question as it was actually written, not as it was normalised.","example":"Have you ever been employed by us before?"},"question_id":{"type":"string","format":"uuid"},"record_id":{"type":["string","null"],"format":"uuid","description":"The `encrypted_record` holding this answer's value, in scope\n`answers:read` with `record_type` `screening_answer`. `null` on a version\n1 answer."},"topic":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"},"value_bool":{"type":["boolean","null"],"description":"`null` on a sealed answer. See the type documentation."},"value_date":{"type":["string","null"],"format":"date","description":"`null` on a sealed answer."},"value_doc_id":{"type":["string","null"],"format":"uuid","description":"The document that answers a `file` question, on a sealed answer as much\nas on a legacy one. Deleting that document clears this, which leaves the\nanswer with no value — truthfully, since the file it named is gone."},"value_multi":{"type":["array","null"],"items":{"type":"string"},"description":"`null` on a sealed answer."},"value_num":{"type":["string","null"],"description":"`null` on a sealed answer. A decimal, as a string, for the reason the pay\nfields are one.","example":"3.5"},"value_text":{"type":["string","null"],"description":"`null` on a sealed answer."}}},"SubmitSuggestionRequest":{"type":"object","description":"Request body for submitting a suggestion.","properties":{"authorize":{"type":"boolean","description":"Authorise this one submission, minting a short-lived\n`applications:submit` grant for it. Omit it — or send `false` — and the\nrequest succeeds only if a standing authorisation already exists."}}},"Suggestion":{"type":"object","description":"One suggestion, as its owner reads it.\n\nThe posting's title and the employer's name are joined in rather than copied\nonto the row: a suggestion is about a live advertisement, and it is deleted\nwhen that advertisement is. `score` is what the matching engine made of the\npair, so the list can be read in the order the run worked it.","required":["id","posting_id","title","organization","draft","filled_answers","gaps","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"draft":{"type":"string","description":"The application text. Drafted from the caller's own record by whichever\nprovider this process is configured for — see [`super::LlmMode`]."},"filled_answers":{"type":"array","items":{"type":"object"},"description":"The screening questions the auto-fill engine answered, each with the\naccount of where the answer came from: `structured` from a profile field,\n`answer_bank` from something this user answered before."},"gaps":{"type":"array","items":{"type":"object"},"description":"The questions it could not answer — the genuinely new ones. Send what the\nuser says to `POST /me/agent/suggestions/{id}/answers`."},"id":{"type":"string","format":"uuid"},"organization":{"type":"string","example":"Aperture Freight"},"posting_id":{"type":"string","format":"uuid"},"score":{"type":["number","null"],"format":"double","description":"The match score behind this suggestion, or `null` if the match has since\nbeen recomputed away.","example":78.5},"status":{"type":"string","description":"`needs_answers` while anything is left to ask, `ready` once nothing is.","example":"needs_answers"},"title":{"type":"string","example":"Class 1 Driver — Leeds"},"updated_at":{"type":"string","format":"date-time","description":"When the suggestion was last rewritten: by a fresh run, or by the owner\nanswering one of its gaps."}}},"TotpStatus":{"type":"object","description":"Whether this account holds a second factor, as its owner reads it back.\n\nThree facts and no secret. There is no field here for the shared secret, for\na recovery code, or for a hash of either, so no reader of this module can\nserve one by accident — the only response that ever carries key material is\n[`Enrolment`], and it is written once and never read back.","required":["enabled","enrolled","recovery_codes_left"],"properties":{"enabled":{"type":"boolean","description":"True once [`confirm`] has proved the secret with a code computed from it.\nThis is the fact `/admin` insists on."},"enabled_at":{"type":["string","null"],"format":"date-time","description":"When the factor was enabled, or `null` while it is not."},"enrolled":{"type":"boolean","description":"True when a secret is stored at all, enabled or not. The two differ for\nexactly one state — an enrolment that was minted and never confirmed —\nand a client that could not tell them apart would offer \"set up a second\nfactor\" to somebody who is halfway through doing so."},"recovery_codes_left":{"type":"integer","format":"int64","description":"How many recovery codes are unspent. Which ones is not a question anybody\ncan ask: the codes are stored as hashes and spending one removes it.","example":10}}},"TrendDay":{"type":"object","description":"One day of the series: the date, and what happened on it.\n\nBoth counts on one row rather than two series of their own, because the\nscreen draws them against one axis and a shape that let them disagree about\nwhich days exist would be a shape that let a console draw two charts a day\nout of step with each other.","required":["day","signups","applications"],"properties":{"applications":{"type":"integer","format":"int64","description":"Applications recorded that day, on the row's own `created_at`, which is\nevery application whether it was sent through this service or logged\nafter the fact. The same population [`Metrics::applications`] counts, cut\nby day."},"day":{"type":"string","format":"date","description":"The day, in UTC. Whole days rather than timestamps: this is a bar on a\nchart, and the hour it was computed at is [`Trends::generated_at`]'s\nbusiness.","example":"2026-09-10"},"signups":{"type":"integer","format":"int64","description":"Accounts created that day. Every account, whatever its status now — a\nsuspended account still signed up, and a series that revised the past\neach time somebody was suspended would not be a series."}}},"Trends":{"type":"object","description":"Signups and applications per day, for the last [`TREND_DAYS`] days.\n\nA sibling of [`Metrics`] rather than a field on it, and the reason is what\neach is: `GET /admin/metrics` is one statement taken so the counters agree\nwith each other, and folding a thirty-row series into that response would\nmake every console load pay for a chart most of them do not draw. The two\nroutes are read together and neither needs the other's consistency.\n\n**No new table and nothing tracked.** Every figure is `date_trunc` over\n`users.created_at` and `application.created_at`, which are columns this\nservice has always written because a row has to know when it was made. There\nis no event log behind this, no analytics vendor, no request to anybody, and\nnothing here is about an individual: a day and two counts is the whole of it,\nand the smallest interesting deployment makes those numbers unattributable in\nthe only sense that matters, which is that the route never had a name to\ndisclose.","required":["days","series","generated_at"],"properties":{"days":{"type":"integer","format":"int64","description":"The length of the window, so a client renders \"the last 30 days\" from\nthe answer rather than from a constant of its own.","example":30},"generated_at":{"type":"string","format":"date-time"},"series":{"type":"array","items":{"$ref":"#/components/schemas/TrendDay"},"description":"One row per day, oldest first, **including the days nothing happened**.\nA dense series is the point: a bar chart drawn from rows that exist only\nwhere there was activity silently rescales its own axis and turns a\nquiet fortnight into a busy one."}}},"UnreadCount":{"type":"object","description":"The badge: how many notices the caller has not read.","required":["unread"],"properties":{"unread":{"type":"integer","format":"int64","example":2}}},"UploadRequest":{"type":"object","description":"Request body for `POST /me/inbox`.\n\nThe same envelope a push carries, minus everything that would be a claim\nabout somebody else. There is no `signing_key_id` and no signature on this\ntype, so an item written through this route cannot carry an attribution even\nby accident — the field it would travel in does not exist.","required":["doc_type","schema","schema_version","byte_class","payload","payload_hash"],"properties":{"byte_class":{"type":"integer","format":"int32","example":1024},"connector":{"type":["string","null"],"description":"Which connector fetched it, from [`crate::connectors::CONNECTORS`], or\nabsent for a document the person put here themselves.\n\nThe one field that changes what the row is called. Present, and the item\nis [`ORIGIN_CONNECTOR`] and its provenance line says *pulled via this\nconnector on this date*; absent, and it is [`ORIGIN_SELF`] and says the\nperson uploaded it. **Neither of them acquires a sender**, because the\nconnector is the service that fetched the document rather than the\ninstitution that issued it, and there is no field on this type that could\ncarry the latter.\n\nChecked against the catalogue rather than stored as written: a\nprovenance line an operator can read has to name something this\ndeployment recognises, or a client could write *pulled via the IRS* on a\ndocument it invented. See [`crate::connectors::check_connector`].","example":"mock"},"doc_type":{"type":"string","example":"tenancy_reference"},"payload":{"type":"string","description":"Sealed to the caller's own `box_pub`, by the caller's own client, under\nthe same construction a push uses. Exactly `byte_class + 48` bytes.","example":"base64 of byte_class + 48 bytes"},"payload_hash":{"type":"string","example":"base64 of 32 bytes"},"schema":{"type":"string","example":"generic"},"schema_version":{"type":"string","example":"1"}}},"Usage":{"type":"object","description":"What an institution has run up since its last invoice.\n\n\"This period\" is defined as *everything the next invoice will contain* —\nwhich is to say the unbilled rows — rather than as a calendar month. That\nmakes the number on this screen the number on the next bill, which is the\nonly definition a customer can check.","required":["pull_throughs","advances","full_reads","previews","free_pulls","amount_cents","monthly_fee_cents","total_cents"],"properties":{"advances":{"type":"integer","format":"int64","description":"Of those, the ones where the institution decided to take a candidate in.","example":30},"amount_cents":{"type":"integer","format":"int64","description":"What the metered acts in the period come to at the plan's current price:\nthe billable ones beyond the included allowance. Zero on `free` and\n`contract`; see [`Billing::unit_price`] and [`BILLABLE_KINDS`].","example":42000},"free_pulls":{"type":"integer","format":"int64","description":"Of the billable ones, how many the plan's included allowance covers — so\nhow many of them cost nothing.\n\nSee [`free_pulls`]: the allowance is counted over the institution's whole\nhistory, so this is whatever is left of it after every earlier invoice,\ncapped at what this period actually contains. It is reported rather than\nimplied because it is the difference between the count above and the\namount below, and a customer checking their bill should not have to infer\na subtraction.","example":0},"full_reads":{"type":"integer","format":"int64","description":"Of those, the ones where a full disclosure was actually handed over.","example":12},"monthly_fee_cents":{"type":"integer","format":"int64","description":"The plan's standing fee, which the next invoice carries beside those\nacts. Zero on every plan but `platform`; see [`Billing::monthly_fee`].\n\nReported separately rather than folded into the number above, because\nthey answer different questions — \"what did our usage cost?\" and \"what do\nwe pay to be here?\" — and an institution comparing plans needs to see\nboth. [`Usage::total_cents`] is the sum, which is the bill.","example":20000},"previews":{"type":"integer","format":"int64","description":"Of those, the ones where a candidate's preview bundle was read for the\nfirst time. **Free on every plan** — see [`KIND_PREVIEW_READ`] — and here\nbecause triage is most of what an institution does and a usage screen\nthat omitted it would describe a quieter customer than the real one.","example":130},"pull_throughs":{"type":"integer","format":"int64","description":"Metered acts in the period, every kind — including the previews, which\ncome to nothing. The number to read as \"what will be charged\" is\n`amount_cents`, not this one.","example":42},"since":{"type":["string","null"],"format":"date-time","description":"The oldest act in the period, which is where the next invoice's window\nwill start. `null` when there is nothing unbilled."},"total_cents":{"type":"integer","format":"int64","description":"What the next invoice will come to: the metered acts plus the standing\nfee.","example":62000}}},"UserMetrics":{"type":"object","description":"The account counters. `active`, `suspended` and `unclaimed` partition\n`total`: `users_status_check` admits those three values and no others, so a\nblock that counted two of them would be a total nobody could reconcile with\n`SELECT status, count(*) FROM users` — the same argument\n[`JobQueueMetrics`] makes about `cancelled`.\n\n`admins` is deliberately *not* part of that partition. It counts a different\naxis — `is_admin`, which is orthogonal to status — and every administrator is\nalso counted in `active`.","required":["total","active","suspended","unclaimed","admins"],"properties":{"active":{"type":"integer","format":"int64"},"admins":{"type":"integer","format":"int64","description":"Platform administrators — the accounts that can reach these routes."},"suspended":{"type":"integer","format":"int64"},"total":{"type":"integer","format":"int64"},"unclaimed":{"type":"integer","format":"int64","description":"Accounts an agent created for somebody who has not claimed them. They\nhold a sealed record and no keyring, cannot be signed in to, and disclose\nnothing until their owner follows a claim link. See [`crate::claim`]."}}},"VeraIdSummary":{"type":"object","description":"One Vera ID as its owner reads it back. Never the id itself.","required":["id","kind","created_at","use_count"],"properties":{"capability_id":{"type":["string","null"],"format":"uuid","description":"The capability behind an auto mode id. `null` in review mode, which is\nthe whole difference: a review mode id authorises no key material at all."},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"`review` or `auto`.","example":"review"},"label":{"type":["string","null"]},"last_used_at":{"type":["string","null"],"format":"date-time","description":"When an agent last presented it, or `null` for one nobody has used."},"revoked_at":{"type":["string","null"],"format":"date-time"},"use_count":{"type":"integer","format":"int64"}}},"Verification":{"type":"object","description":"One verification, as returned to its owner.","required":["id","kind","status","detail","created_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"detail":{"type":"object","description":"Whatever else the checker reported. Never the evidence itself."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the check stops meaning anything. Filled from a vendor's payload;\nnothing in this build writes it."},"id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/Kind"},"provider":{"type":["string","null"],"description":"Who is doing the checking. Absent only on a row that named nobody.","example":"manual"},"reference":{"type":["string","null"],"description":"The checker's own identifier for this check, when they gave one."},"review_reason":{"type":["string","null"],"description":"Why a platform operator rejected this check, when one did.\n\nWritten by `POST /admin/verifications/{id}/reject` — see [`crate::review`]\n— and carried here because the person whose claim was refused is the one\nentitled to read the grounds for it. Who decided is deliberately *not*\nhere: that is in the operator's own trail, and an operator's user id is\nnot something the subject of a decision needs."},"status":{"$ref":"#/components/schemas/Status"},"verified_at":{"type":["string","null"],"format":"date-time","description":"Present only on a `verified` row, and set when it became one."}}},"VerificationMetrics":{"type":"object","required":["pending","verified","failed","expired"],"properties":{"expired":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64"},"pending":{"type":"integer","format":"int64"},"verified":{"type":"integer","format":"int64"}}},"VerifiedEmail":{"type":"object","description":"What `POST /auth/verify-email` answers with.","required":["email","verified_at"],"properties":{"email":{"type":"string","description":"The address that has just been proved. Carried so the app can say which\none, which matters on a browser that is not signed in — following a link\nfrom a mail client usually is one. It discloses nothing: whoever holds\nthe token was reading the mailbox it was sent to.","example":"ada@example.com"},"verified_at":{"type":"string","format":"date-time","description":"The moment it was proved. The first verification's timestamp, so a\nreplayed link would not backdate or advance it — though one cannot,\nbecause a token is spent when it is used."}}},"VerifyCredentialRequest":{"type":"object","description":"Request body for `POST /credentials/verify`.","required":["credential"],"properties":{"credential":{"type":"string","description":"The credential as it appeared in a shared profile.","example":"cred.v1.<payload-base64url>.<signature-base64url>"}}},"VerifyEmailRequest":{"type":"object","description":"Request body for `POST /auth/verify-email` and `POST /auth/reset`: the token\nout of the link.\n\nNo `Debug`, like every other request type in this module: for the length of\nits life this token *is* a credential, and a `{:?}` on the way to a log would\nput a live password reset in it.","required":["token"],"properties":{"token":{"type":"string","description":"The value of the `verify` query parameter in the emailed link.","example":"<43-character opaque token>"}}},"VerifyRegistrationRequest":{"type":"object","description":"Request body for `POST /admin/relying-parties/{id}/verify`.","properties":{"note":{"type":["string","null"],"description":"What the reviewer checked, in a sentence. Optional, and read by the\ninstitution as well as by the next operator.\n\nOptional because an approval that went as expected has nothing to say,\nunlike a refusal, which must always say why.","example":"Company number checked against the register and the domain matches."}}},"VerifyRequest":{"type":"object","description":"Request body for `POST /grants/verify`.","required":["token"],"properties":{"token":{"type":"string","description":"The grant token as handed to the agent by the user.","example":"v1.<payload-base64url>.<signature-base64url>"}}},"WatchCheck":{"type":"object","description":"One check: what was asked, what the answer was, and enough detail to act on\nwithout opening a terminal.\n\n`detail` is the watcher's own sentence — \"12% used of 40G\", \"expires in 63\ndays\" — and is written for a person reading a console, not parsed by\nanything. Nothing in it names a user: every check here is about a process, a\ndisk or a queue depth, and the one that touches user data counts rows\nwithout reading any.","required":["name","status","detail"],"properties":{"detail":{"type":"string"},"name":{"type":"string","description":"The check's short name, stable across runs so it can be looked up in\n`deploy/verigrant-watch.sh`."},"status":{"$ref":"#/components/schemas/CheckOutcome"}}},"WatchState":{"type":"string","description":"The headline of a watch result.","enum":["ok","alert","unreadable"]},"WatchStatus":{"type":"object","description":"What the watcher last found, as `GET /admin/metrics` reports it.\n\n[`WatchStatus::state`] is derived here from the checks rather than read from\nthe file, so the headline and the list it summarises cannot disagree: there\nis no way for this to say `ok` above a list containing an `alert`, because\nthe word is computed from the list every time it is served.","required":["path","state","checks"],"properties":{"checked_at":{"type":["string","null"],"format":"date-time","description":"When the run that wrote the file finished. `null` only when the file\ncould not be parsed, in which case nothing in it is trustworthy\nincluding its clock.\n\nA `checked_at` that has stopped moving is its own alert and is not one\nthis can raise: the reading is five minutes old or five days old and the\nfile says the same thing either way. The console shows the age; the\ntimer failing to run is what `systemctl list-timers` answers."},"checks":{"type":"array","items":{"$ref":"#/components/schemas/WatchCheck"},"description":"One entry per check, in the order the watcher ran them. Empty only when\nthe file could not be used."},"path":{"type":"string","description":"The file this came from — `VERIGRANT_WATCH_FILE`, or the default.\n\nReported rather than assumed, because the first question about a\nmonitoring result that looks wrong is which file it was read out of, and\nthe second is whether that is the file the timer writes."},"problem":{"type":["string","null"],"description":"Why the file could not be used, when it could not. `null` otherwise."},"state":{"$ref":"#/components/schemas/WatchState","description":"`ok`, `alert`, or `unreadable`."}}},"WebhookEvent":{"type":"object","description":"One vendor event, in the shape this service accepts it.\n\nA normalised envelope rather than any vendor's native format, and that is a\ndeliberate choice about where the translation lives. No vendor here is\nintegrated yet — all four are `501` at\n[`POST /me/verifications`](crate::verifications::create) — so there is no\nreal payload to parse, and inventing four speculative parsers from four sets\nof API docs would be four guesses to maintain and re-check the day any of\nthem actually lands. When one does, its adapter converts its native body\ninto this, next to the client code that knows that vendor; the queue, the\nrow and this handler stay as they are.","required":["reference","status"],"properties":{"expires_at":{"type":["string","null"],"format":"date-time","description":"When the check stops meaning anything, if the vendor said. Optional, and\nthe only field here that writes something a user's record did not\nalready have a way to hold."},"reference":{"type":"string","description":"The vendor's own identifier for the check, matched against the\n`reference` column recorded when it was started. This is the whole basis\non which a delivery finds a row: a webhook names a check, never a user.","example":"inq_9RtQm2wG"},"status":{"type":"string","description":"`verified`, `failed` or `expired` — the same terminal set\n`POST /me/verifications/{id}/complete` accepts, and for the same reason:\na callback that moved a check back to `pending` would be a vendor\nun-completing something.","example":"verified"}}},"WorkEligibility":{"type":"object","description":"The eligibility singleton: may this person work here, and on what footing.","required":["updated_at"],"properties":{"auth_type":{"type":["string","null"],"description":"Free text: the work-authorisation categories are numerous,\njurisdiction-specific and revised by agencies, so a closed set here would\neventually refuse a true answer.","example":"H-1B"},"authorized_us":{"type":["boolean","null"]},"citizenship_status":{"type":["string","null"],"description":"One of `us_citizen`, `us_national`, `permanent_resident`,\n`authorized_alien`, `other`."},"clearance_level":{"type":["string","null"],"description":"One of `none`, `public_trust`, `confidential`, `secret`, `top_secret`,\n`ts_sci`."},"clearance_status":{"type":["string","null"],"description":"One of `none`, `active`, `current`, `expired`, `in_process`, `interim`."},"is_over_18":{"type":["boolean","null"],"description":"Stored as the answer, not derived from\n[`SensitiveId::date_of_birth`] — an employer entitled to \"yes\" is not\nthereby entitled to the birth date."},"requires_sponsorship":{"type":["boolean","null"]},"updated_at":{"type":"string","format":"date-time"},"veterans_preference":{"type":["string","null"],"description":"Free text: federal preference categories are named differently by\ndifferent agencies."}}}},"securitySchemes":{"agent_signature":{"type":"apiKey","in":"header","name":"X-Verigrant-Agent-Signature","description":"An Ed25519 signature, base64, over\n\n```\nverigrant/v2/agent-request\nMETHOD\npath?query\ntimestamp\nbase64(sha256(body))\n```\n\nwhere `timestamp` is the RFC 3339 value of `X-Verigrant-Agent-Timestamp` and must be within five minutes of this service's clock. Verified against the `agent_sign_pub` named in the capability statement the **user** signed, which is the second link of the chain: the user's key authorises the agent's key, and the agent's key signs each issuance. The user's key never signs an individual auto-issued grant, which is what makes one attributable."},"grant":{"type":"http","scheme":"bearer","description":"A grant token from `POST /grants`, sent as `Authorization: Bearer <token>` by the third party the grant was issued to. This is not a session and is accepted only where it is named: presenting one to a `/me/*` route is a 401. Every use re-reads the grant row, so revocation takes effect at once.\n\n**A grant token is no longer sufficient on its own to read a record.** `GET /shared/profile` and `GET /shared/documents/{id}/content` also require the `relying_party` credential below. See `docs/product-model.md`: only a registered institution may redeem a Vera ID, so a leaked token presented by anybody else yields nothing."},"relying_party":{"type":"apiKey","in":"header","name":"X-Verigrant-Relying-Party","description":"The credential of a **registered institution**, presented alongside the grant token and never instead of it. Two schemes, and the scheme word is required:\n\n- `Key vgrp_<token>` — an API key from `POST /orgs/{id}/relying-party/api-keys`. Authenticates the institution rather than any person in it, which is what an ATS pulling a candidate overnight actually holds.\n- `Session <org-slug> <session-token>` — a member session, with the organization named because a person may belong to several. Membership is proved the same way every employer route proves it.\n\nEither way the organization must have a relying-party registration whose status is `registered`. A pending or suspended institution, an unregistered one, a revoked key and a missing header all answer the same `403 relying_party_required`, so the refusal cannot be used to enumerate who is registered."},"session":{"type":"http","scheme":"bearer","description":"A session token from `POST /auth/login`, sent as `Authorization: Bearer <token>`. Revoked immediately by `POST /auth/logout` — the session is a row, not a signed claim, so a logout binds on the very next request."},"vera_id":{"type":"apiKey","in":"header","name":"X-Verigrant-Vera-Id","description":"**A Vera ID**, as its owner handed it to an agent: the string `POST /me/vera-ids` returned once, or the one `POST /me/capabilities` returned once when the user paired this agent.\n\nIn **review mode** a Vera ID authorises exactly one thing — `POST /vera/applications`, which writes a pending row in its owner's approval queue and discloses nothing. Every disclosure under it is sealed later, by the owner's own client, after a human looked at it and chose how long it lasts.\n\nIn **auto mode** a Vera ID additionally reaches `/agent/*`, where it auto-issues grants inside the preset its owner signed, including the lifetime that preset sets — and it is not sufficient on its own there: those routes also require the `agent_signature` below. A leaked auto mode Vera ID with no signing key issues nothing."}}},"tags":[{"name":"system","description":"Liveness."},{"name":"relying-party","description":"The institution on the other end of a disclosure. Registering one, the public keys a seeker's client seals to it, the transparency-log and domain-attestation evidence that let a client refuse a key this operator substituted, and the credential an institution authenticates with. Reading any shared record now requires one of these credentials alongside the grant: see the `relying_party` security scheme, and `docs/product-model.md`'s relying-party gate."},{"name":"users","description":"Account creation and public lookup."},{"name":"auth","description":"Sessions: log in, log out, identify — and, under `/me/sessions`, see every session this account holds and end any of them. A session lasts thirty days, so the ability to withdraw one you are not sitting in front of is part of holding the account rather than an operator's favour. No route here ever answers with a session token or its hash."},{"name":"profile","description":"The custodied record. `/me/*` is the owner editing their own; `/shared/profile` is a third party reading it through a grant."},{"name":"contact","description":"Phone numbers, links and addresses — the repeatable contact detail an application form asks for. Read back through `GET /shared/profile` by a delegate, with the rest of the sections this service can still read."},{"name":"background","description":"Languages, and the people who will vouch for the user. References never leave through a grant in this phase: they are somebody else's contact details."},{"name":"credentials","description":"Licences and certifications, the 1:1 CDL detail that turns one of them into a commercial driving licence, and the equipment a driver has operated. Credentials reach a delegate only through a grant that carries `credentials:read`; driving equipment travels with the rest of the history."},{"name":"availability","description":"What the user is looking for, and which days they can work. The preferences are a singleton — `PUT` replaces, `PATCH` merges — and the weekly grid is keyed by day."},{"name":"regulated","description":"The regulated islands: work eligibility, the sensitive identifiers (date of birth and SSN last four — never a full number), the voluntary EEO self-identification, and the consent trail. None of them is part of the profile document, each is optional, and each reaches a delegate only through a grant carrying its own scope — `eligibility:read`, `identity:read`, `eeo:read`, `consents:read`. EEO is never an input to matching, search or ranking. Every regulated disclosure through `GET /shared/profile` is written to the owner's audit trail."},{"name":"answers","description":"The answer bank: the per-posting screening questions a canonical profile cannot cover, stored once and reused. A question is identified by a fingerprint of its normalised text, so the same question asked by two employers — spelled differently — is one row. `GET /me/answers/lookup?question=…` is the reuse path."},{"name":"autofill","description":"The engine that spends the record on an actual form. `POST /me/autofill` takes the questions a page is asking and answers each one from the canonical profile, from the answer bank, or not at all — a gap, which is the question to put to the user. `POST /me/autofill/learn` stores what they say, so the next form to ask it resolves from the bank. Two of the regulated islands are never read here: no rule maps the date of birth, the SSN last four or the EEO self-identification, whatever a form asks."},{"name":"applications","description":"What was applied for: the record of where this person sent an application and where it got to, and the act of applying to a posting this service holds. `POST /postings/{id}/apply` mints a grant for exactly the scopes the posting asked for, runs the auto-fill engine over its screening questions, writes the applicant's row and tells the employer — all on one transaction. The employer then reads the applicant's profile through that grant at `GET /orgs/{id}/applications/{application_id}/profile`, and the applicant can withdraw it at any time with `DELETE /me/grants/{id}`. The application record itself is session-only and no grant scope delegates it."},{"name":"documents","description":"The files an application attaches. Real bytes, stored by this service: uploaded as `multipart/form-data`, at most 10 MiB, and only PDF, PNG, JPEG or `.docx` — checked against the file's own leading bytes and not merely against what the upload declared. Content is served by exactly two routes, one to the owner through a session and one to a delegate through a grant carrying `documents:read`, and a delegated download is written to the owner's audit trail."},{"name":"grants","description":"Scoped, revocable delegations. The token is returned once, at creation, and never stored — only its signature could reconstruct it, and we keep no copy."},{"name":"llm-key","description":"The BYO-key vault. No endpoint here or anywhere returns a stored key."},{"name":"verifications","description":"Checks on a user's claims, run by somebody other than the user. The summary they produce travels inside the profile document, so a delegate reading through a grant sees it too."},{"name":"portability","description":"Taking the record out, and destroying it. `GET /me/export` returns everything this service holds about the caller in one document — every table, named in the bundle's `tables` field — without session token hashes and without the stored provider key. `DELETE /me/account` destroys all of it, along with every session and grant that reached it, and takes the account password in the body as confirmation. What survives an erasure is one row in `erasure_log` carrying no name and no address."},{"name":"orgs","description":"The employer's side. An organization, and the people who may act for it — where an employer is a regular user who belongs to one, on the same account and the same session as any other. Membership is not access to anybody's record: reading a profile still takes a grant its owner issued. Three ranked roles — owner > admin > recruiter — gate the roster. `POST /orgs/{id}/members` adds an *existing* account by the address it registered with rather than mailing an invitation to an unproven one, which is a lookup and not an invitation.\n\n**These are the only two routes in this service that require a confirmed email address.** `POST /orgs` and `POST /orgs/{id}/members` answer `403 email_unverified` until the caller has opened the link sent to them, and the account being added has to have opened its own. An institution is the party other people's records are disclosed to, and an account behind an address nobody has answered is the cheapest way onto that side of the market. Nothing on the seeker's side is gated: a person with an unconfirmed address keeps their whole record, every route over it, every grant they issue and every application they send."},{"name":"postings","description":"Job postings. `/orgs/{id}/postings` is the employer writing them — any member may, recruiters included, because the rank guards the roster rather than the work — and `/postings` is the public half: open postings only, no credential of any kind, and `GET /postings/{id}` carries a `json_ld` object (schema.org `JobPosting`, as Google for Jobs reads it) whose every string is HTML-escaped so it is safe inside a `<script>` element. A posting's `required_scopes` is what applying will *ask* the candidate to delegate and grants nothing by itself, and its `screening_questions` are in exactly the shape `POST /me/autofill` takes."},{"name":"agent","description":"Drafting, and the loop around it. `POST /me/agent/draft-application` is the owner drafting one application from their own profile; `/shared/agent/*` is a delegate doing it through a grant that carries `applications:submit`. `POST /me/agent/run` is the agent deciding for itself: it queues a job that takes the caller's top matches, drafts for each and resolves each posting's screening questions through the auto-fill engine, leaving at `GET /me/agent/suggestions` a draft and — the point of the whole loop — only the questions this service genuinely could not answer from the record. Answering one at `POST /me/agent/suggestions/{id}/answers` stores it in the answer bank, so the next posting to ask it is answered without asking anybody. `POST /me/agent/suggestions/{id}/submit` is the last step, and the one with a permission in front of it: sending a suggestion needs an active grant carrying `applications:submit`, standing or minted for that one act, and without one the suggestion stays exactly where it was.\n\nWho pays for the model is `VERIGRANT_LLM_MODE`, and `GET /agent/mode` reports it without a credential so a client never has to guess: `mock` calls nobody, `live` spends each caller's own custodied key, `claude-cli` runs the operator's local CLI, and `hosted` spends the platform's own credential — metered per person against a daily cap, answered as `429 draft_quota` when it is spent, and readable at `GET /me/agent/quota`. A caller who has stored a key of their own spends it and is not metered, in hosted mode as in live."},{"name":"access","description":"The employer asking a candidate for a delegation rather than assuming one. A request is not access: it creates a `pending` row and a notice, and nothing else, until the seeker approves it — at which point an ordinary grant is minted, listed in `GET /me/grants` and revocable there. An employer can address only somebody who has already applied to them, because nothing in this service hands a company a user id. Declining and ignoring look the same from the outside, deliberately."},{"name":"notifications","description":"What the service tells a person about work that outlived the request which started it. A notice is a row held here and read over an authenticated route, and that row is the whole of it — there is no route that creates one, they are written by the `notify` job on the worker, and no account can put a row in another's list.\n\nA notice is **also** mailed when three things are true: the row was written, the account's address is confirmed, and `users.notification_email` is on. `PATCH /me/settings` is the one switch, and what crosses is the notice's own title, its body and a link to the app — no field of the record underneath it, no deep link, no tracking pixel and no wrapped URL. A deployment with no `VERIGRANT_SMTP_HOST` sends nothing and logs what it would have sent. The list is unread first, then newest first; marking read is idempotent and keeps the first timestamp. The kinds a client can expect: `match.new`, `profile.viewed` — somebody used a delegation to read the record, debounced so one sitting is one notice — `application.received`, `application.submitted`, `application.stage_changed`, `access.requested`, `access.approved`, `access.declined`, `agent.suggestions` and `verification.completed`."},{"name":"admin","description":"The platform administrator: the one privilege in this service that runs vertically rather than across one person's record. `users.is_admin` is what these routes require, it is established at boot from `VERIGRANT_ADMIN_EMAIL`, and it has nothing to do with an organization's `admin` role — an employer's admin governs a roster and a platform admin governs accounts. What that means concretely: search and read the account directory, suspend an account and put it back, read the custody trail across every user, read the service's own counters, and work the verification queue. **No route here returns a field of anybody's career record** — no profile, no document, no answer, no draft — and that is the boundary, not a gap: an operator who needs to read a record asks its owner for a grant, and the read lands in that person's trail like every other. Suspending is immediate in both directions: the account cannot log in, and its live sessions stop working on their very next request. `/admin/verifications` is the one thing here that is not about an account: this build's only checker is the manual one, so somebody on this side has to decide each check, and the queue is where they do it. It discloses the *check* — kind, provider, the checker's reference and notes, the subject's id — and never the evidence behind it, which by design never enters this service. Approving mints a signed `cred.v1` credential from the same key and the same code path `GET /shared/profile` uses; rejecting records a reason that the person checked reads in their own `GET /me/verifications`. `/admin/billing` and `PATCH /admin/orgs/{id}/billing` are the same shape one level up: they report *organizations* and counts, never a field of a record, and putting an institution on a plan writes an `admin.billing.changed` trail naming every field it moved. `/admin/jobs` is the third thing that is about neither an account nor a company: the job queue is the worker's own table, so the three routes on it report the *work* — kind, status, attempts, timings, the last error — and never a payload, which is the one field of a job row that names people. Requeuing a failed job gives it a fresh budget of attempts and wakes a worker; cancelling moves a queued or failed job to a terminal `cancelled`. Both are audited, as `admin.job.requeued` and `admin.job.cancelled`."},{"name":"billing","description":"What an institution owes. **Seekers and AI agents are never charged, and there is no route here they can reach**: the job board, the postings, applying, the preview an institution triages on and the profile it reads through a grant are all free, on every plan. What is *billed* is one thing — an institution taking an application into its own system, which `docs/product-model.md` calls a pull-through — and it happens at exactly two moments: advancing a candidate (`PATCH /orgs/{id}/applications/{id}` to `interview`, `offer` or `hired`, or `POST /shared/advance`) and taking the full disclosure (`GET /shared/records?kind=full`). A third act is **metered and priced at nothing**: the first time an institution opens a candidate's preview bundle, at `GET /shared/records?kind=preview` or `GET /orgs/{id}/applications/{application_id}/records`, writes a `preview_read` row so that triage is visible on both sides' usage screens. It is free on every plan and appears on an invoice as a line charged at zero. Exactly one row per application per kind, enforced by a unique index, so a retried request and a re-read bundle are never billed twice.\n\nThe reading routes are **owner only**, which is stricter than every other employer route: what the company pays is a fact about the company, and a recruiter's job is candidates. `POST /orgs/{id}/billing/card` is the one exception, at owner **or admin**, because adding a payment method is an errand and the plan is not. `GET /orgs/{id}/billing/usage` serves the metered acts themselves rather than a summary, so a customer can check the bill against the same rows it was drawn from.\n\n**A card comes before the first pull-through**, on every plan but `contract` and including the free one: an institution with no payment method on file is refused `402 card_required`, before the allowance is even consulted. It is a card saved rather than charged — a bill that comes to nothing is never presented. `POST /orgs/{id}/billing/card` answers with a URL to send the browser to, and the card is on file once the gateway's signed event has been applied, which is *after* the redirect — poll `GET /orgs/{id}/billing` for `has_payment_method`.\n\nThere are four plans. `free` includes ten pull-throughs, priced at nothing and recorded like everybody else's; the eleventh answers `402 upgrade_required`, and nothing else changes — receiving applications, working the queue and reading previews stay free and unlimited at every count. `per_pull` includes ten and then charges a price per pull-through, `platform` is a monthly fee with forty included and a lower price per pull-through after them on the same invoice, and `contract` is a flat annual invoiced outside this meter with unlimited pulls, which is where higher-ed and multi-site institutions sit. The included pulls are a deduction spent once over an institution's lifetime rather than an allowance that refills. A pull-through is priced **per candidate ingested and never per field**, on every plan, so a disclosure carrying eight scopes costs exactly what one carrying two costs and there is no volume bracket anywhere.\n\nAn organization nobody has priced is on the `free` plan and `active` — that is the answer, not a `404`. An organization whose status is `suspended` keeps everything it already has and cannot take anything new in: advancing and the full disclosure answer `403 billing_suspended`, and nothing else changes. `past_due` gates nothing. Neither refusal reaches an act already recorded: a bundle an institution has already pulled stays readable, and a candidate it has already advanced can still be moved.\n\nA monthly `billing.invoice` sweep draws the unbilled meter into one invoice per institution. On a deployment with no payment gateway configured it leaves that invoice at `draft`, which is the honest status for a bill presented to nobody. On one configured with Stripe it charges the saved card for the computed total with an off-session PaymentIntent, leaves the bill at `open`, and marks it `paid` only when Stripe's signed `payment_intent.succeeded` arrives at `POST /stripe/webhook` — reached from the internet at `/api/stripe/webhook`."}]}