Requests, records & kept copies

Requests, records & kept copies

List your requests

GET /org/v1/requests
GET /org/v1/requests?email=anna@example.com

Every request your organization holds — pending invitations it sent and the connections people accepted — newest first, optionally narrowed to one recipient email (normalized, exact). Metadata only, all of it your own, so the call writes no receipt. Requires request_read.

{
  "requests": [
    {
      "requestId": "6f9b2c9e-6a4e-4a44-9d5e-2f0f6f2a9b11",
      "manifestId": "b1af2d3c-9c1f-4f6e-8f2a-77aa01c2d9f0",
      "manifestName": "Investor onboarding",
      "manifestVersion": 3,
      "recipientEmail": "anna@example.com",
      "state": "active",
      "initiatedBy": "recipient",
      "initiatingClientId": "your-app",
      "invitationExpiresAt": "2026-10-01T00:00:00Z",
      "createdAt": "2026-09-01T10:12:00Z"
    }
  ]
}
  • state is one of pending, active, rejected_by_sender, rejected_by_recipient, expired, revoked, suspended, evaluated at call time on both clocks — the invitation window while pending, the accepted terms’ retention window once active — so a connection past retention reads expired.
  • initiatedBy is org (your team sent it from the dashboard) or recipient (a person opened it from your app’s hosted ceremony; initiatingClientId names the app).
  • manifestVersion is pinned at consent; re-publishing never widens a live connection.

One request in full

GET /org/v1/requests/{requestId}

The request, the manifest version it pinned and — per slot — whether the person granted it, plus one records entry per standing answer, the copies your organization kept, and the open update proposals. One call renders the organization’s view of a connection; the copies’ values are read separately below. Requires request_read.

{
  "request": { "requestId": "6f9b…", "state": "active", "…": "…" },
  "manifestName": "Investor onboarding",
  "items": [
    {
      "slotId": "7c2b9e11-30cf-4f2e-9f57-b8a4f7f4f2ad",
      "label": "Payout account",
      "group": "payout",
      "multiple": false,
      "kind": "schema",
      "target": "PersonBankAccount",
      "verbs": ["fill", "edit", "keep"],
      "adoption": "manual",
      "status": "granted",
      "records": [
        { "resourceId": "b7d0…", "participant": null, "sourceVersion": 4, "status": "available" }
      ]
    },
    {
      "slotId": "3b8f0c6a-9d2e-4e1b-b1c4-1f7a8e2d9c55",
      "label": "Company details",
      "group": "company",
      "subject": "investing-company",
      "multiple": false,
      "kind": "schema",
      "target": "LegalEntity",
      "verbs": ["fill", "edit", "keep"],
      "adoption": "auto",
      "status": "granted",
      "records": [
        { "resourceId": "e41c…",
          "participant": { "subjectId": "investing-company", "type": "legal_entity", "alias": "9a7f…" },
          "status": "unavailable", "reason": "participant_ineligible" }
      ]
    }
  ],
  "adoptions": [ { "adoptionId": "5d2a…", "slotId": "7c2b…", "sourceResourceId": "b7d0…",
                   "sourceVersion": 3, "orgResourceId": "c9e1…", "orgVersion": 1,
                   "resourceType": "document", "state": "materialized",
                   "adoptedAt": "2026-09-02T09:00:00Z", "materializedAt": "2026-09-02T09:00:00Z",
                   "participant": null } ],
  "proposals":  [ { "proposalId": "a1b2…", "slotId": "7c2b…", "resourceId": "b7d0…",
                    "newVersion": 4, "proposedAt": "2026-09-10T08:00:00Z", "participant": null } ]
}
  • status is granted or pending, per slot — any record granted makes the item granted, and pending stays indistinguishable from “the person has no such data” and from “they never will”. A slot every one of whose records is unavailable is still granted: the person said yes and has not taken it back.
  • records is where a slot answered by several parties becomes legible. Each entry names its participant — null for the recipient’s own item, otherwise { subjectId, type, alias } with type in the manifest’s own vocabulary (person or legal_entity; the partner plane says company for the same lane). The alias is pairwise: stable across every connection your organization has with that party, unrelated to any other organization’s handle, never a vault id — and the same handle the partner plane and the kept copies carry, so the three line up.
  • sourceVersion is where the person’s resource stands right now; compare it with the sourceVersion of the copy you kept to see whether you hold the current one. Absent on an unavailable record.
  • status: unavailable comes with a reason: connection_inactive (the connection itself is revoked or past retention — every record of it is unavailable), participant_ineligible (the recipient may no longer answer for that party: left the company, lost the Manager role, membership revoked), or source_unavailable (the resource no longer resolves). None of them deletes anything — the grant stands, and any copy already kept is untouched; standing regained makes a participant_ineligible record available again.
  • adoption is the keep slot’s policy: manual — your team keeps copies explicitly (below); auto — Geena keeps the first version and every later one as soon as they exist, whenever your organization’s key is warm, and queues the copy as pending until a member signs in.

