Skip to main content
A card holder = one person in one organization whose card transactions you want in your EMS. There are two ways to create them. This is one of the most important decisions in your integration — pick based on whether the person already exists in OpenCard or not.

At a glance

🅰️ Email + eID🅱️ identity_id (instant)
Best forNew employees, self-servicePerson already known to OpenCard
You provideemailidentity_id (from identities API)
User actionClick email → sign PDPC with eIDNone — instant
Transactions startAfter user completes eIDImmediately on create
Retroactive txsYes, after eID signYes, dispatched on create
Typical UX”Check your email to activate”Bulk import from employee list

Path 🅰️ — Email + eID (self-service)

Use when the person doesn’t exist in OpenCard yet — typical for new employees joining a client.

Create

POST /accounts/{accountId}/organizations/{organizationId}/cardholders
Scope: card-holders-write
{
  "reference_id": "employee_john_42",
  "email": "john@acme.se",
  "language": "sv"
}
FieldRequiredNotes
reference_idYour internal user ID
emailWhere PDPC signing link is sent
languagePDPC legal text: sv, no, da, en, fi
skip_pdpc_emailDefault false — email is sent

What happens

  1. Card holder + PDPC record created
  2. 📧 Email queued — subject: We need your approval.
  3. Link: https://{env}/accounts/{accountId}/pdpcs/{pdpcId}/sign/{token}
  4. User reads PDPC → checks consent → identifies with eID
  5. On eID success → identity linked → card_holder.identified webhook
  6. Retroactive transactions dispatched (everything since last card invoice)
  7. New transactions flow in real time

Timeline

You:     POST cardholder ─────────────────────────────────────────►
User:              receives email ──► signs PDPC/eID ──►
OpenCard:                                    identified ──► txs flow ⚡
Subscribe to card_holder.identified — that’s your signal that transactions are coming (including a retroactive batch). Don’t wait for card_holder.signed.pdpc alone; identification is what unlocks the pipe.
Full eID details → eID Signing · PDPC details → PDPC Flow

Path 🅱️ — identity_id (instant)

Use when the person already exists in OpenCard on this TPA — e.g. they have corporate cards from the issuer, or were onboarded to another organization under the same TPA. An identity = a physical person (SSN-linked via eID). They may already have cards attached. You skip the email entirely and link directly.

Step 1: List identities on the TPA

GET /accounts/{accountId}/tpas/{tpaId}/identities
Scope: account-tpa-identities-read
Filter to people not yet in your org:
GET .../identities?is_card_holder=false
Query paramValuesMeaning
is_card_holdertrueOnly identities that already have card holders
is_card_holderfalseOnly identities without card holders — good for finding who you still need to onboard
(omit)All identities on this TPA
Response (per identity):
{
  "id": 123,
  "name": "Anna Andersson",
  "employee_id": "001",
  "cards": [
    {
      "id": 10,
      "last_four": "1234",
      "token": "ext-card-id-abc",
      "type": "corporate"
    }
  ],
  "card_holders": [
    {
      "id": 22,
      "reference_id": "anna_other_org",
      "organizations": { "id": 3, "name": "Acme Corp" }
    }
  ]
}
FieldWhat it tells you
idUse this as identity_id when creating card holder
namePerson’s name from eID
employee_idEmployee ID at client (if set)
cardsCards already linked to this person on this TPA
card_holdersExisting card holder records (maybe in other orgs)

Step 2: Create card holder with identity_id

{
  "reference_id": "employee_anna_07",
  "identity_id": 123,
  "skip_pdpc_email": true
}
FieldRequiredNotes
reference_idYour internal user ID
identity_idFrom identities list. Must belong to same client as TPA.
emailNot needed when identity_id is set
skip_pdpc_emailrecommended truePerson already identified — no need for another email

What happens

  1. Card holder created
  2. Immediately linked to existing identity → identify() called
  3. 🔔 card_holder.identified webhook fires right away
  4. 🚀 SendRetroactiveWebhookEventsJob dispatched — all historical transaction states for their cards on this TPA are replayed to your webhook
  5. New transactions flow in real time from this point

Timeline

You:  GET identities ──► POST cardholder with identity_id ──► txs flow ⚡ (same second)
The retroactive burst can be large — all transaction states since last invoice replay as webhooks. Make sure your handler can upsert by transaction id and handle the volume.

Validation rules

  • identity_id must exist
  • Identity must belong to a client linked to this TPA — you can’t use identities from a different client’s TPA
  • Either email or identity_id required (not both required, but one must be present)

Which path should I use?

ScenarioPath
New hire, never used OpenCard🅰️ Email
Employee already has corp card via issuer🅱️ identity_id
Bulk onboarding 50 people who all have cards🅱️ identity_id
Re-adding person to a new organization under same TPA🅱️ identity_id
You want user to explicitly consent via email link🅰️ Email
Self-service “activate your card” flow in your app🅰️ Email

Webhooks to handle

EventPath 🅰️Path 🅱️
card_holder.created✅ on create✅ on create
card_holder.identified✅ after eID signimmediately on create
card_holder.signed.pdpc✅ after eID signMay not fire (already identified)
card.transaction.*After identifiedImmediately (+ retroactive batch)
Your integration should key off card_holder.identified as the moment transactions are expected.

Update / delete

PUT    .../cardholders/{cardHolderId}   ← resends PDPC email if unsigned
DELETE .../cardholders/{cardHolderId}  ← fires card_holder.deleted

meta in API responses

Check consent status without polling webhooks:
{
  "id": 7,
  "reference_id": "employee_john_42",
  "meta": {
    "signed": true,
    "signed_at": "2026-06-08T10:30:00Z",
    "pdpc_url": "https://api.opencard.io/accounts/1/pdpcs/3/sign/abc...",
    "email_delivered": true,
    "email_failed": false
  }
}