Recipes

Recipes

Copy-paste sequences for the common jobs. Everything below assumes an active connection and a bearer token:

API=https://api.test.geena.eu/partner/v1
REQ=<request_id>          # from the token exchange, or GET $API/requests
AUTH='Authorization: Bearer <access token>'

All of these flows are implemented end-to-end in the public demo apps — github.com/Identa-io/demo is the reference integration.

Read everything the user granted

curl -H "$AUTH" $API/requests/$REQ/status
# for each item with status=granted:
curl -H "$AUTH" $API/requests/$REQ/slots/<slotId>

Serve-on-read means you always get the current version — if the user updates their address in Geena, your next read has it. Do not cache what you were not granted keep for. On a slot that asks about a subject, records holds one entry per party — group them by participant.alias, and read each record’s status before its type: an unavailable record is a party the user can no longer speak for, not a missing grant.

Fill a pending slot from the vault

With the user present in your app (the tap is the consent):

curl -H "$AUTH" $API/requests/$REQ/slots/<slotId>/candidates
# render the rows (name + current data), the user taps one:
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"resourceId":"<resourceId>"}' \
  $API/requests/$REQ/slots/<slotId>/attach

Create a value the vault doesn’t hold yet

Supply the naming vocabulary yourself (“Personal”, “Work”, “Mobile”) so the user’s vault stays navigable:

curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"name":"Work","data":{"email":"anna@firm.se"}}' \
  $API/requests/$REQ/slots/<slotId>

Create is always-create: a fresh document, granted in the same act — it never overwrites anything.

Set a one-value-per-person field

Some schemas hold one value per person (PersonFullName, PersonTaxStatus, …). Don’t make the user pick between instances — fill the value:

# 1. candidates — is there an existing document (often an empty starter)?
curl -H "$AUTH" $API/requests/$REQ/slots/<slotId>/candidates
# 2a. one exists: attach it (skip if already granted), then write the value
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"resourceId":"<resourceId>"}' $API/requests/$REQ/slots/<slotId>/attach
curl -X PUT -H "$AUTH" -H 'content-type: application/json' \
  -d '{"data":{"firstName":"Anna","lastName":"Ek"}}' \
  $API/requests/$REQ/slots/<slotId>
# 2b. none exists: create
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"data":{"firstName":"Anna","lastName":"Ek"}}' \
  $API/requests/$REQ/slots/<slotId>

Prefill your form from the candidate’s data so the user corrects rather than retypes. The write path needs the slot’s edit verb; the create path fill.

Update data later (the manage-account pattern)

The user changed banks and tells you first. Push the update through the connection — their vault stays the source of truth:

# 1. read the current record
curl -H "$AUTH" $API/requests/$REQ/slots/<slotId>
# 2. merge your change over the CURRENT data — a write replaces wholesale
# 3. write the next version
curl -X PUT -H "$AUTH" -H 'content-type: application/json' \
  -d '{"data":{"bankName":"Nya Banken","accountNumber":"SE45…","currency":"SEK"}}' \
  $API/requests/$REQ/slots/<slotId>

Never PUT a partial object: read, merge, write. The user sees the new version in Geena, attributed to your app — and every other connection they granted that document to serves the update on its next read. Your own organization’s kept copy is not rewritten by this: the new version reaches it as an update proposal, or as an automatic keep, under the slot’s adoption policy. Skip the write when nothing changed; a no-op version helps nobody.

List the user’s relatives

For manifests with subjects:

curl -H "$AUTH" $API/requests/$REQ/status          # → subjects[] + participants[] + their slots
curl -H "$AUTH" $API/requests/$REQ/subjects/<subjectId>/candidates
# → participants: [{ "alias": "…", "label": "Alma", "bound": true }, …]

Consent to the manifest is what authorizes this enumeration — it exists only per subject of an active connection, filtered to the declared type and relation, and it is receipted like every listing.

Add a child and fill their details

# the user types the name in YOUR ui — "stays in your Geena"
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"label":"Alma"}' $API/requests/$REQ/subjects/<subjectId>/participants
# → { "alias": "c0ffee12-…", … }   then fill each subject slot for that participant:
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"participant":"c0ffee12-…","data":{"firstName":"Alma","lastName":"Ek"}}' \
  $API/requests/$REQ/slots/<slotId>

Onboard the company the user invests through

For a manifest with a company subject (repeat: false — exactly one):

# 1. which companies does the user manage? details help them tell two apart
curl -H "$AUTH" $API/requests/$REQ/subjects/investing-company/candidates
# → participants: [{ "alias": "9a7f…", "label": "Acme B.V.", "bound": false,
#                    "details": { "registrationCountry": "NL", "registrationNumber": "12345678" } }]
# 2. none yet: found one — the user types the company's name; details come next
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"label":"Acme B.V."}' $API/requests/$REQ/subjects/investing-company/participants
# 3. fill the company's slots for that participant
curl -H "$AUTH" -H 'content-type: application/json' \
  -d '{"participant":"9a7f…","data":{"legalName":"Acme B.V.","registrationNumber":"12345678","registrationCountry":"NL"}}' \
  $API/requests/$REQ/slots/<companyDetailsSlotId>
curl -H "$AUTH" -F 'participant=9a7f…' -F 'label=Chamber of commerce extract' \
  -F 'file=@extract.pdf' $API/requests/$REQ/slots/<companyEvidenceSlotId>

Joining a company that already exists happens in Geena (its manager invites the user); it then appears in step 1. Company records stay editable from your app with a delegated PUT while the user manages the company — and a record served as unavailable with participant_ineligible means they no longer do.

Upload a file

curl -H "$AUTH" -F 'label=Driving licence' -F 'file=@licence.jpg' \
  $API/requests/$REQ/slots/<slotId>

Propose the label from the filename minus its extension and let the user edit it. Reading it back: the slot’s record carries a downloadUrl; stream it server-side — never hand your token to the browser. Files are capped at 32 MiB by default (413 too_large above it).

Use the connection as a login

The second visit needs no consent screen: an active connection plus a live login binding makes the ceremony skippable — the user clicks your button, Geena bounces them straight back with a code, and the token response carries the same request_id. That round-trip is your passwordless login: no password, no email code, and revocation in Geena is session revocation with you. Run the same authorize flow every time; keep no local credential.

Sign your back office in

The same ceremony with scope=profile offline_access organization and no manifest_id. If the token response’s scope carries organization, the person is a member of your organization and the token opens the Organization API; if not, they are an ordinary user — route them to your customer flow. See Organization access.

Handle revocation honestly

When the user revokes you in Geena, every serve answers 404/403 and GET /status reports the state. Poll it (or react to the first refused call), then:

  • drop everything you were not granted keep for — the grant to hold it is gone;
  • flip your UI to its honest empty state (“access ended — reconnect to continue”), not an error page;
  • offer the connect button again. A re-connect is a fresh consent, never a resurrection.

Build this state on day one. It is the demo every privacy-conscious customer runs first.