Reading & writing data

Reading & writing data

Slot kinds map to two record types: schema slots serve structured documents, and personal_files and entity_files slots serve files (the person’s own, or a company’s). Reading a granted item needs no verb — that is what a connection is; writing and deleting need the slot’s edit verb (visible in /status). Every operation below also requires a grant; a slot the user has not granted answers 404 not_found, indistinguishable from one that does not exist.

Info

Every serve is written to the user’s receipt ledger before the response — reads included. If the receipt cannot be recorded, the call fails. The user sees each access, attributed to your app.

Read a granted slot

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

Data is decrypted from the vault at call time — Geena stores nothing for you and your access lasts exactly as long as the connection. The response carries one record per grant on the slot:

{
  "slotId": "7c2b9e11-30cf-4f2e-9f57-b8a4f7f4f2ad",
  "label": "Proof of address",
  "kind": "schema",
  "target": "PersonAddress",
  "records": [
    {
      "resourceId": "b7d0…",
      "participant": null,
      "type": "document",
      "version": 4,
      "name": "Home address",
      "data": { "streetAddress": "Keizersgracht 1", "city": "Amsterdam" }
    }
  ]
}

A file record instead carries label, fileName, mimeType, fileSize and a downloadUrl — file content never arrives inline. A file’s version tracks its bytes: renaming a file changes its label in place and leaves the version, and the downloadUrl it pins, alone.

Every record carries participant: null for the account holder’s own data, or { "subjectId", "type", "alias" } naming the party it belongs to — a family member (person) or a company (company). On a slot that asks about a subject, the response also repeats the ask as subject, and records holds one entry per party the user bound: three children means three records, each with its own participant block, telling you which name belongs to which child. The alias is the same pairwise handle /status lists under participants — see Subjects: relatives & companies.

Unavailable records

A record may arrive without type and without data:

{ "resourceId": "e41c…", "participant": { "subjectId": "investing-company", "type": "company", "alias": "9a7f…" },
  "status": "unavailable", "reason": "participant_ineligible" }

Read each record’s status before its type, and treat an unknown status as unavailable. The two reasons mean opposite things:

  • participant_ineligible is about the user: they can no longer speak for the party that answered — typically a company they were removed from or demoted in. The grant is untouched and copies your organization kept are unaffected; if they regain the role the record serves again with no re-consent. Act on it in your UI.
  • record_unreadable is about this server: the record could not be produced right now. It is not consent withdrawn — do not drop the party or ask for re-consent; retry later and report it if it persists. It is rendered per record so one bad record cannot hide the rest of a slot.

A record whose resource left the user’s own vaults is neither: it is simply absent from records, and a slot with nothing left to serve answers 404, exactly like one that was never granted.

Stream file content

GET /partner/v1/requests/{requestId}/slots/{slotId}/files/{fileId}

Exactly the downloadUrl a file record hands you. The file must be granted on that very slot; the response streams the decrypted bytes with the file’s own Content-Type and a Content-Disposition: attachment header. A loss of standing on a company’s file is an ordinary 404 here — a byte stream has no envelope to carry a reason; the record surface is where it is rendered.

Write a new version (documents only)

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

{ "data": { "streetAddress": "New street 5", "city": "Amsterdam" }, "name": "Home address" }
Warning

A write replaces the document’s data wholesale. Send the complete object, never a patch: read the current record, merge your change over it, and PUT the result — any field you omit is gone in the new version.

Delegated writes update, they never create: the slot must hold exactly one granted structured document, and your write becomes its next version — validated against the document’s own schema (see the built-in catalog), attributed to your app in the version history, receipted before the mutation. name is optional.

{
  "slotId": "7c2b9e11-30cf-4f2e-9f57-b8a4f7f4f2ad",
  "kind": "schema",
  "target": "PersonAddress",
  "resourceId": "b7d0…",
  "version": 5
}

Who may be written for follows the slot’s subject:

  • The account holder’s own slots — as above.
  • A company subject’s slots — the record lives in the company’s vault, and the write goes through while the user holds a live Manager key on that company, exactly the standing the fill and serve paths re-check. Once they do not, the write answers 409 participant_ineligible.
  • A person subject’s slots stay read-and-fill. A family member’s data is corrected first-party in Geena, never on a partner’s behalf: PUT and DELETE answer 403 subject_item_readonly. The fill-surface create is the one partner-side write into a family vault — always a new document, granted in the same act.

The stored version is then announced to every connection holding a grant on that record — including the one you wrote through. Your organization’s kept copy is a separate thing from the source you just changed, so your own request receives an update proposal, or keeps the version automatically, under the adoption policy its manifest set for the slot (see the Organization API).

Delete (documents only)

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

The user’s own hard delete, delegated: every version is removed, 204 on success, and the slot’s grant goes with it — /status reads pending again. Platform-protected documents (the one-per-vault starters, see the catalog) refuse with 403 delete_refused. Only the account holder’s own records are deleted this way: a company’s record answers 404 not_found here, and a family member’s 403 subject_item_readonly. The receipt ledger outlives the data — the deletion stays attributed to your app.

Sealed keys

Reads and writes need a key context that can open the record. Geena opens the user’s vault key whenever a call needs it, so a seal is rare: 423 sealed means the key could not be opened right now (retry, or ask the user to sign in), and 423 org_sealed means the grant’s key is also wrapped for your organization and a member of your organization signing in (to the dashboard, or through the organization-access ceremony) makes the record readable, with no action by the user. Both seals are slot-wide: one key context covers every record of the slot. See sealed keys.

Legacy addressing

GET|PUT|DELETE /partner/v1/requests/{requestId}/items/{kind}/{target} resolves a slot by its (kind, target) pair — e.g. items/schema/PersonAddress, with the target plain or base64url-encoded. It works only while the pair is unique in the manifest and answers 409 ambiguous_item once it is not, or 400 invalid_target when the target cannot be decoded. Prefer slot ids from /status; the pair form exists for early integrations.