Keep a copy

POST /org/v1/requests/{requestId}/slots/{slotId}/adopt
Content-Type: application/json

{ "participant": "9a7f…" }      # optional — no body keeps every record of the slot

Materializes a copy of the slot’s currently granted resource(s) into your organization’s vault — the manual “Keep” on a manual slot, or a first copy after the fact. Requires the slot’s keep verb and request_manage. Idempotent per (slot, source version): a version already kept is returned, never copied twice. Files are kept like records — the copy becomes a version of the one org file this (request, slot, source file) already has.

{
  "adoptions": [ { "adoptionId": "5d2a…", "sourceVersion": 4, "orgVersion": 2, "state": "materialized", "…": "…" } ],
  "skipped":   [ { "resourceId": "e41c…",
                   "participant": { "subjectId": "investing-company", "type": "legal_entity", "alias": "9a7f…" },
                   "reason": "participant_ineligible",
                   "message": "the recipient no longer manages this company" } ]
}

The keep is per participant. On a slot several parties answered, send {"participant": "<alias>"} — the alias from the slot’s own records — to keep just that party’s records; with no body every standing record is kept. A record the recipient may no longer answer for does not stop the others: it comes back in skipped with its reason, and the call still answers 200. Read skipped on every keep — a 200 whose adoptions is shorter than the slot’s records is not a failure to report, it is this. Only a keep that took nothing answers a typed refusal (403 participant_ineligible, 403 adoption_refused; an alias naming no granted record of this slot is the latter). Retrying is safe, and a retry after standing is regained adds only what was skipped.

A copy taken while your organization’s key was cold is state: "pending" with no orgVersion yet; retry the request’s pending copies once a member is in:

POST /org/v1/requests/{requestId}/adoptions/drain
{ "materialized": 2, "stillPending": 0 }

Read the copies you kept

GET /org/v1/requests/{requestId}/copies

Every materialized adoption of the request with its content — your own copy, opened with your organization’s key. Requires request_manage. Pending adoptions are listed on the request detail, not here.

{
  "copies": [
    {
      "adoption": { "adoptionId": "5d2a…", "slotId": "7c2b…", "sourceResourceId": "b7d0…",
                    "sourceVersion": 4, "orgResourceId": "c9e1…", "orgVersion": 2,
                    "resourceType": "document", "state": "materialized", "…": "…" },
      "resourceId": "c9e1…",
      "name": "Payout account",
      "schemaRef": "https://schema.identa.io/core/PersonBankAccount.json",
      "data": { "bankName": "Nya Banken", "accountNumber": "SE45…", "currency": "SEK" },
      "orgVersion": 2,
      "origin": "copy",
      "currentOrgVersion": 3,
      "currentOrigin": "correction",
      "currentData": { "bankName": "Nya Banken AB", "accountNumber": "SE45…", "currency": "SEK" },
      "participant": null
    }
  ]
}

Every copy of one person’s record under one request and slot is a version of one org document, interleaved with your own corrections. Read it as two layers:

  • data is the copy as kept — the version this adoption pinned (orgVersion, origin: copy).
  • currentData is the head of that chain — what your organization uses today: the same values unless a newer copy was kept or a member corrected it (currentOrgVersion, currentOrigin: copy | correction).

