Filling slots in-app

Filling slots in-app

A pending slot does not have to send the user to Geena: with the user present in your app you can render a native picker over what their vault already holds, let their tap grant an existing item, or create the asked-for value directly into their vault — granted to the slot in the same act.

Every route on this page requires the slot’s fill verb: the permission to put data into a slot is what buys the right to see what could go in it.

Slots that ask about another party (they carry subject in /status) take the same three routes with a participant — the alias of the family member or company the act is for, from the subject’s candidates. It travels as the participant query parameter or as a participant field in the body (a JSON property, or a multipart part); the query parameter wins when both are present. A subject slot without one answers 422 participant_required; a participant on a recipient slot answers 422 participant_not_allowed. See Subjects: relatives & companies.

Three bounds hold everywhere on this surface:

  • Slot-scoped. Enumeration exists only per slot of an active connection, only for what the accepted manifest asked. There is no vault-wide listing.
  • Content rides the listing (documents). A document candidate carries its current data when the user’s key context can open it — so your picker can show which email, and prefill a one-value-per-person form. Whatever cannot be decrypted degrades to a metadata-only row, so listing still works against a sealed vault. Files stay metadata-only: their content flows exclusively through a grant and the serving routes.
  • Everything receipted. Listing writes a listed receipt (coalesced per slot per 15 minutes), a pick writes attached, a create writes created — all attributed to your app.

1. List candidates

GET /partner/v1/requests/{requestId}/slots/{slotId}/candidates
GET /partner/v1/requests/{requestId}/slots/{slotId}/candidates?participant=c0ffee12-…
{
  "slotId": "7c2b9e11-30cf-4f2e-9f57-b8a4f7f4f2ad",
  "kind": "schema",
  "target": "PersonAddress",
  "candidates": [
    {
      "resourceId": "b7d0…",
      "type": "document",
      "name": "Home address",
      "data": { "streetAddress": "Keizersgracht 1", "city": "Amsterdam" },
      "version": 4,
      "createdAt": "2026-05-02T10:00:00Z",
      "updatedAt": "2026-07-12T08:30:00Z",
      "granted": false
    }
  ]
}

granted: true marks the candidate currently backing the slot — render it as the active pick. A row without data could not be decrypted (sealed vault) — render it by name and let the pick still work. With a participant, the listing enumerates that party’s vault: the family member’s documents, or the company’s records.

2. Attach the user’s pick

POST /partner/v1/requests/{requestId}/slots/{slotId}/attach
Content-Type: application/json

{ "resourceId": "b7d0…" }
{ "resourceId": "44e1…", "participant": "c0ffee12-…" }

The user’s tap in your UI is the approval — make the tap unmistakably an act of sharing. The grant is minted with your app recorded as its source; the call is idempotent (alreadyGranted: true on a repeat) and any resource that is not the named vault’s own slot-matching item answers 404 not_found. The alias only says who you meant: the resource itself decides which vault is checked, so naming one participant’s alias with another participant’s resource answers 404 participant_unknown.

Cardinality: on a single slot (multiple: false, the default) attaching a different resource replaces the standing grant — the second consent supersedes the first, so “switch to this” is one tap. Only a multiple: true slot accrues grants; on a subject slot the count is per participant. A concurrent fill of the same request answers 409 slot_conflict; re-list the candidates and let the user tap again.

Casting: a subject the manifest asks about once cannot be answered by two different participants, and one participant cannot play two subjects of one request. Both are checked before anything is written and re-decided inside the write — a fill that loses that race answers 409 participant_conflict, and nothing is granted.

{ "slotId": "7c2b9e11-…", "resourceId": "b7d0…", "granted": true }

3. Create-and-attach

POST /partner/v1/requests/{requestId}/slots/{slotId}

For a schema slot, send JSON — the payload is validated against the slot’s schema (its target names one of the built-in schemas), whatever you send:

{ "name": "Home address", "data": { "streetAddress": "Keizersgracht 1", "city": "Amsterdam" } }
{ "participant": "9a7f…", "data": { "legalName": "Acme B.V.", "registrationNumber": "12345678", "registrationCountry": "NL" } }

For a personal_files or entity_files slot, send multipart/form-data with a file part, an optional label, and — for a subject slot — a participant part. Uploads are capped per file (32 MiB by default); over the cap answers 413 too_large and writes nothing, and the stored mimeType is detected from the bytes, never taken from the part.

Create is always-create: it mints a fresh resource in the named vault — the user’s own, the family member’s, or the company’s — and grants it to the slot atomically. It never overwrites, never binds to existing data, and its outcome never depends on what the vault already holds. Requires the slot’s fill verb. On a single slot the fresh resource replaces the standing grant (the named participant’s only, for a subject slot); a concurrent fill answers 409 slot_conflict, a lost casting race 409 participant_conflict.

{ "slotId": "7c2b9e11-…", "resourceId": "44e1…", "version": 1, "granted": true }

Creates need a key context that can open the target vault; one that cannot be opened right now answers 423 sealed — see sealed keys. Creating into a company’s vault additionally needs the user’s live Manager key on that company (403 participant_ineligible otherwise).