A consumer that wants what the organization uses today reads currentData of the newest adoption per slot — and, on a slot the manifest asks about a subject, per participant within it: two children answering one slot are two parties, two source records and two chains, and collapsing them by slot alone would show one child’s data under the other’s question. participant is snapshotted at keep time, so a copy of a child’s passport keeps saying “this child” after the grant is superseded or the connection revoked.

A kept file chains the same way — one org file per (request, slot, source file), one version per keep — but has no values: data is that version’s metadata (label, fileName, mimeType, fileSize), currentData is absent, and the bytes of each version come through that adoption’s own downloadUrl:

GET /org/v1/requests/{requestId}/copies/{adoptionId}/download

Streams the version this adoption kept (not the head) as an attachment, decrypted with your organization’s key. A document adoption answers 400 unsupported_item; a copy still pending answers 403 adoption_refused. To read a request’s files as files — kept copies beside your own attachments, head first — use Request files.

Correct a kept copy

POST /org/v1/requests/{requestId}/slots/{slotId}/correct
Content-Type: application/json

{
  "sourceResourceId": "b7d0…",
  "expectedSourceVersion": 4,
  "expectedOrgVersion": 2,
  "data": { "bankName": "Nya Banken AB", "accountNumber": "SE45…", "currency": "SEK" }
}

Your organization’s own revision of a copy it keeps — an address in the format you want, a name cased your way — appended as the next version of the copy’s chain, tagged correction, held beside what the person gave. It never changes the person’s data and never reaches them: data you believe wrong rather than badly shaped is the person’s to fix, through the partner plane’s edit verb or in Geena.

  • It corrects a copy that already exists; it never keeps. Keep first — correcting a record with no materialized copy answers 403 adoption_refused. Structured records only: a kept file is refused the same way. Requires the slot’s keep verb and request_manage.
  • Pinned to what the member saw. Take the three addressing fields from the newest materialized keep of the record on /copies (the one with the highest adoption.sourceVersion): sourceResourceId and expectedSourceVersion from its adoption — the person’s record, never the org copy’s own resourceId — and expectedOrgVersion from its currentOrgVersion, the head the correction appends to. A newer source version published since, or another member’s keep or correction, answers 409 version_conflict with nothing written: re-read /copies and re-apply the edit over what comes back. sourceResourceId may be omitted only when the slot holds exactly one granted record.
  • data is the complete corrected document, never a patch, validated against the copy’s own schema — a correction changes the values, never what the record is.
{ "adoption": { "adoptionId": "5d2a…", "orgVersion": 2, "…": "…" }, "orgVersion": 3 }

The adoption block is the record’s newest keep, unchanged — a correction creates no adoption, writes no receipt and changes no consent. orgVersion is the version the correction became: the new head /copies will report as currentOrgVersion with currentOrigin: correction, and the value to send as the next expectedOrgVersion to correct again without a re-read.

Recipe: keep your system in sync

ORG=https://api.test.geena.eu/org/v1
AUTH='Authorization: Bearer <member access token>'   # scope carries organization

# 1. the requests for one investor
curl -H "$AUTH" "$ORG/requests?email=anna@example.com"
# 2. what stands on each, and whether you hold the current version
curl -H "$AUTH" $ORG/requests/$REQ            # items[].records[].sourceVersion vs adoptions[].sourceVersion
# 3. keep what is new on manual slots (auto slots kept themselves); read skipped[]
curl -X POST -H "$AUTH" $ORG/requests/$REQ/slots/<slotId>/adopt
# 4. read the values you use today — newest adoption per slot, per participant
curl -H "$AUTH" $ORG/requests/$REQ/copies     # currentData of each
# 5. decide the open update proposals
curl -H "$AUTH" $ORG/requests/$REQ/proposals

Poll the request detail rather than the partner plane: it is your own material, unreceipted, and it tells you whose record moved.