# Get your account Source: https://developers.opencard.io/api-reference/ems/account/get-your-account /openapi/ems-api.json get /accounts/{accountId} # Update your account Source: https://developers.opencard.io/api-reference/ems/account/update-your-account /openapi/ems-api.json put /accounts/{accountId} # EMS Authentication Source: https://developers.opencard.io/api-reference/ems/authentication Authenticate to the OpenCard EMS API with OAuth2 account_client credentials, request a bearer token, and scope it for the endpoints you call. The EMS API uses **`account_client`** OAuth credentials — client credentials flow, no user login. ## Get a token ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: application/json" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "scope=organizations-write webhooks-write card-holders-write account-tpas-write" ``` **Response:** ```json theme={null} { "token_type": "Bearer", "expires_in": 31536000, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..." } ``` ## account\_client scopes for EMS ``` organizations-read organizations-write card-holders-read card-holders-write webhooks-read webhooks-write webhook-events-read account-tpas-read account-tpas-write account-tpa-signatories-write account-tpa-identities-read payment-products-read account-payment-products-read account-payment-products-write public-records-read oauth-clients-read billings-write receipts-write ``` Create clients in the dashboard (Account → OAuth Clients) or via `POST /accounts/{accountId}/oauthclients`. Full scope reference → [Authentication](/getting-started/authentication) # Create billing profile Source: https://developers.opencard.io/api-reference/ems/billings/create-billing-profile /openapi/ems-api.json post /accounts/{accountId}/billings # Create card holder Source: https://developers.opencard.io/api-reference/ems/card-holders/create-card-holder /openapi/ems-api.json post /accounts/{accountId}/organizations/{organizationId}/cardholders Path A: `email` → PDPC email + eID. Path B: `identity_id` → instant, transactions flow immediately. # Delete card holder Source: https://developers.opencard.io/api-reference/ems/card-holders/delete-card-holder /openapi/ems-api.json delete /accounts/{accountId}/organizations/{organizationId}/cardholders/{cardHolderId} Fires `card_holder.deleted` webhook. # List card holders Source: https://developers.opencard.io/api-reference/ems/card-holders/list-card-holders /openapi/ems-api.json get /accounts/{accountId}/organizations/{organizationId}/cardholders Paginated list. Each item includes `identity` (`name`, `employee_id`) when linked — no separate identities call needed for display. # Update card holder Source: https://developers.opencard.io/api-reference/ems/card-holders/update-card-holder /openapi/ems-api.json put /accounts/{accountId}/organizations/{organizationId}/cardholders/{cardHolderId} `reference_id` is required. `email` is optional — omit to keep the current value (including null for identity-linked holders). Resends PDPC email only when unsigned/no identity, `skip_pdpc_email` is false, and an email address exists. Duplicate `reference_id` within the organization returns 400. # List identities on TPA Source: https://developers.opencard.io/api-reference/ems/identities/list-identities-on-tpa /openapi/ems-api.json get /accounts/{accountId}/tpas/{tpaId}/identities Physical persons linked to this TPA. Use `id` as `identity_id` for instant card holder onboarding. # Create OAuth client Source: https://developers.opencard.io/api-reference/ems/oauth-clients/create-oauth-client /openapi/ems-api.json post /accounts/{accountId}/oauthclients # List OAuth clients Source: https://developers.opencard.io/api-reference/ems/oauth-clients/list-oauth-clients /openapi/ems-api.json get /accounts/{accountId}/oauthclients # Get access token Source: https://developers.opencard.io/api-reference/ems/oauth/oauth/get-access-token /openapi/oauth.json post /oauth/token # Create organization Source: https://developers.opencard.io/api-reference/ems/organizations/create-organization /openapi/ems-api.json post /accounts/{accountId}/organizations # Get organization Source: https://developers.opencard.io/api-reference/ems/organizations/get-organization /openapi/ems-api.json get /accounts/{accountId}/organizations/{organizationId} # List organizations Source: https://developers.opencard.io/api-reference/ems/organizations/list-organizations /openapi/ems-api.json get /accounts/{accountId}/organizations # EMS API Overview Source: https://developers.opencard.io/api-reference/ems/overview Reference for the api.opencard.io EMS API — manage TPAs, organizations, card holders, OAuth clients, and webhooks for Expense Management Systems. This API is for **EMS integrators**. You manage clients, legal signing, card holders, and webhooks. Transaction data arrives via **webhooks**, not by polling. ## Base URL | Environment | URL | | - | - | | Production | `https://api.opencard.io/api/v1/application` | | Sandbox | `https://sandbox-api.opencard.io/api/v1/application` | ## What's included | Area | Endpoints | | - | - | | Account | Get/update your account | | OAuth Clients | Create API credentials | | Public Records | Company lookup before TPA | | Payment Products | Catalog of cards → enable on your account | | TPAs + Signatories | Legal authorization flow | | Identities | List people for instant card holder onboarding | | Organizations | Your end-clients | | Card Holders | List (with `identity.name` / `employee_id`), onboard employees (email or `identity_id`) | | Webhooks | Receive transaction events | | Receipt Scanner | OCR photo receipts | ## What's NOT here * `/me` — end-user profile API (not for EMS integration) * `/transactions` query — use webhooks instead * Admin endpoints (global users, logs, statistics, etc.) ## Auth ``` POST https://api.opencard.io/oauth/token → Bearer token on all requests ``` See [Authentication](/api-reference/ems/authentication). ## Typical flow 1. `GET /payment-products` → `POST .../payment-products/{id}` 2. `POST /accounts/{id}/tpas` + signatories (`payment_product_id` = product `id`) 3. `POST /accounts/{id}/organizations` 4. `POST .../webhooks` 5. `POST .../cardholders` 6. Receive `card.transaction.*` on your webhook URL Full walkthrough → [Quickstart](/getting-started/quickstart) # Disable payment product Source: https://developers.opencard.io/api-reference/ems/payment-products/disable-payment-product /openapi/ems-api.json delete /accounts/{accountId}/payment-products/{paymentProductId} # Enable payment product Source: https://developers.opencard.io/api-reference/ems/payment-products/enable-payment-product /openapi/ems-api.json post /accounts/{accountId}/payment-products/{paymentProductId} Enable a payment product from the [catalog](#tag/Payment-Products/operation/listPaymentProducts) on your account. Empty body. `201` on success; `400` if already enabled. # Get enabled payment product Source: https://developers.opencard.io/api-reference/ems/payment-products/get-enabled-payment-product /openapi/ems-api.json get /accounts/{accountId}/payment-products/{paymentProductId} # List available payment products Source: https://developers.opencard.io/api-reference/ems/payment-products/list-available-payment-products /openapi/ems-api.json get /payment-products Platform catalog of payment products you can enable. Pick an `id`, then [enable it on your account](#tag/Payment-Products/operation/enablePaymentProduct). Optional `q` searches name, subtitle, and program name. Optional `funding` filters `debit` or `credit`. # List enabled payment products Source: https://developers.opencard.io/api-reference/ems/payment-products/list-enabled-payment-products /openapi/ems-api.json get /accounts/{accountId}/payment-products Payment products already enabled on your account (what clients can pick in onboarding). Same optional filters as the catalog: `q` searches name, subtitle, and program name; `funding` filters `debit` or `credit`. # Lookup company + signing combinations Source: https://developers.opencard.io/api-reference/ems/public-records/lookup-company-+-signing-combinations /openapi/ems-api.json get /accounts/{accountId}/publicrecords # OCR scan receipt Source: https://developers.opencard.io/api-reference/ems/receipt-scanner/ocr-scan-receipt /openapi/ems-api.json post /accounts/{accountId}/receipts/scan # Add signatory (sends email) Source: https://developers.opencard.io/api-reference/ems/tpa-signatories/add-signatory-sends-email /openapi/ems-api.json post /accounts/{accountId}/tpas/{tpaId}/signatories # List signatories Source: https://developers.opencard.io/api-reference/ems/tpa-signatories/list-signatories /openapi/ems-api.json get /accounts/{accountId}/tpas/{tpaId}/signatories # Create TPA Source: https://developers.opencard.io/api-reference/ems/tpas/create-tpa /openapi/ems-api.json post /accounts/{accountId}/tpas Creates TPA, fetches registry data, generates legal text. # Delete TPA Source: https://developers.opencard.io/api-reference/ems/tpas/delete-tpa /openapi/ems-api.json delete /accounts/{accountId}/tpas/{tpaId} # Get TPA Source: https://developers.opencard.io/api-reference/ems/tpas/get-tpa /openapi/ems-api.json get /accounts/{accountId}/tpas/{tpaId} # List TPAs Source: https://developers.opencard.io/api-reference/ems/tpas/list-tpas /openapi/ems-api.json get /accounts/{accountId}/tpas # Create webhook Source: https://developers.opencard.io/api-reference/ems/webhooks/create-webhook /openapi/ems-api.json post /accounts/{accountId}/organizations/{organizationId}/webhooks # Delivery log Source: https://developers.opencard.io/api-reference/ems/webhooks/delivery-log /openapi/ems-api.json get /accounts/{accountId}/organizations/{organizationId}/webhooks/{webhookId}/events # List webhooks Source: https://developers.opencard.io/api-reference/ems/webhooks/list-webhooks /openapi/ems-api.json get /accounts/{accountId}/organizations/{organizationId}/webhooks # Create card Source: https://developers.opencard.io/api-reference/issuers/cards/create-card /openapi/issuer-api.json post /api/v1/issuers/{slug}/cards # Delete card Source: https://developers.opencard.io/api-reference/issuers/cards/delete-card /openapi/issuer-api.json delete /api/v1/issuers/{slug}/cards/{cardId} # Get card Source: https://developers.opencard.io/api-reference/issuers/cards/get-card /openapi/issuer-api.json get /api/v1/issuers/{slug}/cards/{cardId} # List cards Source: https://developers.opencard.io/api-reference/issuers/cards/list-cards /openapi/issuer-api.json get /api/v1/issuers/{slug}/cards # Update card Source: https://developers.opencard.io/api-reference/issuers/cards/update-card /openapi/issuer-api.json put /api/v1/issuers/{slug}/cards/{cardId} # Issuer API Overview Source: https://developers.opencard.io/api-reference/issuers/overview Reference for the OpenCard Issuer API — register cards and push transaction states under /api/v1/issuers/{slug}/ on api.opencard.io. Card issuers use this API to **register cards** and **deliver transaction states** to OpenCard. Connected EMS apps receive the data via webhooks. This is **not** `receipts.opencard.io`. For digital receipt matching only → [Digital Receipts API](/api-reference/receipts/overview). ## Base URLs | Environment | URL | | - | - | | Sandbox | `https://sandbox-api.opencard.io` | | Production | `https://api.opencard.io` | ## Path prefix ``` /api/v1/issuers/{slug}/ ``` `{slug}` is assigned at integration start (e.g. your program identifier in the URL). ## Auth OAuth 2.0 client credentials → `POST /oauth/token` → [Authentication](/card-issuers/issuer-integration/authentication) ## Card endpoints | Method | Path | Description | | - | - | - | | `POST` | `/api/v1/issuers/{slug}/cards` | Create card | | `DELETE` | `/api/v1/issuers/{slug}/cards/{cardId}` | Delete card | | `PUT` | `/api/v1/issuers/{slug}/cards/{cardId}` | Update card | | `GET` | `/api/v1/issuers/{slug}/cards/{cardId}` | Get card | | `GET` | `/api/v1/issuers/{slug}/cards` | List cards | | `POST` | `/api/v1/issuers/{slug}/cards/{cardId}/transaction_states` | Create transaction state | ## Integration guides | Guide | Topic | | - | - | | [How it works](/card-issuers/issuer-integration) | Flow and prerequisites | | [Onboarding](/card-issuers/issuer-integration/onboarding) | TPA and go-live | | [Cards](/card-issuers/issuer-integration/cards) | Card registry | | [Transaction states](/card-issuers/issuer-integration/transaction-states) | Lifecycle events | | [TPA delivery](/card-issuers/issuer-integration/tpa-integration) | Signed / terminated TPA callbacks | Contact **[support@opencard.io](mailto:support@opencard.io)** for `client_id`, `client_secret`, scopes, and `{slug}`. # Create transaction state Source: https://developers.opencard.io/api-reference/issuers/transaction-states/create-transaction-state /openapi/issuer-api.json post /api/v1/issuers/{slug}/cards/{cardId}/transaction_states # Deliver receipt enrichment Source: https://developers.opencard.io/api-reference/receipt-providers/callback/deliver-receipt-enrichment /openapi/receipt-provider-callback.json post /v1/service/marcet/callback/{referenceId} HMAC-SHA256 via X-Data-Signature header. # Receipt Provider Callback API Source: https://developers.opencard.io/api-reference/receipt-providers/overview Reference for the inbound receipt provider callback API — POST matched receipt payloads to OpenCard with HMAC-SHA256 signatures on api.opencard.io. Receipt providers POST enriched receipt data to OpenCard when a match is found. This is the **inbound** callback API. For the full delivery flow (headers, HMAC, payloads) see [Callback delivery](/receipt-providers/callback-delivery). ## Base URL ``` https://api.opencard.io/api ``` Sandbox: `https://sandbox-api.opencard.io/api` ## Auth `X-Data-Signature` header — HMAC-SHA256 of the raw request body with your client secret. ## Key endpoint | Method | Path | Purpose | | - | - | - | | `POST` | `/v1/service/marcet/callback/{referenceId}` | Deliver matched receipt data | # Create callback request Source: https://developers.opencard.io/api-reference/receipts/callback-requests/create-callback-request /openapi/receipts-api.json post /api/v1/marcet/callbackrequests Submit transaction for receipt matching. OpenCard calls your callback_url when matched. # Delete callback request Source: https://developers.opencard.io/api-reference/receipts/callback-requests/delete-callback-request /openapi/receipts-api.json delete /api/v1/marcet/callbackrequests/{callbackRequestId} # Get callback request Source: https://developers.opencard.io/api-reference/receipts/callback-requests/get-callback-request /openapi/receipts-api.json get /api/v1/marcet/callbackrequests/{callbackRequestId} # List callback requests Source: https://developers.opencard.io/api-reference/receipts/callback-requests/list-callback-requests /openapi/receipts-api.json get /api/v1/marcet/callbackrequests # Update (e.g. clearing status) Source: https://developers.opencard.io/api-reference/receipts/callback-requests/update-eg-clearing-status /openapi/receipts-api.json put /api/v1/marcet/callbackrequests/{callbackRequestId} # Digital Receipts API Overview Source: https://developers.opencard.io/api-reference/receipts/overview Reference for receipts.opencard.io — card issuers discover publishers and submit card transactions for digital receipt matching across OpenCard merchants. Payment providers and card issuers use this API to **request digital receipt matching** from OpenCard's merchant network. This API is for **card issuers** only. If you collect receipts and deliver them to OpenCard → see [Receipt Providers](/receipt-providers). ## Base URL ``` https://receipts.opencard.io ``` ## How it works ```mermaid theme={null} sequenceDiagram participant Issuer as Card Issuer participant R as receipts.opencard.io participant M as Merchant Network Issuer->>R: GET /publishers (which merchants?) Issuer->>R: POST /callbackrequests (transaction data) R->>M: Match receipt R->>Issuer: Callback to your URL with receipt ``` ## Auth Bearer JWT. Contact **[support@opencard.io](mailto:support@opencard.io)** for credentials. ## Key endpoints | Method | Path | Purpose | | - | - | - | | `GET` | `/api/v1/marcet/publishers` | List merchants with digital receipts | | `POST` | `/api/v1/marcet/callbackrequests` | Submit transaction for matching | | `PUT` | `/api/v1/marcet/callbackrequests/{id}` | Update (e.g. mark cleared) | | `DELETE` | `/api/v1/marcet/callbackrequests/{id}` | Cancel request | Full guide → [Integration guide](/card-issuers/digital-receipts/integration) # List active merchants Source: https://developers.opencard.io/api-reference/receipts/publishers/list-active-merchants /openapi/receipts-api.json get /api/v1/marcet/publishers # Overview Source: https://developers.opencard.io/card-issuers/digital-receipts/index Digital receipts for card issuers — match merchant receipts to card transactions via a separate API on receipts.opencard.io. Give your cardholders **digital receipts** from merchants in OpenCard's network — matched to card transactions and delivered to your callback URL. This is a **separate product** from the [Issuer transaction API](/card-issuers/issuer-integration) on `api.opencard.io`. Different host, credentials, and use case. You can run one or both. *** ## What you get | Step | What happens | | - | - | | 1. Discover | List merchants (publishers) with active digital receipts and their CAIDs | | 2. Submit | POST transaction data for matching | | 3. Receive | OpenCard calls your callback when a receipt is found | **Host:** `receipts.opencard.io`\ **Auth:** Bearer JWT (provided at onboarding) *** ## How it fits ```mermaid theme={null} sequenceDiagram participant You as Card Issuer participant R as receipts.opencard.io participant M as Merchant Network You->>R: GET /publishers You->>R: POST /callbackrequests R->>M: Match receipt R->>You: Callback with receipt ``` *** ## Guides | Guide | What it covers | | - | - | | [Integration](/card-issuers/digital-receipts/integration) | Step-by-step: publishers, callback requests, clearing | | [Digital Receipts API](/api-reference/receipts/overview) | Full endpoint reference | Contact **[support@opencard.io](mailto:support@opencard.io)** for JWT credentials and callback setup. # Integration Source: https://developers.opencard.io/card-issuers/digital-receipts/integration Step-by-step guide for card issuers to discover merchant publishers, submit transactions via CAID, and receive matched digital receipts on a callback URL. This API is **separate** from the [Issuer transaction API](/card-issuers/issuer-integration) on `api.opencard.io`. You can use either or both — they do not share credentials or URLs. *** ## Step 1: Discover merchants ``` GET https://receipts.opencard.io/api/v1/marcet/publishers Authorization: Bearer {jwt} Accept: application/json ``` Returns merchants (publishers) with active digital receipts. Each has **CAIDs** (Card Acceptor IDs / Merchant IDs) you match against transaction data. **Response example:** ```json theme={null} [ { "id": "79e59794-449e-4f3c-b584-0e9e9547501a", "caids": [ { "caid": "00004172235", "activated": "2014-03-31" } ] } ] ``` *** ## Step 2: Submit transaction for matching ``` POST https://receipts.opencard.io/api/v1/marcet/callbackrequests Authorization: Bearer {jwt} Accept: application/json Content-Type: application/json ``` **Body** (array of callback transactions): ```json theme={null} [ { "callback_path": "https://your-issuer.com/receipts/callback/txn_001", "callback_content_type": "text/xml", "transaction": { "id": "txn_001", "auth_date": "2026-06-08", "auth_time": "11:12:14", "auth_timezone": "Europe/Stockholm", "reference_no": "190102519907", "auth_code": "ABC123", "terminal_id": "0000000015745355", "merchant_no": "3462188", "merchant_name": "Espresso House", "merchant_country": "SE", "auth_amount": 99.95, "auth_currency": "SEK", "auth_masked_card_number": "****1234", "clearing": "false", "type": "CARD_PURCHASE", "state": "AUTHORIZED", "mcc": "5814" } } ] ``` ### Required transaction fields | Field | ISO8583 | Description | | - | - | - | | `id` | — | Your public transaction ID | | `auth_date` | Field 13 | `YYYY-MM-DD` | | `auth_time` | Field 12 | `HH:mm:ss` | | `auth_timezone` | — | e.g. `Europe/Stockholm` | | `reference_no` | Field 37 | Retrieval reference number | | `auth_code` | Field 38 | Authorization code | | `terminal_id` | Field 41 | Terminal ID | | `merchant_no` | Field 42 | Merchant ID (CAID) | | `merchant_name` | Field 43 | Merchant name | | `auth_amount` | — | Original amount | | `auth_currency` | — | ISO 4217 | | `auth_masked_card_number` | — | Masked PAN | | `clearing` | — | `"true"` / `"false"` | *** ## Step 3: Receive matched receipt OpenCard calls your `callback_path` when a receipt is matched. Your endpoint must: * Accept the configured `callback_content_type` * Return HTTP `200` * Verify webhook signature (provided during onboarding) *** ## Update when transaction clears ``` PUT /api/v1/marcet/callbackrequests/{id} ``` Update `clearing` to `"true"` when the transaction settles — improves match accuracy. *** ## API reference → [Digital Receipts API](/api-reference/receipts/overview) for full endpoint docs with examples. Contact **[support@opencard.io](mailto:support@opencard.io)** for JWT credentials and callback verification setup. # Overview Source: https://developers.opencard.io/card-issuers/index OpenCard integration paths for card issuers — deliver transactions to connected EMS apps, or fetch digital receipts from the merchant network. This section is for **card issuers** — banks and card programs integrating with OpenCard. *** ## Two integrations — pick what you need OpenCard offers **two separate APIs**. Most issuers start with transaction delivery; digital receipts is optional and runs on a different host. Receive signed TPAs, register cards, and push transaction states (`authorized`, `cleared`, `invoiced`, `deleted`) to OpenCard for connected Expense Management Systems. Submit transactions to `receipts.opencard.io` for merchant receipt matching — independent from transaction delivery. *** ## Issuer Integration at a glance | | | | - | - | | **Base URL (sandbox)** | `https://sandbox-api.opencard.io` | | **Base URL (production)** | `https://api.opencard.io` | | **API prefix** | `/api/v1/issuers/{slug}/` | | **Auth** | OAuth 2.0 client credentials | | **Credentials** | `client_id`, `client_secret`, and `{slug}` delivered when integration starts | → [How it works](/card-issuers/issuer-integration) # Authentication Source: https://developers.opencard.io/card-issuers/issuer-integration/authentication OAuth 2.0 client credentials for the OpenCard Issuer API — obtain a bearer token and call endpoints under /api/v1/issuers/{slug}/. The Issuer API uses **OAuth 2.0 client credentials**. OpenCard delivers a `client_id`, `client_secret`, issuer `{slug}`, and scope list when your integration starts. *** ## Get an access token ```bash theme={null} curl -X POST {base_url}/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: application/json" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "scope=issuer-{slug}-cards-write issuer-{slug}-transaction-states-write" ``` Replace `{base_url}` with `https://sandbox-api.opencard.io` or `https://api.opencard.io`. Replace `{slug}` in scope names with your assigned slug. **Response `200`:** ```json theme={null} { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600 } ``` Tokens expire — cache until shortly before `expires_in` and refresh with a new token request. *** ## Call the API ```bash theme={null} curl {base_url}/api/v1/issuers/{slug}/cards \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Accept: application/json" ``` Always send `Accept: application/json`. Without it, auth failures may return an HTML login page instead of a JSON error body. *** ## Scopes OpenCard assigns scopes per issuer slug: | Scope | Allows | | - | - | | `issuer-{slug}-cards-read` | `GET` cards | | `issuer-{slug}-cards-write` | `POST` and `PUT` cards | | `issuer-{slug}-cards-delete` | `DELETE` cards | | `issuer-{slug}-transaction-states-write` | `POST` transaction states | Your exact scope string is provided at onboarding. Request only the scopes you need in the token call — space-separated. Sandbox and production use **separate** credentials. Never point production traffic at sandbox URLs or vice versa. # Cards Source: https://developers.opencard.io/card-issuers/issuer-integration/cards Register and maintain cards on the OpenCard Issuer API — create, update, list, get, and delete cards under /api/v1/issuers/{slug}/cards. The cards endpoints maintain the set of cards enrolled in OpenCard per customer organisation. Register cards before sending transaction states — `{card_id}` in the transaction path is **your** card id from the create payload. **Base path:** ``` {base_url}/api/v1/issuers/{slug}/cards ``` *** ## Endpoints | Method | Path | Scope | Purpose | | - | - | - | - | | `POST` | `/cards` | `issuer-{slug}-cards-write` | Create | | `PUT` | `/cards/{card_id}` | `issuer-{slug}-cards-write` | Update | | `GET` | `/cards/{card_id}` | `issuer-{slug}-cards-read` | Get one | | `GET` | `/cards` | `issuer-{slug}-cards-read` | List / filter | | `DELETE` | `/cards/{card_id}` | `issuer-{slug}-cards-delete` | Delete | *** ## Create card ``` POST /api/v1/issuers/{slug}/cards Scope: issuer-{slug}-cards-write ``` ```json theme={null} { "id": "12345", "payment_product": "acmebank_credit", "last_four": "1234", "bin_number": "123456", "liability": "corporate_with_personal_invoice", "scheme": "visa", "funding": "credit", "issuer_organization_number": "5555555551", "issuer_country_code": "SE", "identity": { "ssn": "190001011111", "ssn_country_code": "SE", "name": "Test Testsson", "company_organization_number": "5555555555", "company_country_code": "SE" } } ``` | Field | Required | Notes | | - | - | - | | `id` | ✅ | **Your** unique card id — used as `{card_id}` in transaction URLs | | `payment_product` | ✅ | Code for the payment product this card belongs to — the same code you receive on the TPA `initiate`. OpenCard uses it to link the card to the customer's signed TPA | | `last_four` | ✅ | Last four digits | | `bin_number` | ✅ | BIN, max 12 characters | | `liability` | ✅ | `personal`, `corporate`, or `corporate_with_personal_invoice` | | `scheme` | ✅ | e.g. `visa`, `mastercard` | | `funding` | ✅ | `debit` or `credit` — appears on EMS webhooks as `card_funding` | | `issuer_organization_number` | ✅ | Your organisation number (seller party) | | `issuer_country_code` | ✅ | ISO 3166-1 alpha-2 | | `identity.ssn` | ✅ | Cardholder national id | | `identity.ssn_country_code` | ✅ | ISO 3166-1 alpha-2 | | `identity.name` | ✅ | Cardholder display name | | `identity.company_organization_number` | ✅ | Customer company org number (buyer party) | | `identity.company_country_code` | ✅ | ISO 3166-1 alpha-2 | `payment_product` must match a product the customer has signed a TPA for. Cards on a product without a signed TPA are stored but their transactions are **not** delivered to the EMS. **Responses:** | Code | Meaning | | - | - | | `201` | Card created | | `200` | Card already exists (idempotent — same `id`) | Create is **idempotent** on `id`. Re-posting the same card returns the existing record. *** ## Update card ``` PUT /api/v1/issuers/{slug}/cards/{card_id} Scope: issuer-{slug}-cards-write ``` Same body shape as create (except `id` comes from the path). Supports moving a card between identities/companies when holder or employer changes. **Response `200`** with `updated: true`. *** ## Get card ``` GET /api/v1/issuers/{slug}/cards/{card_id} Scope: issuer-{slug}-cards-read ``` **Response `200`:** ```json theme={null} { "ok": true, "card": { "id": "12345", "payment_product": "acmebank_credit", "last_four": "1234", "bin_number": "123456", "liability": "corporate_with_personal_invoice", "funding": "credit", "buyer_party": "5555555555", "seller_party": "5555555551", "active": true, "created_at": "2026-01-15T10:00:00+00:00", "updated_at": "2026-06-08T10:00:00+00:00" } } ``` *** ## List cards ``` GET /api/v1/issuers/{slug}/cards Scope: issuer-{slug}-cards-read ``` Query parameters: `last_four`, `buyer_party`, `seller_party`, `bin_number`, `id`, `active`, `per_page` (1–100, default 15), `page`. **Response `200`:** ```json theme={null} { "ok": true, "data": [ { "id": "12345", "payment_product": "acmebank_credit", "last_four": "1234", "bin_number": "123456", "liability": "corporate_with_personal_invoice", "funding": "credit", "buyer_party": "5555555555", "seller_party": "5555555551", "active": true, "created_at": "2026-01-15T10:00:00+00:00", "updated_at": "2026-06-08T10:00:00+00:00" } ], "meta": { "current_page": 1, "last_page": 1, "per_page": 15, "total": 1 } } ``` *** ## Delete card ``` DELETE /api/v1/issuers/{slug}/cards/{card_id} Scope: issuer-{slug}-cards-delete ``` Soft-deletes the card and marks it inactive. **Response `204`** with empty body. *** ## Why the registry matters OpenCard matches every transaction state to a registered card, its `payment_product`, and identity. Without an up-to-date registry: * Transaction posts return `card_not_found` * EMS apps never see data for unknown cards * Cards with the wrong `payment_product` are linked to the wrong TPA — or to none Keep cards in sync when issued, replaced, or closed at your end. # How it works Source: https://developers.opencard.io/card-issuers/issuer-integration/index Connect your card program to OpenCard — push cards and transaction states so connected EMS apps receive real-time spend data and enrichment. The **Issuer API** lets card issuers integrate directly with OpenCard and distribute transaction data to connected Expense Management Systems (EMS). In addition to standard transaction data, OpenCard can attach **enriched information** — digital receipts, correct VAT, line items, environmental metrics — through partner networks. EMS users get a fuller picture of each purchase without you building that layer yourself. This is **not** the digital receipts API on `receipts.opencard.io`. That is a separate product for receipt matching only. See [Digital receipts](/card-issuers/digital-receipts) if you need it. *** ## End-to-end flow ```mermaid theme={null} sequenceDiagram participant Customer as Customer signatory participant EMS as EMS participant OC as OpenCard participant Issuer as You (issuer) Customer->>EMS: Sign TPA (eID) EMS->>OC: TPA signed OC->>Issuer: Signed TPA (API / email) Issuer->>OC: POST /issuers/{slug}/cards Issuer->>OC: POST .../transaction_states OC->>EMS: Webhooks (transactions + enrichment) ``` *** ## API surface All endpoints live under: ``` {base_url}/api/v1/issuers/{slug}/ ``` Replace `{slug}` with the value OpenCard assigns your integration. | Area | Methods | Purpose | | - | - | - | | **Cards** | `POST` create, `PUT` update, `GET` one, `GET` list, `DELETE` close | Register, maintain, and inspect the card registry | | **Transaction states** | `POST .../transaction_states` | Deliver transaction lifecycle events | *** ## Guides | Guide | What it covers | | - | - | | [Onboarding](/card-issuers/issuer-integration/onboarding) | TPA handoff and when to start sending data | | [Authentication](/card-issuers/issuer-integration/authentication) | OAuth client credentials and scopes | | [Cards](/card-issuers/issuer-integration/cards) | Create, update, list, and delete cards | | [Transaction states](/card-issuers/issuer-integration/transaction-states) | Push purchases through their lifecycle | | [TPA delivery](/card-issuers/issuer-integration/tpa-integration) | How OpenCard delivers signed / terminated TPAs to you (email or API) | API reference → [Issuer API](/api-reference/issuers/overview) *** ## Environments | | Sandbox | Production | | - | - | - | | **Base URL** | `https://sandbox-api.opencard.io` | `https://api.opencard.io` | | **OAuth token** | `POST {base_url}/oauth/token` | Same path | Contact **[support@opencard.io](mailto:support@opencard.io)** to start integration and receive sandbox credentials. # Onboarding Source: https://developers.opencard.io/card-issuers/issuer-integration/onboarding How issuer integration starts — TPA signing by the customer, receiving the signed agreement, and when to register cards and send transactions. Integration starts when a **customer** (your corporate card client) authorises card data to flow through OpenCard to their EMS. *** ## 1. Customer signs the TPA The customer's authorized signatories sign a **Transaction Processing Authorisation (TPA)** digitally via their EMS. The TPA states that the customer authorises **you** (the issuer) to release their card transactions to OpenCard for delivery to that EMS. You do not run the signing flow — the EMS and OpenCard handle it. *** ## 2. You receive the signed TPA When signing is complete, OpenCard notifies you via **API callback** and/or **email** (configured for your integration). On API, you receive an `initiate` payload with the signed PDF and the `payment_product` code the customer signed for — archive the PDF, enable **only that payment product** for the customer, then start sending cards and transactions. Full contract (initiate + terminate) → [TPA delivery](/card-issuers/issuer-integration/tpa-integration) ```mermaid theme={null} flowchart TD A[Customer signs TPA via EMS] --> B[OpenCard notifies you] B --> C{Email or API?} C -->|API| D[POST action=initiate + PDF] C -->|Email| E[Inbox with signed PDF] D --> F[Archive TPA — start card delivery] E --> F ``` *** ## 3. Register cards Create cards with `POST /api/v1/issuers/{slug}/cards` for each card that should flow through OpenCard. Every card carries the `payment_product` code it belongs to — OpenCard uses it to link the card to the TPA the customer signed. The card registry is the foundation for transaction routing — every `transaction_states` call references a `{card_id}` you registered earlier. → [Cards](/card-issuers/issuer-integration/cards) *** ## 4. Push transaction states Send all lifecycle states for each purchase: * `authorized` — purchase just happened * `cleared` — settled * `invoiced` — included on issuer invoice (with `invoice_number` when applicable) * `deleted` — authorization reversed → [Transaction states](/card-issuers/issuer-integration/transaction-states) *** ## Get credentials Contact **[support@opencard.io](mailto:support@opencard.io)** with: * Your legal name * Expected markets (SE, NO, DK, FI) * Technical contact for OAuth credentials and `{slug}` assignment # TPA delivery Source: https://developers.opencard.io/card-issuers/issuer-integration/tpa-integration How OpenCard delivers signed (and terminated) TPAs to you as the card issuer — by email or by POST to your callback endpoint. When a customer finishes signing a **Transaction Processing Authorisation (TPA)** in their EMS, OpenCard notifies **you** (the issuer) so you can start delivering cards and transactions for that company. The same path is used when the customer **terminates** the connection from the EMS — you stop processing for that party. *** ## Delivery options OpenCard can notify you in either (or both) of these ways — configured when your integration is set up: | Channel | What you get | | - | - | | **Email** | Signed TPA PDF + party details to your operations inbox | | **API** | `POST` to an HTTPS endpoint **you** provide | This page describes the **API** contract. Email is the same information packaged for humans. ```mermaid theme={null} sequenceDiagram participant Customer as Customer participant EMS as EMS participant OC as OpenCard participant Issuer as You (issuer) Customer->>EMS: Sign TPA (eID) EMS->>OC: TPA complete alt API callback OC->>Issuer: POST action=initiate + PDF else Email OC->>Issuer: Email with signed PDF end Issuer->>Issuer: Onboard customer + archive TPA Issuer->>OC: Cards + transaction states ``` → Signing flow on the EMS side: [Issuer onboarding](/card-issuers/issuer-integration/onboarding) *** ## Your callback endpoint You give OpenCard a URL. OpenCard calls it for both **initiate** and **terminate**. | | | | - | - | | **Method** | `POST` | | **Content-Type** | `application/json` | | **Auth** | Whatever you require — agreed at onboarding (e.g. Bearer token, mutual TLS, HMAC header) | | **Success** | `200` when processing is accepted or completed | | **Client error** | `4xx` with a clear body (validation / reject) | | **Temporary failure** | `5xx` — OpenCard may retry | When this endpoint receives a valid **`initiate`** event, you should: 1. **Initialize** the customer for OpenCard card and transaction delivery — for the `payment_product` in the payload **only** 2. **Archive** the signed TPA PDF as legal evidence 3. Return **`200`** when accepted/completed — otherwise a clear **`4xx`** / **`5xx`** When you receive **`terminate`**, stop card/transaction delivery for that party and payment product, and archive the decision according to your process. *** ## Payment product A TPA is signed for **one payment product** — for example your corporate credit card, or your corporate debit card. If you offer several products, the customer signs a separate TPA for each one they want connected. Every TPA event carries a `payment_product` code so you know exactly which product to enable or disable. OpenCard assigns the codes with you at onboarding, one per product you offer through OpenCard. | You offer | `payment_product` codes (example) | | - | - | | Corporate credit card | `acmebank_credit` | | Corporate debit card | `acmebank_debit` | On `initiate`, enable **only** the product in `payment_product`. Do not enable other products for the same customer — a TPA for one product does not authorise data for another. The same code is required on every card you register → [Cards](/card-issuers/issuer-integration/cards). That is how OpenCard links a card to the TPA the customer signed. *** ## `initiate` — TPA signed Sent after all required signatories have signed. Includes the signed PDF as Base64. ```json theme={null} { "action": "initiate", "payment_product": "acmebank_credit", "party_information": { "id": 6011, "name": "Open API Int. AB", "country": "SE", "organization_number": "5593958076", "validate_signatures": false, "processing_setup": { "authorized": "When they occur", "cleared": "Not invoiced yet - First not invoiced transaction", "invoiced": "Invoiced - Next ordinary invoice date" }, "signatories": [ { "name": "Andersson, Anders" }, { "name": "Svensson, Lisa" } ] }, "system": { "name_legal": "KwickExpense AB", "name_system": "KwickExpense" }, "document": { "file_name": "TPA_20260414_OpenAPIInt.AB.pdf", "mime_type": "application/pdf", "content_base64": "" } } ``` ### Field reference | Field | Meaning | | - | - | | `action` | Always `initiate` for a new signed TPA | | `payment_product` | Code for the payment product this TPA covers — enable **only** this product for the customer | | `party_information.id` | OpenCard party / TPA-side identifier for this customer | | `party_information.name` | Legal / display name of the customer company | | `party_information.country` | ISO country (e.g. `SE`) | | `party_information.organization_number` | Company registration number | | `party_information.validate_signatures` | Whether OpenCard expects you to treat signature validation as required on your side | | `party_information.processing_setup` | How transaction states should be produced for this customer (`authorized` / `cleared` / `invoiced`) | | `party_information.signatories` | People who signed the TPA for this customer | | `system.name_legal` | Legal name of the EMS that onboarded the customer | | `system.name_system` | System / brand name of that EMS | | `document.file_name` | Suggested filename for the archived PDF | | `document.mime_type` | Always `application/pdf` | | `document.content_base64` | Full signed TPA PDF, Base64-encoded | Decode `content_base64` and store the PDF. *** ## `terminate` — connection ended Sent when the customer ends the OpenCard connection for a payment product in their EMS (or OpenCard terminates on their behalf). Same endpoint, smaller payload — **no PDF**. ```json theme={null} { "action": "terminate", "payment_product": "acmebank_credit", "party_information": { "id": 6011, "name": "Open API Int. AB", "country": "SE", "organization_number": "5593958076" }, "system": { "name_legal": "KwickExpense AB", "name_system": "KwickExpense" } } ``` | Field | Meaning | | - | - | | `action` | Always `terminate` | | `payment_product` | Which product to stop — other products the customer has a TPA for are **not** affected | | `party_information.*` | Same customer identity as on initiate — stop delivery for this party and product | | `system.*` | Which EMS the customer used | *** ## Response expectations | HTTP | When | | - | - | | `200` | You accepted the event (customer initialized / terminated, PDF archived if initiate) | | `4xx` | Bad payload, unknown party, business reject — include a readable error message | | `5xx` | Temporary failure — safe for OpenCard to retry | *** ## After initiate 1. Register cards for the customer → [Cards](/card-issuers/issuer-integration/cards) 2. Push lifecycle states → [Transaction states](/card-issuers/issuer-integration/transaction-states) To wire the callback URL and auth scheme, contact **[support@opencard.io](mailto:support@opencard.io)**. # Transaction states Source: https://developers.opencard.io/card-issuers/issuer-integration/transaction-states Push card transaction lifecycle events to OpenCard — authorized, cleared, invoiced, and deleted states via the Issuer API. Transaction states are the **primary channel** for delivering purchases to OpenCard. Send every state in the lifecycle so connected EMS apps receive a complete and accurate history. ``` POST /api/v1/issuers/{slug}/cards/{card_id}/transaction_states Scope: issuer-{slug}-transaction-states-write ``` `{card_id}` is the `id` you sent when [creating the card](/card-issuers/issuer-integration/cards). **Response `202`** — accepted for asynchronous processing. Empty body on success. *** ## Example — cleared purchase ```json theme={null} { "id": "tx_1234567891", "state": "cleared", "type": "CARD_PURCHASE", "invoice_number": null, "original_amount": 109.38, "original_currency": "SEK", "accounting_amount": 109.38, "accounting_currency": "SEK", "exchange_rate": 1.0, "vat_rate": 0.25, "vat_amount": 21.88, "vat_currency": "SEK", "purchase_merchant": "Coop Vasagatan", "purchase_time": "2025-11-17T15:35:11Z", "purchase_country": "SE", "purchase_city": "STOCKHOLM", "mcc_code": "5411", "merchant_number": "12345678", "terminal_id": "TERM001", "rrn": "123456789012", "auth_code": "A1B2C3" } ``` *** ## Field reference | Field | Required | Description | | - | - | - | | `id` | ✅ | Your unique transaction id (same across states for one purchase) | | `state` | ✅ | `authorized`, `cleared`, `invoiced`, or `deleted` | | `type` | ✅ | `CARD_PURCHASE`, `CASH_WITHDRAWAL`, or `FEE_AND_DISCOUNT` | | `invoice_number` | ❌ | Issuer invoice number when `state` is `invoiced` | | `original_amount` | ✅ | Amount at point of purchase | | `original_currency` | ✅ | ISO 4217 (3 letters) | | `accounting_amount` | ✅ | Amount in cardholder billing currency | | `accounting_currency` | ✅ | ISO 4217 (3 letters) | | `exchange_rate` | ✅ | Rate between original and accounting amounts | | `vat_rate` | ❌ | VAT rate at merchant | | `vat_amount` | ❌ | VAT amount | | `vat_currency` | ❌ | ISO 4217 for VAT | | `purchase_merchant` | ✅ | Merchant name | | `purchase_time` | ✅ | ISO 8601 UTC, e.g. `2025-11-17T15:35:11Z` | | `purchase_country` | ✅ | ISO 3166-1 alpha-2 | | `purchase_city` | ❌ | City | | `mcc_code` | ✅ | Merchant Category Code | | `merchant_number` | ✅ | ISO 8583 field 42 | | `terminal_id` | ✅ | ISO 8583 field 41 | | `rrn` | ✅ | ISO 8583 field 37 (retrieval reference number) | | `auth_code` | ✅ | ISO 8583 field 38 | *** ## States | State | When to send | | - | - | | `authorized` | Once, when the purchase is authorized — see below | | `cleared` | When the transaction settles | | `invoiced` | When included on the cardholder's issuer invoice (credit products only) | | `deleted` | When an authorization is reversed before clearing | ### `authorized` — one event per purchase Send **one** `authorized` state per purchase: the final authorization at the amount the cardholder approved. This is what EMS apps show the cardholder in real time. Do **not** send any of these as separate states: | Do not send | Why | | - | - | | `pre-authorized` / pre-auth holds | Hotel and fuel holds are not purchases. Wait for the actual authorization | | Incremental authorizations | Amount top-ups on an open authorization — fold them into the final `authorized` or into `cleared` | | Authorization adjustments / reversals of a partial amount | Send `cleared` with the final amount instead. Use `deleted` only when the whole authorization is reversed | If your systems produce several authorization messages for one purchase, collapse them to a single `authorized` state with the same transaction `id`, then continue the lifecycle with `cleared`. Use the **same `id`** for all states belonging to one purchase. EMS apps upsert on OpenCard's internal id after ingestion — your `id` is the issuer reference across states. *** ## Errors | HTTP | `error` | Meaning | | - | - | - | | `400` | `card_not_found` | No card registered for `{card_id}` | | `409` | `duplicate_transaction_state` | Same `id` + `state` already received | | `422` | `invalid_purchase_time` | `purchase_time` not valid ISO 8601 | | `422` | `card_missing_party` | Card missing buyer or seller party | *** ## What happens next OpenCard normalizes the payload, links it to the card and TPA, and forwards events to EMS webhooks. Optional enrichment (receipts, true VAT, line items) may follow as separate webhook events at the EMS — you do not implement that layer as an issuer. # Card Holder Onboarding Source: https://developers.opencard.io/ems/card-holders Onboard card holders to your EMS via two paths — email plus eID PDPC signing, or instant creation with identity_id for people already known to OpenCard. 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. Card holders come **after** client setup (billing, TPA, organization, webhook). See the [full onboarding flow](/ems/customer-onboarding) if you have not finished Phase 1 yet. *** ## At a glance | | 🅰️ Email + eID | 🅱️ `identity_id` (instant) | | - | - | - | | **Best for** | New employees, self-service | Person already known to OpenCard | | **You provide** | `email` | `identity_id` (from identities API) | | **User action** | Click email → sign PDPC with eID | None — instant | | **Transactions start** | After user completes eID | **Immediately** on create | | **Retroactive txs** | Yes, after eID sign | Yes, dispatched on create | | **Typical UX** | "Check your email to activate" | Bulk import from employee list | | **Client already has issuer feed?** | → [Current card issuer integration](/ems/current-card-issuer-integration) | Same | ```mermaid theme={null} sequenceDiagram participant You as Your EMS participant OC as OpenCard participant User as Card holder You->>OC: POST cardholder (email) OC->>User: PDPC email User->>OC: Sign PDPC with eID OC->>You: card_holder.identified OC->>You: card.transaction.* ``` ```mermaid theme={null} sequenceDiagram participant You as Your EMS participant OC as OpenCard You->>OC: GET .../tpas/{id}/identities You->>OC: POST cardholder (identity_id) OC->>You: card_holder.identified OC->>You: Retroactive card.transaction.* OC->>You: Ongoing card.transaction.* ``` *** ## 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 ``` ```json theme={null} { "reference_id": "employee_john_42", "email": "john@acme.se", "language": "sv" } ``` | Field | Required | Notes | | - | - | - | | `reference_id` | ✅ | Your internal user ID | | `email` | ✅ | Where PDPC signing link is sent | | `language` | ❌ | PDPC legal text: `sv`, `no`, `da`, `en`, `fi` | | `skip_pdpc_email` | ❌ | Default `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](/ems/eid-signing) *** ## 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 param | Values | Meaning | | - | - | - | | `is_card_holder` | `true` | Only identities that already have card holders | | `is_card_holder` | `false` | Only identities **without** card holders — good for finding who you still need to onboard | | *(omit)* | — | All identities on this TPA | **Response (per identity):** ```json theme={null} { "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" } } ] } ``` | Field | What it tells you | | - | - | | `id` | **Use this as `identity_id`** when creating card holder | | `name` | Person's name from eID | | `employee_id` | Employee ID at client (if set) | | `cards` | Cards already linked to this person on this TPA | | `card_holders` | Existing card holder records (maybe in other orgs) | ### Step 2: Create card holder with `identity_id` ```json theme={null} { "reference_id": "employee_anna_07", "identity_id": 123, "skip_pdpc_email": true } ``` | Field | Required | Notes | | - | - | - | | `reference_id` | ✅ | Your internal user ID | | `identity_id` | ✅ | From identities list. Must belong to same client as TPA. | | `email` | ❌ | Not needed when `identity_id` is set | | `skip_pdpc_email` | recommended `true` | Person 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) * `reference_id` must be unique within the organization (create and update) *** ## Which path should I use? Two ways to link a person in your EMS to card spend in OpenCard: | | 🅱️ Match on employee number | 🅰️ Email + eID | | - | - | - | | **How** | Map the issuer’s employee number to yours → create with `identity_id` | Create with `email` + your `reference_id` | | **User action** | None | Opens OpenCard email and identifies with **eID** | | **You need** | An up-to-date employee register in **both** places (issuer + your EMS) | A working email address for the person | | **Transactions** | Start as soon as you create the card holder | Start only **after** they complete eID | **Choose 🅱️** when you can reliably match people across systems — bulk onboard, no inbox friction, but the two registers must stay in sync. **Choose 🅰️** when you want the person to prove who they are before you receive their transactions — self-service activate flows, new hires without a shared employee id, or when you don’t maintain dual registers. *** Client already receives transactions from the issuer another way? See [Current card issuer integration](/ems/current-card-issuer-integration). ## Webhooks to handle | Event | Path 🅰️ | Path 🅱️ | | - | - | - | | `card_holder.created` | ✅ on create | ✅ on create | | `card_holder.identified` | ✅ after eID sign | ✅ **immediately** on create | | `card_holder.signed.pdpc` | ✅ after eID sign | May not fire (already identified) | | `card.transaction.*` | After identified | **Immediately** (+ retroactive batch) | **Your integration should key off `card_holder.identified`** as the moment transactions are expected. *** ## Update / delete ``` GET .../cardholders ← list (includes identity name + employee_id) PUT .../cardholders/{cardHolderId} ← update reference_id and/or email DELETE .../cardholders/{cardHolderId} ← fires card_holder.deleted Scope: card-holders-write (PUT), card-holders-delete (DELETE) ``` ### Update `reference_id` is **required**. `email` is **optional** — omit it to keep the current value (including `null` for identity-linked holders without email). **Change reference ID only** (no PDPC email if already identified / no email on record): ```json theme={null} PUT .../cardholders/15 { "reference_id": "employee_john_99" } ``` **Change reference ID and email** (may resend PDPC email — see below): ```json theme={null} { "reference_id": "employee_john_42", "email": "john.newemail@acme.se", "skip_pdpc_email": false, "language": "sv" } ``` | Field | Required | Notes | | - | - | - | | `reference_id` | ✅ | Your internal user ID | | `email` | ❌ | Only sent when you want to change it | | `skip_pdpc_email` | ❌ | Default unchanged if omitted | | `language` | ❌ | PDPC mail language if a resend is triggered | PDPC email is resent only when **all** of: PDPC unsigned or no `identity_id`, `skip_pdpc_email` is false, and the card holder has an email address. `reference_id` must be unique within the organization. Duplicate → `400 Bad Request` with message `Card holder reference … already exists` (same as create). *** ## List card holders ``` GET /accounts/{accountId}/organizations/{organizationId}/cardholders Scope: card-holders-read ``` Supports the same filters as before (`q`, `reference_id`, `where_ssn_set`, `where_signed_pdpc`, `where_email_delivered`). **Response (per item):** ```json theme={null} { "id": 15, "reference_id": "employee_john_42", "organization_id": 3, "identity_id": 123, "email": "john@acme.se", "created_at": "2026-06-08T10:00:00.000000Z", "updated_at": "2026-06-08T10:30:00.000000Z", "meta": { "ssn": true, "signed": true, "signed_at": "2026-06-08T10:30:00.000000Z", "email_status": "delivered", "pdpc_url": "https://api.opencard.io/accounts/1/pdpcs/8/sign/abc...", "system": "Acme EMS", "organization_number": "5561234567" }, "identity": { "name": "Anna Andersson", "employee_id": "001" } } ``` | Field | What it tells you | | - | - | | `identity_id` | OpenCard identity ID when linked (null before eID sign on Path 🅰️) | | `identity.name` | Person name from eID — use for display in your UI | | `identity.employee_id` | Employee ID on the TPA client (if set on `client_identity`) | | `identity` | `null` when not yet linked to an identity | You no longer need a separate `GET .../tpas/{tpaId}/identities` call just to show names in a card holder list — read `identity` from this endpoint. Keep the identities endpoint for Path 🅱️ onboarding (finding `identity_id` to create with). *** ## `meta` in API responses Check consent status without polling webhooks: ```json theme={null} { "id": 7, "reference_id": "employee_john_42", "meta": { "ssn": true, "signed": true, "signed_at": "2026-06-08T10:30:00Z", "email_status": "delivered", "pdpc_url": "https://api.opencard.io/accounts/1/pdpcs/3/sign/abc...", "system": "Acme EMS", "organization_number": "5561234567" }, "identity": { "name": "John Doe", "employee_id": "042" } } ``` # Current Card Issuer Integration Source: https://developers.opencard.io/ems/current-card-issuer-integration Integrate OpenCard when a client already has another card issuer feed — retroactive replay and deduplicating cleared transaction events. Your client may already receive card transactions from the same card program through a direct issuer integration or another middleware. That is common during rollout. *** ## What to expect with two feeds | | Existing integration | OpenCard | | - | - | - | | **Typical states** | Often `cleared` only | `authorized` → `cleared` → `invoiced` | | **Starts per card holder** | Varies | On `card_holder.identified` | **Authorized events are OpenCard-specific.** Many legacy feeds never send authorisations. You will see `card.transaction.authorized` from OpenCard that have no counterpart on the old feed — use them for in-app previews, not for deduplication against the other source. **Retroactive batch on onboard.** When `card_holder.identified` fires, OpenCard replays transaction states since the latest card invoice for that person's cards on the TPA. The same cleared purchases may already exist in your system from the other integration. → [Card holder onboarding](/ems/card-holders) (Path 🅰️ and Path 🅱️) *** ## Deduplicating with `card_issuer_reference` The practical way to run both feeds in parallel: 1. Start processing OpenCard per card holder when **`card_holder.identified`** arrives. 2. **Upsert on OpenCard transaction `id`** as your primary key for the OpenCard feed. 3. On **`card.transaction.cleared`**, check whether you already stored the purchase from the other feed using **`card_issuer_reference`** on the cleared payload. **Cleared matches cleared** — compare `card_issuer_reference` only on `card.transaction.cleared`, not on `authorized`. The same purchase can carry **different** `card_issuer_reference` values on `authorized` vs `cleared`; that depends on the card issuer and OpenCard cannot normalize it. 4. To end the old feed, your **client** terminates it with their **card issuer**. Agree with your client when to stop processing the other integration on your side. → [Transaction webhook upsert rules](/ems/webhooks/transactions) # Overview Source: https://developers.opencard.io/ems/customer-onboarding Full EMS customer onboarding in the order the API expects — billing, TPA, organization, webhook, card holders — with the id each step produces and the API call each step makes. This is the **end-to-end path** for connecting one of your clients to OpenCard. Everything lives in the Application API — no embed plugin required. If you just want to run it once with curl, start with the [Quickstart](/getting-started/quickstart). This page is the same flow written as an integration guide: what to build in your product, which call to make, what to store, and what breaks if a step is skipped. *** ## Who does what | Actor | Role | | - | - | | **Client** | Your end-customer company — signs the TPA; their employees sign PDPC | | **You (EMS)** | Orchestrate setup in your product — call OpenCard APIs, store ids and `reference_id`s, show status in your UI | | **OpenCard** | Legal signing (eID), card issuer communication, webhooks with transactions and enrichment | You own the UX. OpenCard owns signing pages, identity verification, and data delivery. *** ## The order matters Each object references the one before it. Create them in this order and store the id you get back. ```mermaid theme={null} flowchart TD PP["0 · Payment product
once per account
POST /accounts/{a}/payment-products/{id}"] B["1 · Billing
POST /accounts/{a}/billings
→ store billing_id"] PR["2 · Public records
GET /accounts/{a}/publicrecords
→ who may sign"] T["3 · TPA
POST /accounts/{a}/tpas
→ store tpa_id"] S["4 · Signatories
POST /tpas/{tpa_id}/signatories
→ signing email sent"] O["5 · Organization
POST /accounts/{a}/organizations
needs tpa_id + billing_id
→ store organization_id"] W["6 · Webhook
POST /organizations/{org}/webhooks
→ challenge → active"] SG["7 · tpa.signed
arrives on your webhook"] CH["8 · Card holders
POST /organizations/{org}/cardholders
→ PDPC → card_holder.identified"] TX["9 · Transactions
webhooks flow"] PP -->|payment_product_id| T B -->|billing_id| O PR -.->|signatories| S T -->|tpa_id| S T -->|tpa_id| O O -->|organization_id| W O -->|organization_id| CH W --> SG S -.->|client signs with eID| SG SG --> CH W --> CH CH --> TX ``` Solid arrows are hard dependencies (the call fails or misbehaves without the id). Dotted arrows are information flow. *** ## Step by step Steps 0 is once per account. Steps 1–8 are repeated **per client**. Pick which card products your clients can choose from and enable them on your account. `GET /payment-products` → `POST /accounts/{accountId}/payment-products/{paymentProductId}` **Store:** `payment_product_id` — used on the TPA. → [Payment product setup](/ems/payment-products-setup) · API: [List available](/api-reference/ems/payment-products/list-available-payment-products) · [Enable](/api-reference/ems/payment-products/enable-payment-product) The client's invoice profile **and** the product switch. `product_transaction`, `product_digital_receipt` and `product_aland_index` decide which webhook events the client's organizations may subscribe to. `POST /accounts/{accountId}/billings` **Store:** `id` as `billing_id`. **If skipped:** the organization has no `billing_id`, and creating a webhook with transaction or receipt events fails — the webhook is validated against the billing's product flags. → [Organization setup](/ems/organization-setup) · [Model: Billing](/ems/model/billing) · API: [Create billing profile](/api-reference/ems/billings/create-billing-profile) Ask the public registry for the company's signing combinations so you can present the right signatories to the client. `GET /accounts/{accountId}/publicrecords?country=SE&organization_number=…` → [TPA flow — check who can sign](/ems/tpa-flow#step-2-check-who-can-sign-recommended) · API: [Lookup company signing combinations](/api-reference/ems/public-records/lookup-company-signing-combinations) The legal agreement between the client and the card issuer that allows transaction data to flow. `POST /accounts/{accountId}/tpas` with `payment_product_id`, `country`, `organization_number`, `language` **Store:** `id` as `tpa_id`. → [TPA flow](/ems/tpa-flow) · [Model: TPA](/ems/model/tpa) · API: [Create TPA](/api-reference/ems/tpas/create-tpa) One call per person who must sign. The email with the eID signing link is queued immediately. `POST /accounts/{accountId}/tpas/{tpaId}/signatories` Don't wait for the signature — continue with steps 5 and 6 right away so the webhook exists when `tpa.signed` fires. → [TPA flow — signatories](/ems/tpa-flow) · API: [Add signatory](/api-reference/ems/tpa-signatories/add-signatory-sends-email) The runtime container for the client. Links the TPA (legal) and the billing (products) together, and carries your `reference_id` into every webhook. `POST /accounts/{accountId}/organizations` with `reference_id`, `tpa_id`, `billing_id`, and all three `process_card_type_*` flags set to `true` **Store:** `id` as `organization_id`. **If the card type flags are left off:** they default to `false`, and no transactions are delivered for that organization. **If `billing_id` is skipped:** webhook creation with transaction events fails (see step 1). → [Organization setup](/ems/organization-setup) · [Model: Organization](/ems/model/organization) · API: [Create organization](/api-reference/ems/organizations/create-organization) Where OpenCard delivers events. Creation triggers a challenge request to your URL; once your endpoint answers correctly the webhook becomes `active`. `POST /accounts/{accountId}/organizations/{organizationId}/webhooks` with `url` + event flags **Verify:** `active: true` on `GET .../webhooks`. **If skipped:** card holder lifecycle and PDPC signing write events to the organization's webhook — without one, PDPC signing cannot complete. This is the single most common onboarding mistake. → [Organization setup](/ems/organization-setup) · [Webhook setup](/ems/webhooks/setup) · [Events](/ems/webhooks/events) · API: [Create webhook](/api-reference/ems/webhooks/create-webhook) The client's signatories sign with eID. When all required signatures are in, OpenCard generates the signed PDF and POSTs `tpa.signed` to your webhook — your first real event for this client. The TPA moves to `pending-activation` and then `activated`. Card holders can be created before activation, but transactions only flow once the TPA is activated. → [eID signing](/ems/eid-signing) · [TPA lifecycle](/ems/tpa-flow#tpa-lifecycle-states) · API: [Get TPA](/api-reference/ems/tpas/get-tpa) One per employee. **Email path** sends a PDPC consent + eID; **instant path** links an existing `identity_id` with no user action. `POST /accounts/{accountId}/organizations/{organizationId}/cardholders` **Verify:** `card_holder.identified` arrives on your webhook. That is the signal that transactions are coming (including a retroactive batch). → [Card holder onboarding](/ems/card-holders) · [Model: Card holder](/ems/model/card-holder) · API: [Create card holder](/api-reference/ems/card-holders/create-card-holder) · [List identities on TPA](/api-reference/ems/identities/list-identities-on-tpa) After that you are live: the card issuer pushes transaction states → OpenCard POSTs to your webhook → enrichment may follow. → [Transaction states](/ems/webhooks/transactions) · [Receipts](/ems/receipts) *** ## Pre-flight check per client Before you add the first card holder for a client, all of these must be true: * [ ] Billing exists with the right `product_*` flags, and the organization's `billing_id` points to it * [ ] TPA exists and the organization's `tpa_id` points to it * [ ] Organization exists with your `reference_id` * [ ] Webhook exists under the organization and is `active` * [ ] TPA is `signed` (and `activated` for live transactions) Then, and only then: card holders. *** ## Build it yourself vs embed plugin | Approach | When | | - | - | | **API only (this guide)** | Rebuild the steps in your own UI — recommended for production | | **[ocTPA plugin](/ems/plugins)** | Ready-made wizard for steps 1–4 (payment product → products → invoice → signatories). Hands you the payload in `onDataSend`; **you still POST** billing, TPA and signatories | The plugin stops at the TPA. Organization (step 5), webhook (step 6) and card holders (step 8) are always your integration's responsibility — there is nothing in the plugin payload for them, because the webhook URL is *your* endpoint, not client input. *** ## Deep dives in this section | Page | What it covers | | - | - | | [Payment product setup](/ems/payment-products-setup) | Enable products, marketplace UI, `payment_product_id` | | [TPA flow](/ems/tpa-flow) | Create TPA, signatories, activation, signed PDF | | [Organization setup](/ems/organization-setup) | Billing, organization, webhook — the three calls every client needs before card holders | | [eID signing](/ems/eid-signing) | Sign vs auth modes across Nordic countries | | [Card holder onboarding](/ems/card-holders) | Email vs `identity_id`, webhooks, updates | **Models:** [Billing](/ems/model/billing) · [Organization](/ems/model/organization) · [TPA](/ems/model/tpa) · [Card holder](/ems/model/card-holder) # eID Signing — 4 Countries Source: https://developers.opencard.io/ems/eid-signing How OpenCard handles TPA and PDPC eID signing — sign and auth modes using national eID across Sweden, Norway, Denmark, and Finland. OpenCard uses **national eID** for electronic identity and signing across all four Nordic countries. Used in customer onboarding for **TPA** (company signatories) and **PDPC** (card holders). → [Full onboarding flow](/ems/customer-onboarding) Two modes: | Mode | Used for | What happens | | - | - | - | | **`sign`** | TPA signing | User signs a specific legal document (hash-bound) | | **`auth`** | PDPC consent | User identifies themselves + checks consent box | *** ## Countries | Country | Supported | | - | - | | 🇸🇪 Sweden | ✅ | | 🇳🇴 Norway | ✅ | | 🇩🇰 Denmark | ✅ | | 🇫🇮 Finland | ✅ | Legal text and the signing UI follow the country on the TPA or PDPC. In Finland the user may choose between available national eID options. *** ## How the token flow works ```mermaid theme={null} sequenceDiagram participant User as Signatory/Cardholder participant Page as OpenCard signing page participant eID as National eID participant API as OpenCard API User->>Page: Click email link (token in URL) Page->>eID: Load eID widget eID->>User: Authenticate with national eID eID->>Page: Identity result Page->>API: POST /sign/{token} API->>User: Redirect back with success ``` The signatory or card holder never needs an OpenCard account. The token in the email link is their authentication. *** ## TPA signing page **URL:** `GET /accounts/{accountId}/tpas/{tpaId}/sign/{token}` **What the user sees:** 1. TPA legal text (rendered markdown) 2. "Sign" button → eID widget 3. National eID provider for the company's country 4. Document signing (legal text is bound to the signature) On success the signatory is marked signed and OpenCard checks whether the TPA is fully signed. Full TPA flow → [TPA Flow](/ems/tpa-flow) *** ## PDPC signing page **URL:** `GET /accounts/{accountId}/pdpcs/{pdpcId}/sign/{token}` **What the user sees:** 1. PDPC legal text 2. Checkbox: "I have read the text above" 3. "Approve & Identify" button 4. eID widget in **`auth`** mode (identity only, no document hash) On success: * Identity created or matched * Card holder linked * PDPC marked signed * Signed PDF generated and emailed to the cardholder * `card_holder.signed.pdpc` webhook fires Full card holder flow → [Card Holder Onboarding](/ems/card-holders) *** ## Supported languages for legal text Templates available in: `sv`, `no`, `da`, `en`, `fi` Query available languages: ``` GET /api/v1/application/open/legaltexts?type=tpa GET /api/v1/application/open/legaltexts?type=pdpc ``` No auth required for this endpoint. # Environmental Impact Source: https://developers.opencard.io/ems/environmental-impact Subscribe to OpenCard environmental impact webhooks for per-transaction CO₂ emissions and carbon social cost data delivered alongside card transaction events. Subscribe to environmental impact webhooks per organization. Two optional providers — enable the flag that matches your OpenCard contract: | Flag | Event | | - | - | | `aland_index` | `environmental.impact.aland_index` | | `deedster` | `environmental.impact.deedster` | Both return the same payload shape: ```json theme={null} { "carbon_emission_in_grams": 0.66, "carbon_social_cost_amount": 2.1, "carbon_social_cost_currency": "SEK", "transaction": { "id": "10", "card_issuer": "nordea" }, "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "organization": { "reference_id": "client_acme_001" }, "card_holder": { "reference_id": "employee_john_42" } } ``` Also available via receipt scanner when `include: ["environmental_impact"]`. # Error Handling Source: https://developers.opencard.io/ems/error-handling Reference for OpenCard HTTP status codes, validation errors, webhook delivery retries, and common integration gotchas when building on the EMS API. ## API errors | Code | Meaning | Fix | | - | - | - | | `400` | Bad request / validation | Check field formats (org numbers, country codes) | | `401` | Invalid token | Refresh OAuth token | | `403` | Missing scope | Add scope to OAuth client | | `404` | Not found | Check IDs, or no access | | `409` | Conflict | Duplicate (e.g. issuer transaction state ID) | | `422` | Business rule | e.g. delete signed signatory | *** ## Webhook delivery * OpenCard retries failed deliveries * Check delivery log: `GET .../webhooks/{id}/events` * Your endpoint must return `200` fast * Process heavy work async (queue it) *** ## Common gotchas | Issue | Cause | Fix | | - | - | - | | HTML login page instead of JSON error | Missing `Accept: application/json` | Send `Accept: application/json` on every API call | | Webhook `active=false` | Challenge failed | Implement HMAC challenge handler | | No transactions | TPA not activated | Wait for issuer card confirmation | | No transactions | PDPC not signed | Cardholder must sign consent | | Duplicate txs | Same transaction received twice | Upsert by transaction `id` | | Wrong VAT on tx | Network VAT, not merchant | Subscribe to `transaction.true.vat` | # Overview Source: https://developers.opencard.io/ems/index How Expense Management Systems integrate with OpenCard — receive real-time card data via webhooks, and build customer onboarding for payment products, TPA, and card holders. This section is for **Expense Management Systems (EMS)** — the apps that show employees and finance teams their card spend. OpenCard sits between **card issuers** and **you**. Issuers send card and transaction data to OpenCard. You receive it as webhooks, enriched with digital receipts, VAT, line items, and more when available. Your job has two halves. Both matter — and both should live **in your product**, not only in OpenCard's UI. *** ## What you integrate ```mermaid theme={null} flowchart LR Issuer[Card issuer] -->|cards + txs| OC[OpenCard] You[Your EMS] -->|onboard clients| OC OC -->|webhooks| You You -->|spend UI| User[Your customers] ``` | Half | You build | OpenCard does | | - | - | - | | **Receive data** | HTTPS endpoint + auth + challenge | POSTs transactions, TPA events, card-holder events, enrichment | | **Onboard customers** | Marketplace, TPA flow, card-holder maintenance in your UX | eID signing, issuer handoff, matching, webhook delivery | We recommend you **build the integration** end to end. OpenCard's application and optional [plugins](/ems/plugins) exist as shortcuts — the durable path is your own API integration. *** ## 1. Receive data — webhooks first Before a single client is live, decide **where** OpenCard should send events and **which** events you want. 1. Stand up an HTTPS endpoint that accepts POSTs from OpenCard 2. Choose a **security scheme** (`basic`, `oauth`, `custom`, or `none`) so only OpenCard can call you 3. Complete the **webhook challenge** (HMAC handshake) so OpenCard knows you own the URL 4. Subscribe to the events you need — transactions, TPA signed, card holder identified, receipts, TrueVAT, line items, … Every organization has its own webhook config. Transaction data arrives here — there is no “list all transactions” pull API for day-to-day spend. | Guide | What it covers | | - | - | | [Webhook setup](/ems/webhooks/setup) | Create webhook, auth, **challenge**, event flags | | [Webhook events](/ems/webhooks/events) | Full catalog + payload samples | | [Transactions](/ems/webhooks/transactions) | States, `receiptable`, how to apply updates | | [Authentication](/getting-started/authentication) | OAuth client credentials for your outbound API calls | *** ## 2. Onboard customers — in your product Once webhooks work, wire **client onboarding** into your EMS. That is the path we want partners on. ### Marketplace — payment products Show the cards you sell. Clients pick a **payment product** before legal setup. → [Payment product setup](/ems/payment-products-setup) ### TPA — company authorization Create the **Transaction Processing Authorization**, look up who may sign via **public records**, collect signatory emails, and let OpenCard run eID signing. You get `tpa.signed` when it is done. → [Customer onboarding](/ems/customer-onboarding) · [TPA flow](/ems/tpa-flow) · [eID signing](/ems/eid-signing) ### Card holders — keep people in sync Create and maintain **card holders** for each employee whose spend should appear. Email + PDPC, or instant match with `identity_id` when the person is already known to OpenCard. → [Card holders](/ems/card-holders) *** ## Recommended order | Step | Goal | Start here | | - | - | - | | **A** | Sandbox account + OAuth client | [Register](/getting-started/register) · [Quickstart](/getting-started/quickstart) | | **B** | Webhook URL, auth, challenge, event flags | [Webhook setup](/ems/webhooks/setup) | | **C** | Enable payment products on your account | [Payment product setup](/ems/payment-products-setup) | | **D** | One test client: billing → TPA → org → webhook | [Customer onboarding](/ems/customer-onboarding) | | **E** | Card holders → first live / sandbox transactions | [Card holders](/ems/card-holders) | | **F** | Enrichment (receipts, environment) as needed | [Digital receipts](/ems/receipts) · [Environmental impact](/ems/environmental-impact) | *** ## The model (why the entities exist) You will create **billing**, **organization**, **TPA**, and **card holder** records while onboarding. They are not abstract — each maps to invoice roll-up, a client company, legal permission, or an employee. | Concept | Role | | - | - | | [Billing](/ems/model/billing) | Who OpenCard invoices **you** for; orgs with the same `billing_id` roll up as one line | | [Organization](/ems/model/organization) | One end-client (your customer company) | | [TPA](/ems/model/tpa) | Company-level permission for card data to flow to your EMS | | [Card holder](/ems/model/card-holder) | One person whose transactions you receive | Deep hierarchy context → [How OpenCard works](/introduction/how-it-works) *** ## Map of this section End-to-end client setup — marketplace, TPA, public records, card holders, then live webhooks. Endpoint, security scheme, challenge, and every event you can subscribe to. Billing, organization, TPA, and card holder — the objects your API calls create. Application API reference — payment products, public records, TPAs, orgs, webhooks. Also useful: [Current card issuer integration](/ems/current-card-issuer-integration) if the client already has an issuer feed, [Error handling](/ems/error-handling), and [Plugins](/ems/plugins) if you temporarily use OpenCard-hosted UI widgets. # Billing Source: https://developers.opencard.io/ems/model/billing The OpenCard Billing model — invoice recipient for your EMS account, and how organizations with the same billing_id roll up as one line on that invoice. **Billing** is not about your end-clients paying their card bills. It is how **OpenCard invoices you** — the EMS — for platform usage. A billing profile is the **invoice recipient** (legal entity, address, email, your reference) and the bucket organizations link to via `billing_id`. Organizations that share the same `billing_id` are **rolled up as one line** on your OpenCard invoice — not billed as separate invoices per organization. *** ## What it represents | Concept | Meaning | | - | - | | **Who gets the invoice** | Your finance team (`email_invoice`, legal name, org number) | | **Same `billing_id`** | Those organizations’ usage appears as **one line** on the invoice | | **What it does not control** | Card issuer invoices to cardholders, webhooks, or transaction routing | *** ## How it fits ``` Account (you) └── Billing profile ├── Organization A ─┐ ├── Organization B ─┼─ same billing_id → one line on your OpenCard invoice └── Organization C ─┘ ``` Each organization still has its own webhooks, card holders, and transactions. Billing only controls **invoice recipient** and **how usage is aggregated onto a line** on the invoice OpenCard sends you. *** ## Common setups | Your situation | Billing setup | | - | - | | One org per client, each as its own invoice line | One billing profile per organization | | Several orgs that should appear as a single line | One billing profile shared by those orgs | | Large client with many orgs, one roll-up line | One billing profile for all their orgs | *** ## Key fields | Field | Purpose | | - | - | | `id` | Use as `billing_id` when creating organizations | | `name_legal` | Legal entity on the OpenCard invoice | | `organization_number` | Company registration number | | `email_invoice` | Where OpenCard sends your invoice | | `your_reference_invoice` | Your PO or reference on the invoice | | `product_transaction` | Real-time transactions enabled for orgs on this billing | | `product_digital_receipt` | Digital receipt enrichment | | `product_aland_index` | Environmental impact enrichment | *** ## Create and link ``` POST /accounts/{accountId}/billings Scope: billings-write POST /accounts/{accountId}/organizations Set billing_id on create ``` ```json theme={null} { "name_legal": "Acme Expense Group AB", "organization_number": "5561234567", "country": "SE", "email_invoice": "finance@acme-ems.se", "your_reference_invoice": "PO-12345", "product_transaction": true, "product_digital_receipt": true, "product_aland_index": false } ``` When creating an organization, set `"billing_id": 1` (or whichever profile applies). Multiple organizations with the **same** `billing_id` share **one line** on your OpenCard invoice. Product flags are what the client chooses in onboarding (“transactions” vs optional enrichment). → [Customer onboarding](/ems/customer-onboarding) Create billing during [client setup](/ems/customer-onboarding#step-by-step), together with the TPA. An optional [ocTPA plugin](/ems/plugins) collects the same fields — you still POST this API. # Card holder Source: https://developers.opencard.io/ems/model/card-holder The OpenCard Card holder model — an individual in an organization, linking cards to identities and driving transaction and receipt webhooks to your EMS. A **card holder** is one person in one [organization](/ems/model/organization) whose card transactions you want in your EMS. You create the record via API. OpenCard links it to a real person (identity) and, once identified, starts delivering that person's card events to the organization's webhook. You set `reference_id` to your internal user ID — it appears on every transaction webhook for that person. *** ## What it represents | Concept | Meaning | | - | - | | **Who** | An employee or card user at your client's company | | **`reference_id`** | Your string ID for that person — in every webhook | | **`organization_id`** | Which client they belong to | | **`identity_id`** | Link to the person in OpenCard (after eID, or immediately on instant onboard) | | **PDPC** | Individual GDPR consent — required unless onboarded via `identity_id` | A card holder is not the physical card. Cards are managed by the [card issuer](/ems/model/tpa) and appear on the identity once the TPA is activated. *** ## When transactions start | Path | How | Transactions begin | | - | - | - | | 🅰️ Email + eID | You send `email`, user signs PDPC | After `card_holder.identified` | | 🅱️ `identity_id` | Person already known to OpenCard | Immediately on create | On identification, OpenCard replays transaction states since the latest card invoice for that person's cards, then continues in real time. → [Card holder onboarding](/ems/card-holders) *** ## Key fields | Field | Purpose | | - | - | | `id` | OpenCard card holder ID | | `reference_id` | **Your** user ID (required, unique per organization) | | `organization_id` | Parent organization | | `identity_id` | Linked person (null until eID on Path 🅰️) | | `email` | For PDPC signing link (Path 🅰️) | | `meta.signed`, `meta.pdpc_url` | PDPC status and signing URL | *** ## Webhook signal Subscribe to **`card_holder.identified`** — that is when you should expect transactions (including any retroactive batch) for that person. ``` POST /accounts/{accountId}/organizations/{organizationId}/cardholders Scope: card-holders-write ``` → [Card holder onboarding — paths and API](/ems/card-holders) · [Current card issuer integration](/ems/current-card-issuer-integration) # Organization Source: https://developers.opencard.io/ems/model/organization The OpenCard Organization model — your end-client company, the container that groups card holders, webhooks, TPA linkage, and billing for EMS partners. An **organization** is one of your EMS customers — a company whose card program you integrate through OpenCard. It is the main **runtime container**: webhooks, card holders, and event routing all live under an organization. You identify it in every webhook with `reference_id`, which you set to your own internal client ID. *** ## What it represents | Concept | Meaning | | - | - | | **Your client** | One end-customer (usually one company) | | **`reference_id`** | Your string ID — echoed in all webhooks for that client | | **`tpa_id`** | Which signed legal agreement covers this client's card program | | **`billing_id`** | Which [billing profile](/ems/model/billing) this org rolls into — same id = **one line** on your OpenCard invoice | One organization typically maps to one client. Split into multiple organizations only when you need separate webhook endpoints, event subscriptions, or card-holder groups for the same client. *** ## What lives under an organization ``` Organization ├── Webhook ← where OpenCard POSTs events ├── Card holders ← people whose transactions you receive └── (via TPA) ← which payment product and legal scope apply ``` | Area | What you configure | | - | - | | **Connection** | Webhook URL, secret, auth, event flags | | **People** | Card holders with your `reference_id` per employee | | **Legal scope** | Linked TPA — must be signed and activated for data to flow | *** ## Key fields | Field | Purpose | | - | - | | `id` | OpenCard organization ID | | `reference_id` | **Your** client ID (required, unique per account) | | `tpa_id` | TPA on your account (signed and activated for live data) | | `billing_id` | Invoice line roll-up — see [Billing](/ems/model/billing) | | `name` | Display name | | `process_card_type_corporate` | Deliver transactions for corporate cards invoiced to the company. Default `false` | | `process_card_type_corporate_with_private_invoice` | Deliver transactions for corporate cards invoiced to the card holder. Default `false` | | `process_card_type_private` | Deliver transactions for private cards invoiced to the card holder. Default `false` | *** ## Create ``` POST /accounts/{accountId}/organizations Scope: organizations-write ``` ```json theme={null} { "reference_id": "client_acme_001", "tpa_id": 42, "billing_id": 1, "name": "Acme AB", "process_card_type_corporate": true, "process_card_type_corporate_with_private_invoice": true, "process_card_type_private": true } ``` Set all three `process_card_type_*` flags to `true` so transactions are delivered, then turn a type off if the client should not receive it. OpenCard only sends transaction events for the types that are on. → [Organization setup — card types](/ems/organization-setup#card-types) ``` GET / PUT / DELETE .../organizations/{organizationId} ``` Delete cascades webhooks and card holders. Use with care on production orgs. # TPA Source: https://developers.opencard.io/ems/model/tpa The TPA model — Transaction Processing Authorization, the company-level legal agreement that allows card data to flow to your EMS via OpenCard. **TPA** = **Transaction Processing Authorization**. It is the legal agreement between a **client company** and OpenCard that card transaction data from a specific **payment product** may flow to your EMS. Without a signed and activated TPA, no transactions are delivered for that client and product combination. *** ## What it represents | Concept | Meaning | | - | - | | **Who consents** | The client company — authorized signatories (CEO, board, etc.) | | **What they approve** | Card data from a given payment product may pass through OpenCard to you | | **Scope** | One TPA per **client company + payment product** pair | | **Audience** | Company level — not the individual cardholder | The individual employee signs a separate document — [PDPC](/ems/eid-signing) — when they onboard as a [card holder](/ems/model/card-holder). TPA is the company saying yes; PDPC is the person saying yes. *** ## Lifecycle | Status | Meaning | | - | - | | `pending-signatures` | Signatories added, not all signed | | `pending-activation` | Waiting for TPA to be confirmed | | `activated` | Live — transactions can flow | ```mermaid theme={null} stateDiagram-v2 [*] --> pending-signatures: create TPA pending-signatures --> pending-activation: all signed pending-activation --> activated: TPA confirmed ``` *** ## What is attached to a TPA * **Signatories** — people who receive email and sign with eID * **Legal text** — snapshot at creation * **Signed PDF** — generated when signing is complete * **Organizations** — your runtime orgs link here via `tpa_id` *** ## Key fields | Field | Purpose | | - | - | | `id` | Use when creating organizations (`tpa_id`) | | `payment_product_id` | Which payment product the client signed for | | `name`, `country`, `organization_number` | Client company identity | | `activated` | Whether live transaction flow is enabled | | `signatures_verified` | Whether signing is complete | *** ## Your integration You create the TPA and add signatories via API. Signatories complete signing in a web flow — you do not implement eID yourself. → [TPA flow — full walkthrough](/ems/tpa-flow) · [eID signing](/ems/eid-signing) # Organization Setup Source: https://developers.opencard.io/ems/organization-setup Set up one EMS client in OpenCard — create the billing profile, the organization that links TPA and billing, and the webhook that receives events. The three calls every client needs before card holders. The TPA is the legal side of a client. **This guide is the operational side**: the three objects that make a client actually work in OpenCard. ``` billing → organization (tpa_id + billing_id) → webhook ``` Do this **per client**, right after you have created the TPA and added signatories. You do not need to wait for the TPA to be signed — in fact you want the webhook in place *before* it is signed so `tpa.signed` lands in your app. Where this fits: [Customer onboarding](/ems/customer-onboarding#step-by-step) steps 1, 5 and 6. The TPA itself is covered in [TPA flow](/ems/tpa-flow). *** ## What each object does | Object | Role | Produces | | - | - | - | | **Billing** | Invoice profile for the client + the `product_*` switches that decide which events the client may receive | `billing_id` | | **Organization** | Runtime container. Links `tpa_id` (legal) and `billing_id` (products). Carries your `reference_id` into every webhook | `organization_id` | | **Webhook** | Your endpoint for this organization's events. Verified by a challenge before anything is delivered | `webhook_id`, `secret` | None of them is optional. See [Common mistakes](#common-mistakes) for what breaks without each. *** ## 1. Create the billing profile ``` POST /accounts/{accountId}/billings Scope: billings-write ``` ```json theme={null} { "name_display": "Acme AB", "name_legal": "Acme AB", "organization_number": "5561234567", "country": "SE", "address_street": "Storgatan 1", "address_zip": "111 22", "address_city": "Stockholm", "email_invoice": "finance@acme.se", "your_reference_invoice": "PO-12345", "product_transaction": true, "product_digital_receipt": false, "product_aland_index": false } ``` | Field | Notes | | - | - | | `name_display`, `name_legal`, `organization_number`, `country` | Required. `country` is `SE`, `DK`, `NO` or `FI` | | `email_invoice`, `your_reference_invoice` | Where the OpenCard invoice goes and what reference it carries | | `product_transaction` | **Set `true` for every normal client.** Unlocks `card_transaction_*` events on the webhook | | `product_digital_receipt` | Unlocks `receipt_fetched` and `transaction_true_vat` | | `product_aland_index` | Unlocks `aland_index` | Response → save `id` as **`billing_id`**. One billing profile can be shared by several organizations. Every organization with the same `billing_id` becomes **one line** on your OpenCard invoice. Use that when a client has multiple legal entities or departments but one finance function. → [Model: Billing](/ems/model/billing) API reference → [Create billing profile](/api-reference/ems/billings/create-billing-profile) *** ## 2. Create the organization ``` POST /accounts/{accountId}/organizations Scope: organizations-write ``` ```json theme={null} { "reference_id": "client_acme_001", "tpa_id": 42, "billing_id": 1, "name": "Acme AB", "process_card_type_corporate": true, "process_card_type_corporate_with_private_invoice": true, "process_card_type_private": true } ``` | Field | Notes | | - | - | | `reference_id` | **Your** internal client ID. Unique per account. Echoed as `organization.reference_id` in every webhook payload — this is how you route events back to the right client in your system | | `tpa_id` | From [TPA flow](/ems/tpa-flow). The TPA does not need to be signed yet | | `billing_id` | From step 1 | | `name` | Display name | | `process_card_type_corporate` | Corporate card with invoice to the company. Default `false` | | `process_card_type_corporate_with_private_invoice` | Corporate card with invoice to the card holder. Default `false` | | `process_card_type_private` | Private card with invoice to the card holder. Default `false` | Response → save `id` as **`organization_id`**. ### Card types OpenCard sends transaction events only for the card types you turn on for this organization. A card of a type left at `false` produces no `card.transaction.*` events, and no enrichment on that transaction (receipts, true VAT, line items, Åland Index). Card holder events and `tpa.signed` are not filtered this way. | Flag | Card type | | - | - | | `process_card_type_corporate` | Corporate card with invoice to the company | | `process_card_type_corporate_with_private_invoice` | Corporate card with invoice to the card holder | | `process_card_type_private` | Private card with invoice to the card holder | All three default to `false`. **Set all three to `true` when you create the organization** so transactions start arriving. Turn a type off later with `PUT` if that client should not receive it. ``` PUT /accounts/{accountId}/organizations/{organizationId} ``` ```json theme={null} { "process_card_type_corporate": true, "process_card_type_corporate_with_private_invoice": false, "process_card_type_private": false } ``` ### Already created an organization without `billing_id`? Fix it with a `PUT` — no need to delete and recreate: ``` PUT /accounts/{accountId}/organizations/{organizationId} ``` ```json theme={null} { "billing_id": 1 } ``` ### One client, several organizations? Usually one client = one organization. Split only when the client needs **separate webhook endpoints**, separate event subscriptions, or separate card-holder groups. All of them can point at the same `tpa_id` and `billing_id`. API reference → [Create organization](/api-reference/ems/organizations/create-organization) · [Model: Organization](/ems/model/organization) *** ## 3. Create the webhook ``` POST /accounts/{accountId}/organizations/{organizationId}/webhooks Scope: webhooks-write ``` ```json theme={null} { "url": "https://your-app.com/hooks/opencard", "enabled": true, "tpa_signed": true, "card_holder_created": true, "card_holder_identified": true, "card_holder_signed_pdpc": true, "card_holder_deleted": true, "card_transaction_authorized": true, "card_transaction_cleared": true, "card_transaction_deleted": true } ``` Response includes `secret` — **save it**, you need it to answer the challenge. OpenCard immediately sends `GET {url}?challenge=…` with `X-Event: challenge`. Your endpoint answers with `X-Verify-Token: HMAC-SHA256(challenge, secret)`. When that passes, the webhook is `active` and events start flowing. **Verify:** ``` GET /accounts/{accountId}/organizations/{organizationId}/webhooks ``` → `active: true`. If it is `false`, the challenge failed — see [Webhook setup](/ems/webhooks/setup#challenge-handshake-) for the handshake in detail, authentication headers and the test endpoint. Event flags are validated against the billing. Requesting `card_transaction_*` when the organization's billing has `product_transaction: false` — or when the organization has **no `billing_id`** — is rejected. Fix the billing (or the `billing_id`) first, then create the webhook. API reference → [Create webhook](/api-reference/ems/webhooks/create-webhook) · Full guide → [Webhook setup](/ems/webhooks/setup) · [Event reference](/ems/webhooks/events) *** ## Done — what you should have | Stored | From | Used by | | - | - | - | | `billing_id` | step 1 | organization | | `organization_id` | step 2 | webhook, card holders | | webhook `secret` | step 3 | challenge, signature verification | And on the OpenCard side: * [ ] `GET .../organizations/{organizationId}` shows `tpa_id`, `billing_id`, and the card types you want as `true` * [ ] `GET .../organizations/{organizationId}/webhooks` shows one webhook with `active: true` Now you can add employees → [Card holder onboarding](/ems/card-holders). When the TPA is signed you will see it arrive as `tpa.signed` on the webhook you just created. *** ## Common mistakes | Symptom | Cause | Fix | | - | - | - | | Webhook create fails when `card_transaction_*` is `true` | Organization has no `billing_id`, or billing has `product_transaction: false` | Create billing, `PUT` `billing_id` on the organization, retry | | Employee signs PDPC with eID and gets an error page; no `card_holder.identified` | Organization has **no webhook** | Create the webhook, then create a new card holder (the signed PDPC is stored, but the flow could not complete) | | Webhook stays `active: false` | Challenge not answered correctly | Check `X-Verify-Token` = HMAC-SHA256 of the challenge string with the webhook `secret` | | Events arrive but you can't tell which client | `reference_id` on the organization is not your client ID | Set `reference_id` to your internal id when creating the organization | | No `tpa.signed` received | Webhook was created after the TPA was signed, or `tpa_signed: false` | Poll `GET .../tpas/{tpaId}` for `signed`; enable `tpa_signed` for future clients | | Webhook is `active` but no transactions arrive | The card's `process_card_type_*` flag is `false` — that is the default for all three | `PUT` the organization with the types you want. Start with all three `true` | *** ## Using the ocTPA plugin? The plugin hands you `billing`, `tpa` and `tpa_signatories` in `onDataSend`. It does **not** create the organization or the webhook. Do steps 2 and 3 in your `onDataSend` handler right after the billing and TPA calls. → [Plugins](/ems/plugins) # Payment product setup Source: https://developers.opencard.io/ems/payment-products-setup One-time setup — browse the payment product catalog, enable the ones you sell, and let clients pick a product in your marketplace before TPA onboarding. Client onboarding starts with **choosing a payment product**. That only works if you have already decided **which products your EMS offers**. This is a **one-time account setup** — not part of the per-client wizard. Part of the full path → [Customer onboarding overview](/ems/customer-onboarding) A payment product is the card a client activates — for example a debit or credit card under a program, issued by a bank. ```mermaid theme={null} flowchart TD A[GET /payment-products — catalog] --> B{Enable on your account} B -->|API| C[POST .../accounts/id/payment-products/id] B -->|UI| D[application.opencard.io] C --> E[GET .../accounts/id/payment-products — enabled list] D --> E E --> F[Your marketplace — client picks one] F --> G[TPA with payment_product_id] ``` *** ## 1. Browse the catalog ``` GET /payment-products Scope: payment-products-read ``` Returns every payment product on the OpenCard platform. Your EMS account does **not** have them yet — this is the full catalog. Optional query params: | Param | What it does | | - | - | | `q` | Space-separated search across product name, subtitle, and program name | | `funding` | `debit` or `credit` | | `per_page` | Page size (default 15) | → [List available payment products](/api-reference/ems/payment-products/list-available-payment-products) *** ## 2. Enable the ones you sell Pick the products you want in your marketplace, then attach them to **your account**: ``` POST /accounts/{accountId}/payment-products/{id} Scope: account-payment-products-write ``` Empty body. `201` on success; `400` if that product is already enabled. You can do this via the API or in **[application.opencard.io](https://application.opencard.io)**. Same result either way. → [Enable payment product](/api-reference/ems/payment-products/enable-payment-product) · [Quickstart](/getting-started/quickstart#step-2-enable-a-payment-product) *** ## 3. Show enabled products in your marketplace When a **client** onboards, list only what you enabled: ``` GET /accounts/{accountId}/payment-products Scope: account-payment-products-read ``` Same optional query params as the catalog: | Param | What it does | | - | - | | `q` | Space-separated search across product name, subtitle, and program name | | `funding` | `debit` or `credit` | | `per_page` | Page size (default 15) | Build a marketplace (or wizard step) where the client picks one product. Store the product **`id`** — you pass that as `payment_product_id` when you create the TPA. The optional [ocTPA plugin](/ems/plugins) renders the same step as product cards:
Nordea Business Card
Corporate Card
Nordea

Corporate card with real-time transaction data, digital receipts, and expense enrichment via OpenCard.

Mastercard
Info & Pricing Activate
Fields from each enabled product: | API field | Use in UI | | - | - | | `name` | Card title | | `subtitle` | Subtitle (e.g. Corporate Card) | | `description_short` | Short description on the card | | `description_long` | Longer copy on confirmation | | `payment_program.name` | Program name on confirmation | | `payment_program.logo_url` | Program logo | | `scheme.name` | Card scheme (`mastercard`, `visa`) | | `scheme.logo_url` | Visa / Mastercard badge | | `id` | Pass this as `payment_product_id` on `POST .../tpas` | *** ## Next step Once products are enabled, each client picks one in Phase 1 and you create the TPA with that `id` as `payment_product_id`. → [TPA flow](/ems/tpa-flow) · [Customer onboarding](/ems/customer-onboarding#step-by-step) # Embed Plugins Source: https://developers.opencard.io/ems/plugins Embed the ocTPA and ocPDPC JavaScript widgets on your site to run self-service TPA onboarding wizards and cardholder PDPC consent flows for OpenCard. OpenCard ships embeddable JS plugins for partner sites. These are **optional UI helpers**. For the full API path (billing → TPA → organization → webhook → card holders), start with [Customer onboarding](/ems/customer-onboarding) — you can integrate without these plugins. *** ## ocTPA — TPA onboarding wizard Self-service wizard that mirrors [customer onboarding steps 1–4](/ems/customer-onboarding#step-by-step): 1. Pick payment product (`GET .../payment-products` on the account) 2. Pick add-ons (transactions + optional enrichment) 3. Invoice email / reference 4. Signatory combination + emails (`GET .../publicrecords`) 5. Hand payload to you via `onDataSend` **Load:** ```html theme={null} ``` **Init:** ```javascript theme={null} new ocTPA({ environment: 'sandbox', // or 'production' oAuthToken: 'Bearer ...', // scopes: public-records-read + account-payment-products-read only accountId: 1, orgNumber: '5561234567', country: 'SE', allowedPaymentProductIds: [], // optional: payment product `id`s to show (default: all enabled on the account) infoAndPriceMapping: { 1: 'https://your-site/pricing/product-1' }, // optional: "Info & Pricing" URL per payment product id onDataSend: async function(data) { // YOU call the APIs — plugin does not write to OpenCard: // 1. POST /billings (data.billing) — includes product_* flags // 2. POST /tpas (data.tpa) // 3. POST /tpas/{id}/signatories for each in data.tpa_signatories // 4. POST /organizations (tpa_id + billing_id from steps 1–2) // 5. POST /organizations/{id}/webhooks — REQUIRED before any card holder // Then: card holders return true; // or error string } }); ``` **The plugin stops at the TPA.** It never creates the organization or the webhook — there is nothing in the `onDataSend` payload for them, because the webhook URL is *your* endpoint, not client input. You must create both in your `onDataSend` handler (or immediately after) using the ids returned from the billing and TPA calls. A client with a TPA but no webhook will fail at PDPC signing. See [The order matters](/ems/customer-onboarding#the-order-matters). ### `onDataSend` payload ```json theme={null} { "billing": { "name_display": "...", "name_legal": "...", "organization_number": "...", "country": "SE", "address_street": "...", "address_city": "...", "address_zip": "...", "email_invoice": "...", "your_reference_invoice": "...", "product_transaction": true, "product_digital_receipt": true, "product_aland_index": false }, "tpa": { "language": "sv", "name": "...", "country": "SE", "organization_number": "...", "payment_product_id": 1 }, "tpa_signatories": [ { "name": "Anna", "email": "anna@acme.se" } ] } ``` *** ## ocPDPC — consent link helper Lightweight modal that opens the PDPC signing URL in a new tab. ```javascript theme={null} new ocPdpc({ pdpcUrl: 'https://api.opencard.io/accounts/1/pdpcs/3/sign/token...', message: 'Please approve data processing for your corporate card.', buttonText: 'Approve now' }); ``` Display-only. No API calls. # Receipt Scanner (OCR) Source: https://developers.opencard.io/ems/receipt-scanner Use the OpenCard OCR receipt scanner API to upload photo receipts and get back structured merchant, amount, date, line item, and CO₂ data synchronously. Your EMS can scan photo receipts via a synchronous API call. ``` POST /accounts/{accountId}/receipts/scan Scope: receipts-write ``` ```json theme={null} { "receipt": "base64_encoded_image_here", "include": ["line_items", "environmental_impact"] } ``` | Field | Notes | | - | - | | `receipt` | Base64 image. Recommended width \~800px. | | `include` | Optional: `line_items`, `environmental_impact` | Returns structured OCR data — merchant, amounts, dates, line items, optional CO₂ estimate. Use case: employee uploads photo of paper receipt in your expense app → you call this API → get structured data back instantly. 📸 # Digital Receipts Source: https://developers.opencard.io/ems/receipts How OpenCard delivers digital receipts from Receipt provider partners — and which webhooks send receipt, VAT, and line item data to your EMS. When `receiptable: true` on a transaction, a digital receipt exists for that purchase at one of OpenCard's Receipt provider partners. OpenCard fetches it and delivers receipt, VAT, and line-item data to your EMS via webhooks. *** ## What you receive Three separate webhooks, in any order: | Event | Contains | | - | - | | `receipt.fetched` | PDF/image URL (expires — download it!) | | `transaction.true.vat` | Correct VAT breakdown from merchant | | `transaction.line.items` | Individual purchase items | Subscribe to all three in your webhook config for the full picture. *** ## Receipt URL handling ```json theme={null} { "url": "https://api.opencard.io/files/receipts/abc123", "content_type": "application/pdf", "expiration_time": "2022-10-02 11:12:14", "purchase_merchant": "Coop", "original_amount": 109.38, "original_currency": "SEK" } ``` Sample file: [Coop receipt (109.38 SEK)](https://api.opencard.io/files/receipts/abc123) — same purchase as in [Webhook events](/ems/webhooks/events). 1. Download immediately when webhook arrives 2. Store in your system 3. Don't hotlink — URLs expire *** ## When there is no digital receipt If `receiptable: false`, no Receipt provider partner has a receipt for this transaction — don't wait for `receipt.fetched`. # TPA Flow — Full Walkthrough Source: https://developers.opencard.io/ems/tpa-flow Complete TPA walkthrough — create the authorization, add signatories, send signing emails, and finish eID signing across Sweden, Norway, Denmark, and Finland. TPA = **Transaction Processing Authorization**. It's the legal agreement that says "this company's card transaction data is allowed to flow through OpenCard to the EMS." Without a signed TPA, no transactions. New here? Start with the [full customer onboarding flow](/ems/customer-onboarding) — billing, organization, webhook, and card holders around this TPA step. *** ## TPA lifecycle states | Status | Meaning | | - | - | | `pending-signatures` | Signatories added but not all signed yet | | `pending-activation` | Waiting for TPA to be confirmed | | `activated` | Live. Transactions flow. | ```mermaid theme={null} stateDiagram-v2 [*] --> pending-signatures: POST /tpas pending-signatures --> pending-activation: all signed pending-activation --> activated: TPA confirmed ``` *** ## Step 1: Create TPA ``` POST /api/v1/application/accounts/{accountId}/tpas Scope: account-tpas-write ``` ```json theme={null} { "payment_product_id": 1, "name": "Acme AB", "country": "SE", "organization_number": "5561234567", "language": "sv" } ``` | Field | Required | Notes | | - | - | - | | `payment_product_id` | ✅ | Payment product `id` from products **enabled on your account** (`GET .../accounts/{accountId}/payment-products`) | | `name` | ✅ | Overwritten by registry name if found | | `country` | ✅ | `SE`, `DK`, `NO`, or `FI` only | | `organization_number` | ✅ | Format depends on country (see below) | | `language` | ❌ | `sv`, `no`, `da`, `en`, `fi`. Defaults from country | **Response `201`:** ```json theme={null} { "id": 42, "account_id": 1, "payment_product_id": 1, "name": "Acme AB", "country": "SE", "organization_number": "5561234567", "activated": false, "signatures_verified": false, "signed_document_path": null, "signatures_verified_at": null, "created_at": "2026-06-08T10:00:00.000000Z", "updated_at": "2026-06-08T10:00:00.000000Z" } ``` ### Org number validation | Country | Rule | Example | | - | - | - | | 🇸🇪 `SE` | exactly 10 digits | `5561234567` | | 🇳🇴 `NO` | exactly 9 digits | `987654321` | | 🇩🇰 `DK` | exactly 8 digits | `12345678` | | 🇫🇮 `FI` | `\d{7}-\d{1}` | `1234567-8` | *** ## Step 2: Check who can sign (recommended) Before adding signatories, query the public registry: ``` GET /accounts/{accountId}/publicrecords?country=SE&organization_number=5561234567 Scope: public-records-read ``` **Example response:** ```json theme={null} { "organization_number": "5561234567", "name": "Acme AB", "address_street": "Storgatan 1", "address_zip": "111 22", "address_city": "Stockholm", "signature_combinations": [ { "signatories": [ { "admin_sign": false, "name": "Anna Andersson", "ssn_hash": null }, { "admin_sign": false, "name": "Erik Eriksson", "ssn_hash": null } ] } ] } ``` Each entry in `signature_combinations` is one valid signing group — **everyone listed in that group** must sign for the combination to be satisfied. The registry does not always list who may sign. Many organization types — foundations, associations, and similar entities — are not fully registered as companies in public records across the Nordic countries. The response may include company details but an **empty** `signature_combinations` array. In that case OpenCard cannot suggest signatories; your client must decide internally who is authorized to sign and you add them as signatories manually. *** ## Step 3: Add signatories → email fires 📧 ``` POST /accounts/{accountId}/tpas/{tpaId}/signatories Scope: account-tpa-signatories-write ``` ```json theme={null} { "email": "ceo@acme.se", "name": "Anna Andersson", "country_code": "+46", "phone_number": "0701234567" } ``` Only `email` is required. `phone_number` is stored but not returned in API responses. **What happens immediately:** 1. `token` = random 40-char string generated 2. Signatory record created with `signed: false` 3. **Email queued** via issuer-specific mailer: * **Subject:** `Authorise TPA for {tpaName}` * **Template:** TPA signing invitation * **Link:** `https://{env}/accounts/{accountId}/tpas/{tpaId}/sign/{token}` * **Language:** English (hardcoded for signatory emails) The signatory does **not** need an OpenCard account. The token in the URL is their auth. ### Update / delete signatory ``` PUT .../signatories/{tpaSignatoryId} ← only if signed=false, resends email DELETE .../signatories/{tpaSignatoryId} ← only if signed=false ``` ### Reminder emails If a signatory has not signed after 7+ days, OpenCard may send a reminder email automatically. *** ## Step 4: Signatory signs with eID ✍️ This is a **web flow**, not an API call. The signatory: 1. Clicks email link → `GET /accounts/{accountId}/tpas/{tpaId}/sign/{token}` 2. Sees TPA legal text (markdown rendered) 3. Clicks "Sign" → eID widget loads 4. Completes signing with national eID — see [eID Signing](/ems/eid-signing) 5. eID result is posted back → `POST .../sign/{token}` **When all required signatories have signed**, OpenCard verifies signing rights against `signature_combinations` from the registry (stored on the TPA at creation) for Sweden and Norway. If no signatory combinations were available from the registry, OpenCard cannot verify automatically — the TPA goes through **manual review** before `signatures_verified` is set. *** ## Step 5: TPA fully signed → stuff happens When signing is complete: 1. 📄 **Signed PDF generated** with all signatory eID approval blocks 2. 📧 **Email to each signatory** with signed PDF attached 3. 📧 **Card issuer notified** (signed PDF delivered) 4. 🔔 **`tpa.signed` webhook** fires on orgs that subscribed **Webhook payload:** ```json theme={null} { "id": "42", "name": "Acme AB", "organization_number": "5561234567", "url": "https://api.opencard.io/files/tpas/abc123.pdf", "organization": { "reference_id": "client_acme_001" } } ``` ### Download signed PDF via API ``` GET /accounts/{accountId}/tpas/{tpaId}/signeddocuments → application/pdf ``` *** ## Step 6: Activation TPA moves to `activated` when the TPA is confirmed — triggered on the issuer side, not something you call directly. Until activated, transactions won't flow even if TPA is signed. *** ## Delete TPA ``` DELETE /accounts/{accountId}/tpas/{tpaId} ``` If TPA was activated → issuer termination email sent, cascades delete organizations/webhooks/cardholders linked to it. Only delete TPAs you're sure are unused. Activated TPA deletion is destructive. *** ## TPA embed plugin (optional) OpenCard ships a JS wizard (`ocTPA`) for self-service TPA onboarding on your site. It collects issuer selection, signatory combos, billing info — then calls your `onDataSend` callback with the payload for you to hit the APIs. See [Plugins](/ems/plugins). # Webhook Event Reference Source: https://developers.opencard.io/ems/webhooks/events Full catalog of OpenCard webhook events for TPA, card holder, transaction, receipt, and environmental impact — with X-Event headers and JSON payload samples. All webhook deliveries include: ``` X-Event: {event_name} ``` Plus any custom headers you configured, and auth headers if you set `authentication_type`. The transaction, receipt, VAT, and line-item samples below are **one purchase** — Coop in Svängsta, **109.38 SEK**, card ending `1234`, issuer `nordea`. The receipt URL is the real sample file: [Coop receipt](https://api.opencard.io/files/receipts/abc123). *** ## Event catalog ### TPA | Event | Flag to enable | When | | - | - | - | | `tpa.signed` | `tpa_signed` | TPA fully signed, PDF generated | ### Card holder | Event | Flag | When | | - | - | - | | `card_holder.created` | `card_holder_created` | Card holder created via API | | `card_holder.identified` | `card_holder_identified` | Identity linked via eID | | `card_holder.signed.pdpc` | `card_holder_signed_pdpc` | PDPC consent signed | | `card_holder.deleted` | `card_holder_deleted` | Card holder deleted | | `card_holder.email.delivered` | `card_holder_email_delivered` | PDPC email delivered | | `card_holder.email.failed` | `card_holder_email_failed` | PDPC email bounced | ### Transactions | Event | Flag | When | | - | - | - | | `card.transaction.authorized` | `card_transaction_authorized` | Purchase authorized (real-time) | | `card.transaction.cleared` | `card_transaction_cleared` | Transaction settled (3-5 days) | | `card.transaction.deleted` | `card_transaction_deleted` | Authorization reversed | | `card.transaction.invoiced` | `card_transaction_invoiced` | Invoice number assigned | ### Enrichment | Event | Flag | When | | - | - | - | | `receipt.fetched` | `receipt_fetched` | Digital receipt matched | | `transaction.true.vat` | `transaction_true_vat` | Correct VAT from merchant | | `transaction.line.items` | `transaction_line_items` | Purchase line items | | `environmental.impact.aland_index` | `aland_index` | CO₂ estimate (provider 1) | | `environmental.impact.deedster` | `deedster` | CO₂ estimate (provider 2) | *** ## Payloads ### `tpa.signed` ```json theme={null} { "id": "42", "name": "Acme AB", "organization_number": "5561234567", "url": "https://api.opencard.io/files/tpas/abc123.pdf", "organization": { "reference_id": "client_acme_001" } } ``` ### Card holder events (all share same shape) ```json theme={null} { "card_holder": { "reference_id": "employee_john_42" }, "organization": { "reference_id": "client_acme_001" } } ``` ### `card.transaction.*` (all share same shape) ```json theme={null} { "id": "10", "state": "authorized", "receiptable": true, "card_issuer": "nordea", "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "card_issuer_reference": "11212945", "card_issuer_customer_number": null, "type": "card", "invoice_number": null, "invoice_date": null, "original_amount": 109.38, "original_currency": "SEK", "vat_rate": 0, "vat_amount": 0, "vat_currency": "SEK", "accounting_amount": 109.38, "accounting_currency": "SEK", "exchange_rate": null, "purchase_merchant": "Coop Svängsta", "purchase_merchant_common": "Coop", "purchase_date": "2022-09-02", "purchase_time": "11:12:14", "purchase_timezone": null, "purchase_country": "SE", "purchase_city": "SVANGSTA", "card_id": 1, "card_product": "first_card", "card_funding": "credit", "card_type": "corporate", "card_last_four": "1234", "mcc_code": "5411", "mcc_text": null, "mcc_top_level": "Retail Stores", "branch_code": null, "branch_text": null, "category_text": "Card purchase", "card_holder": { "reference_id": "employee_john_42" }, "organization": { "reference_id": "client_acme_001" } } ``` #### Field reference | Field | Type | Description | | - | - | - | | `id` | string | Unique transaction ID. **Use this as your primary key.** | | `state` | string | `authorized`, `cleared`, `deleted`, `invoiced` | | `receiptable` | boolean | `true` if a digital receipt exists at an OpenCard Receipt provider partner | | `card_issuer` | string | Card program identifier (`name_system`, e.g. `nordea`) | | `payment_product` | object | `{ "id": , "name": "" }` | | `card_issuer_reference` | string | Issuer's reference for this state (not globally unique) | | `card_funding` | string | `debit` or `credit` — from the registered card | | `card_type` | string | `private`, `corporate`, `corporate_with_private_invoice` | | `original_amount` | float | Amount at merchant | | `original_currency` | string | ISO 4217 | | `accounting_amount` | float | Amount in customer's home currency | | `accounting_currency` | string | ISO 4217 | | `purchase_merchant` | string | Full merchant name | | `purchase_merchant_common` | string | Normalized merchant name | | `mcc_code` | string | Merchant Category Code | ### `receipt.fetched` Matches the sample file at the URL (Coop, 109.38 SEK, same purchase as the transaction above). ```json theme={null} { "url": "https://api.opencard.io/files/receipts/abc123", "content_type": "application/pdf", "expiration_time": "2022-10-02 11:12:14", "purchase_merchant": "Coop", "original_amount": 109.38, "original_currency": "SEK", "purchase_date": "2022-09-02", "purchase_time": "11:12:14", "transaction": { "id": "10", "card_issuer": "nordea" }, "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "card_holder": { "reference_id": "employee_john_42" }, "organization": { "reference_id": "client_acme_001" } } ``` Receipt URLs expire. Download and store the file, don't hotlink. ### `transaction.true.vat` VAT bands from the Coop receipt (12% + 25%): ```json theme={null} { "transaction": { "id": "10", "card_issuer": "nordea" }, "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "organization": { "reference_id": "client_acme_001" }, "card_holder": { "reference_id": "employee_john_42" }, "purchase_country": "SE", "vat_rates": [ { "vat_amount": 5.52, "taxable_amount": 45.96, "total_amount": 51.48, "percentage": 12, "currency": "SEK" }, { "vat_amount": 11.58, "taxable_amount": 46.32, "total_amount": 57.90, "percentage": 25, "currency": "SEK" } ], "totals": [ { "vat_amount": 17.10, "taxable_amount": 92.28, "total_amount": 109.38, "currency": "SEK" } ] } ``` ### `transaction.line.items` Line items from the same Coop receipt: ```json theme={null} { "transaction": { "id": "10", "card_issuer": "nordea" }, "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "organization": { "reference_id": "client_acme_001" }, "card_holder": { "reference_id": "employee_john_42" }, "line_items": [ { "description": "SERV 33CM VIT", "amount": 12.95, "total_amount": 12.95, "quantity": 1, "currency": "SEK" }, { "description": "VÄRMELJUS STEARIN", "amount": 44.95, "total_amount": 44.95, "quantity": 1, "currency": "SEK" }, { "description": "GODIS LÖSVIKT", "amount": 11.68, "total_amount": 11.68, "quantity": 0.195, "currency": "SEK" }, { "description": "ZUCCHINI GRÖN ST", "amount": 9.95, "total_amount": 39.80, "quantity": 4, "currency": "SEK" } ] } ``` ### `environmental.impact.aland_index` / `deedster` ```json theme={null} { "carbon_emission_in_grams": 0.66, "carbon_social_cost_amount": 2.1, "carbon_social_cost_currency": "SEK", "transaction": { "id": "10", "card_issuer": "nordea" }, "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "organization": { "reference_id": "client_acme_001" }, "card_holder": { "reference_id": "employee_john_42" } } ``` # Webhook Setup Source: https://developers.opencard.io/ems/webhooks/setup Create an OpenCard webhook, complete the verification challenge, configure authentication headers, and route events per organization for your EMS integration. Webhooks are how OpenCard talks to your app. Every transaction, receipt, and lifecycle event gets POSTed to your URL. *** ## Create webhook ``` POST /accounts/{accountId}/organizations/{organizationId}/webhooks Scope: webhooks-write ``` ```json theme={null} { "url": "https://your-app.com/hooks/opencard", "enabled": true, "authentication_type": "basic", "basic_username": "opencard", "basic_password": "your-secret-password", "card_transaction_authorized": true, "card_transaction_cleared": true, "card_transaction_deleted": true, "card_transaction_invoiced": false, "card_holder_created": true, "card_holder_identified": true, "card_holder_signed_pdpc": true, "card_holder_deleted": true, "receipt_fetched": true, "transaction_true_vat": true, "transaction_line_items": true, "tpa_signed": true } ``` | Field | Notes | | - | - | | `url` | Your HTTPS endpoint. Must be reachable from internet. | | `secret` | Auto-generated 60-char string if omitted. Used for challenge HMAC. | | `enabled` | `true` to activate (after challenge passes) | | `authentication_type` | `none`, `basic`, `oauth`, or `custom` | | Event flags | Boolean per event type. Only subscribed events are delivered. | Response includes `secret` — **save it**. You need it for challenge verification. *** ## Challenge handshake 🔐 Immediately after create (or URL change), OpenCard verifies you own the endpoint: ``` GET https://your-app.com/hooks/opencard?challenge=RANDOM_20_CHAR_STRING Header: X-Event: challenge ``` Your server must respond `200` with header: ``` X-Verify-Token: HMAC-SHA256(challenge_string, webhook_secret) ``` **Node.js example:** ```javascript theme={null} const crypto = require('crypto'); app.get('/hooks/opencard', (req, res) => { const challenge = req.query.challenge; if (!challenge) return res.status(400).send('missing challenge'); const token = crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(challenge) .digest('hex'); res.set('X-Verify-Token', token).status(200).send('ok'); }); ``` If HMAC matches → `active=true`. If not → `active=false`, no events delivered. *** ## Receiving events Events come as `POST` to your URL: ``` POST https://your-app.com/hooks/opencard X-Event: card.transaction.authorized Content-Type: application/json { ... payload ... } ``` Plus auth headers if configured: | `authentication_type` | What OpenCard sends | | - | - | | `none` | Just `X-Event` | | `basic` | `Authorization: Basic base64(user:pass)` | | `custom` | Your `custom_key: custom_value` as HTTP header | | `oauth` | Bearer token fetched from your `oauth_access_token_url` | **Always respond `200` quickly.** Process async if needed. OpenCard retries on failure. *** ## Custom headers Add extra headers OpenCard includes on every delivery: ``` POST .../webhooks/{webhookId}/headers { "key": "X-My-App-Id", "value": "ems-prod-001" } ``` *** ## Webhook groups (buyer-party filter) Filter events by card issuer buyer party: ``` POST .../webhooks/{webhookId}/groups { "buyer_party": "5561234567" } ``` Only transactions matching that buyer party hit this webhook. *** ## Test an event ``` POST .../webhooks/{webhookId}/test/card.transaction.authorized ``` Sends a test payload to your endpoint. Good for verifying your handler works. *** ## Delivery log ``` GET .../webhooks/{webhookId}/events Scope: webhook-events-read ``` Paginated log of every delivery attempt — status code, payload, timestamp. Use for debugging. *** ## Checklist ✅ * [ ] HTTPS endpoint deployed * [ ] Challenge handler implemented (GET with `?challenge=`) * [ ] Event handler implemented (POST with `X-Event` header) * [ ] Respond 200 fast, process async * [ ] `secret` stored securely * [ ] Subscribed to the right event flags * [ ] Test event sent and received # Handling Transaction States Source: https://developers.opencard.io/ems/webhooks/transactions Handle OpenCard transaction state webhooks — what authorized, cleared, deleted, and invoiced mean, when amounts settle, and how to post them to a ledger. Getting transaction states wrong = angry accountants. Here's the exact logic. *** ## The 4 states ```mermaid theme={null} stateDiagram-v2 [*] --> authorized: purchase happens authorized --> deleted: auth reversed authorized --> cleared: settles (3-5 days) cleared --> invoiced: issuer invoices ``` | State | Timing | Amounts reliable? | Use for accounting? | | - | - | - | - | | `authorized` | Seconds after purchase | ❌ May change | Preview only | | `deleted` | Before clearing | N/A | Delete from your system | | `cleared` | 3-5 days after purchase | ✅ Final | **YES** | | `invoiced` | When issuer invoices | ✅ Final + invoice # | YES | *** ## Recommended implementation ```javascript theme={null} async function handleTransaction(event, payload) { const txId = payload.id; const state = payload.state; // from X-Event suffix or payload.state switch (event) { case 'card.transaction.authorized': // Create if not exists (upsert by id) await db.transactions.upsert(txId, { ...payload, status: 'pending' }); break; case 'card.transaction.cleared': case 'card.transaction.invoiced': // Update existing OR create if you skipped authorized await db.transactions.upsert(txId, { ...payload, status: 'confirmed' }); break; case 'card.transaction.deleted': // Only relevant if you stored authorized await db.transactions.delete(txId); break; } } ``` ### The golden rules 1. **Always handle `cleared`** — non-negotiable for accounting 2. **`authorized` + `deleted` + `cleared`** = best UX (users see purchases instantly, deletions handled) 3. **`authorized` alone** = you'll miss transactions that clear without prior auth (rare but possible) 4. **Use `id` as primary key** — same `id` across all states for one transaction 5. **`card_issuer_reference` differs per state** — don't use it as your primary key; when merging with another feed, compare cleared-to-cleared only → [Current card issuer integration](/ems/current-card-issuer-integration) *** ## Current card issuer integration If your client already receives transactions from the same card program through another integration, you can run both feeds at once. OpenCard is often added while the old feed is still active — you do not need to wait for it to be switched off. * Start the OpenCard feed per card holder on **`card_holder.identified`** (includes a retroactive batch). * **Upsert on transaction `id`** for OpenCard events. * On **`card.transaction.cleared`**, deduplicate against your other store with **`card_issuer_reference`** — cleared matches cleared only; auth and cleared references may differ per issuer. Full guidance → [Current card issuer integration](/ems/current-card-issuer-integration) ## Upsert logic detail | Event | Transaction exists? | Action | | - | - | - | | `authorized` | No | Create | | `authorized` | Yes | Update (amounts may change before clearing) | | `cleared` | No | Create (you missed authorized — still ok) | | `cleared` | Yes | Update with final amounts | | `invoiced` | No | Create | | `invoiced` | Yes | Update, set `invoice_number` | | `deleted` | Yes | Delete | | `deleted` | No | Ignore (nothing to delete) | *** ## `receiptable` flag ```json theme={null} { "receiptable": true } ``` If `true`, a digital receipt exists for this transaction at one of OpenCard's Receipt provider partners — expect `receipt.fetched` (and often TrueVAT / line items) shortly after. If `false`, don't wait for a digital receipt. Subscribe to `receipt_fetched`, `transaction_true_vat`, and `transaction_line_items` separately — they come as independent events after the transaction. *** ## Card types | `card_type` | Meaning | | - | - | | `corporate` | Corporate card with invoice to the company | | `corporate_with_private_invoice` | Corporate card with invoice to the card holder | | `private` | Private card with invoice to the card holder | ## Card funding | `card_funding` | Meaning | | - | - | | `credit` | Credit card | | `debit` | Debit card | Present on **all** `card.transaction.*` states — same value for a given card across authorized / cleared / deleted / invoiced. Which of these you receive is set on the organization with `process_card_type_*`. Enable all three when you create the organization, then turn a type off if you do not want those transactions. → [Organization setup — card types](/ems/organization-setup#card-types) *** ## VAT caveat `vat_rate` and `vat_amount` on the transaction payload come from the **card network** — often wrong for restaurants, hotels, etc. For correct VAT, subscribe to `transaction.true.vat` — that comes from the actual merchant receipt. # Authentication Source: https://developers.opencard.io/getting-started/authentication Set up OAuth2 account_client machine-to-machine credentials for your EMS integration, request bearer tokens, and pick the right scopes for OpenCard endpoints. EMS integrations authenticate with an **`account_client`** OAuth client — machine-to-machine credentials tied to your partner account. This docs site covers **account\_client** only. Other client types (`organization_client`, `account_user`, etc.) exist internally but are not part of the EMS integration path. *** ## Get a token ``` POST /oauth/token Content-Type: application/x-www-form-urlencoded Accept: application/json ``` ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: application/json" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "scope=organizations-read organizations-write webhooks-write" ``` **Response:** ```json theme={null} { "token_type": "Bearer", "expires_in": 31536000, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc..." } ``` Use on every API call: ``` Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... Accept: application/json ``` Always send `Accept: application/json` on Application API and OAuth requests. Without it, auth failures may return an HTML login page instead of a JSON error body. *** ## Create an account\_client **Dashboard:** Account → OAuth Clients → Create New Or via API (requires an existing client with `oauth-clients-write`): ``` POST /accounts/{accountId}/oauthclients ``` Response includes `secret` in **plain text** — shown once. Store it immediately. Update scopes later: ``` PUT /accounts/{accountId}/oauthclients/{oauthclientId} { "scopes": ["organizations-read", "organizations-write", "webhooks-write"] } ``` Clients created on an account **without** an `organization_id` are `account_client` — this is what you want. *** ## Recommended scopes for EMS Start with this set: ``` account-tpas-read account-tpas-write account-tpa-signatories-write account-tpa-identities-read organizations-read organizations-write card-holders-read card-holders-write webhooks-read webhooks-write webhook-events-read public-records-read payment-products-read account-payment-products-read account-payment-products-write oauth-clients-read billings-write receipts-write ``` Request only what you need. Scopes are space-separated in the token request. *** ## All account\_client scopes | Scope | What it unlocks | | - | - | | `accounts-read` / `accounts-write` | Your account settings | | `oauth-clients-read` / `oauth-clients-write` | Manage OAuth clients | | `public-records-read` | Company registry lookup | | `account-tpas-read` / `account-tpas-write` / `account-tpas-delete` | TPAs | | `account-tpa-signatories-read` / `-write` / `-delete` | TPA signatories | | `account-tpa-identities-read` | Identities on a TPA | | `payment-products-read` | List available payment products | | `account-payment-products-read` / `-write` | List, enable, and disable payment products on your account | | `organizations-read` / `-write` / `-delete` | Organizations | | `card-holders-read` / `-write` / `-delete` | Card holders | | `webhooks-read` / `webhooks-write` | Webhooks | | `webhook-events-read` | Webhook delivery log | | `webhook-headers-read` / `-write` / `-delete` | Custom webhook headers | | `webhook-groups-read` / `-write` / `-delete` | Buyer-party webhook filters | | `billings-read` / `billings-write` | Billing profiles | | `receipts-write` | Receipt OCR scan | | `files-read` | Download files (e.g. receipt URLs) | | `environmental-impacts-read` | CO₂ data on transactions | | `account-users-read` / `-write` / `-delete` | Account user management | | `account-statistics-read` | Account statistics | *** ## Access control Your `account_client` token is tied to your **account**. The API checks: 1. Token has the required scope for the route 2. Token owner has access to `accountId` / `organizationId` in the URL You can only access your own account's resources. *** ## Errors | Status | Meaning | | - | - | | `401` | Invalid or expired token | | `403` | Valid token but missing scope or no access to resource | | `404` | Resource doesn't exist (or you can't see it) | # Quickstart — Zero to First Webhook Source: https://developers.opencard.io/getting-started/quickstart End-to-end OpenCard EMS quickstart in the right order — register, billing, TPA, organization, webhook, TPA signed, card holder, first transaction. This is the full path for **one test client**, in the order the API expects it: ``` account → payment product → billing → TPA + signatories → organization → webhook → tpa.signed → card holder → transaction ``` Every step produces an **id** that the next step needs. Don't skip ahead — the two most common integration failures are an organization without `billing_id` and card holders created before the webhook exists. Both are avoided by simply following the order below. Grab coffee. ☕ Let's go. *** ## Step 0: Register (sandbox) 👉 [sandbox-api.opencard.io/register](https://sandbox-api.opencard.io/register) You get an **Account** + admin user. Verify email. Log in. Your `accountId` is in the dashboard URL or API responses. *** ## Step 1: Create an account\_client and get a token ```bash theme={null} # Dashboard: Account → OAuth Clients → Create # This is an account_client — machine-to-machine credential for your EMS integration. curl -X POST https://sandbox-api.opencard.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: application/json" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "scope=billings-write account-tpas-write account-tpas-read account-tpa-signatories-write organizations-write organizations-read card-holders-write webhooks-write webhooks-read public-records-read payment-products-read account-payment-products-read account-payment-products-write" ``` Save the `access_token`. All subsequent calls: ``` Authorization: Bearer {access_token} Accept: application/json ``` Scopes are space-separated. Request only what you need. Full list in [Authentication](/getting-started/authentication). Always send `Accept: application/json` so error responses stay JSON (not an HTML login page). **Verify:** `GET /api/v1/application/payment-products` returns `200`. Auth works. *** ## Step 2: Enable a payment product (once per account) ```bash theme={null} # List available payment products curl https://sandbox-api.opencard.io/api/v1/application/payment-products \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" # Enable one on your account (use the id from above) curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/payment-products/1 \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" ``` Save `payment_product_id`. You need it for the TPA in step 4. → [Payment product setup](/ems/payment-products-setup) *** ## Step 3: Create a billing profile 🧾 The billing profile is the client's invoice profile **and** the switch that turns on products. The `product_*` flags decide which webhook events the client is allowed to subscribe to — so this must exist before the organization and the webhook. ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/billings \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name_display": "Acme AB", "name_legal": "Acme AB", "organization_number": "5561234567", "country": "SE", "email_invoice": "finance@acme.se", "your_reference_invoice": "PO-12345", "product_transaction": true, "product_digital_receipt": false, "product_aland_index": false }' ``` Response → save `id` as **`billing_id`**. `product_transaction: true` is what allows `card_transaction_*` events on the webhook later. Creating a webhook with transaction events for an organization whose billing lacks this flag (or that has no `billing_id` at all) fails. → [Model: Billing](/ems/model/billing) *** ## Step 4: Create the TPA 📜 TPA = Transaction Processing Authorisation. Legal permission for the client's transaction data to flow. First, check who may sign for the company (optional, but saves a round-trip with the client): ```bash theme={null} curl "https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/publicrecords?country=SE&organization_number=5561234567" \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" ``` Response includes `signature_combinations` — the people you'll add as signatories in step 5. Then create the TPA: ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/tpas \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "payment_product_id": 1, "name": "Acme AB", "country": "SE", "organization_number": "5561234567", "language": "sv" }' ``` **Org number formats:** | Country | Format | Example | | - | - | - | | 🇸🇪 SE | 10 digits | `5561234567` | | 🇳🇴 NO | 9 digits | `123456789` | | 🇩🇰 DK | 8 digits | `12345678` | | 🇫🇮 FI | `XXXXXXX-X` | `1234567-8` | Response → save `id` as **`tpa_id`**. → [TPA flow](/ems/tpa-flow) *** ## Step 5: Add signatories → email goes out 📧 ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/tpas/$TPA_ID/signatories \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "email": "ceo@acme.se", "name": "Anna Andersson" }' ``` The email is queued immediately with a signing link. Repeat for each signatory required by `signature_combinations`. Signatories can only be updated/deleted while `signed=false`. Once signed, they're locked. Don't wait for the signature here — continue with steps 6 and 7 right away so the webhook is in place when `tpa.signed` fires. *** ## Step 6: Create the organization 🏢 The organization is the runtime container for the client. It links the TPA (legal) and the billing (products) together. ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/organizations \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "reference_id": "client_acme_001", "tpa_id": '"$TPA_ID"', "billing_id": '"$BILLING_ID"', "name": "Acme AB" }' ``` `reference_id` = **your** internal client ID. It comes back in every webhook. Response → save `id` as **`organization_id`**. → [Model: Organization](/ems/model/organization) *** ## Step 7: Create the webhook 🔔 ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/organizations/$ORG_ID/webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "url": "https://your-app.com/webhooks/opencard", "enabled": true, "tpa_signed": true, "card_holder_created": true, "card_holder_identified": true, "card_holder_signed_pdpc": true, "card_transaction_authorized": true, "card_transaction_cleared": true, "card_transaction_deleted": true }' ``` **Challenge happens immediately.** OpenCard sends: ``` GET https://your-app.com/webhooks/opencard?challenge=RANDOM_STRING Header: X-Event: challenge ``` Your endpoint must respond with header: ``` X-Verify-Token: HMAC-SHA256(challenge, webhook_secret) ``` **Verify:** `GET .../organizations/$ORG_ID/webhooks` shows `active: true`. Until then no events are delivered. The webhook is not optional. Card holder lifecycle and PDPC signing write events to it — an organization without a webhook cannot complete PDPC signing. Create it now, before any card holder. → [Webhook setup](/ems/webhooks/setup) *** ## Step 8: TPA gets signed → your first webhook ✍️ The signatory clicks the link from step 5, reads the TPA and signs with eID. When every required signatory has signed: * Signed PDF is generated and stored * **`tpa.signed` is POSTed to your webhook** — this is your first real event * TPA status → `pending-activation`, then `activated` once confirmed **Verify:** you received `X-Event: tpa.signed` with `organization.reference_id = client_acme_001`. → [eID signing](/ems/eid-signing) · [Event reference](/ems/webhooks/events) *** ## Step 9: Create card holders 👤 Two paths — pick one. Full guide → [Card holder onboarding](/ems/card-holders). ### Path A: Email + eID (new employee) ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/organizations/$ORG_ID/cardholders \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "reference_id": "employee_john_42", "email": "john@acme.se" }' ``` John gets an email → signs the PDPC with eID → `card_holder.identified` → transactions flow. `language` is optional. If omitted, OpenCard picks the PDPC language from the TPA country and falls back to English where no translation exists. ### Path B: `identity_id` (instant — person already in OpenCard) ```bash theme={null} # 1. List identities on the TPA (people who already have cards) curl "https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/tpas/$TPA_ID/identities?is_card_holder=false" \ -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/json" # 2. Create card holder with identity_id — transactions start immediately curl -X POST https://sandbox-api.opencard.io/api/v1/application/accounts/$ACCOUNT_ID/organizations/$ORG_ID/cardholders \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "reference_id": "employee_anna_07", "identity_id": 123, "skip_pdpc_email": true }' ``` `card_holder.identified` fires instantly + retroactive transaction webhooks replay. ⚡ **Verify:** you received `X-Event: card_holder.identified`. *** ## Step 10: Receive your first transaction 🎉 Once the issuer delivers transaction states for an identified card holder: ```json theme={null} // POST to your webhook URL // Header: X-Event: card.transaction.authorized { "id": "10", "state": "authorized", "receiptable": true, "card_issuer": "nordea", "payment_product": { "id": 1, "name": "Acme Corporate Card" }, "original_amount": 109.38, "original_currency": "SEK", "purchase_merchant": "Coop Svängsta", "purchase_date": "2022-09-02", "card_funding": "credit", "card_holder": { "reference_id": "employee_john_42" }, "organization": { "reference_id": "client_acme_001" } } ``` Nothing arriving yet? Fire a test event: `POST .../webhooks/{webhookId}/test/card.transaction.authorized`. **You did it.** From zero to live transaction data. *** ## Recap — what you stored | Variable | From step | Used in | | - | - | - | | `ACCOUNT_ID` | 0 | everything | | `TOKEN` | 1 | everything | | `payment_product_id` | 2 | TPA | | `BILLING_ID` | 3 | organization | | `TPA_ID` | 4 | signatories, organization | | `ORG_ID` | 6 | webhook, card holders | Repeat steps 3–9 for every new client. Steps 0–2 are once per account. *** ## What's next? * [Customer onboarding](/ems/customer-onboarding) — the same flow as a production integration guide, with API reference links * [Transaction states](/ems/webhooks/transactions) — how to handle authorized vs cleared vs deleted * [All webhook events](/ems/webhooks/events) — full payload reference * [Receipt enrichment](/ems/receipts) — digital receipts + true VAT # Register & Setup Source: https://developers.opencard.io/getting-started/register Register a sandbox account on OpenCard, verify your email, find your accountId, and create your first account_client OAuth client to start integrating. ## Sandbox registration 1. Go to 👉 [sandbox-api.opencard.io/register](https://sandbox-api.opencard.io/register) 2. Fill in account + user details 3. Verify email (check inbox + spam) 4. Log in You now have: * An **Account** (your EMS partner account) * An **admin user** (for dashboard access) * An `accountId` (visible in dashboard URL) *** ## Create account\_client **Dashboard:** Account → OAuth Clients → Create New This creates an **`account_client`** — your machine-to-machine credential for the EMS API. Give it a name, select scopes (see [Authentication](/getting-started/authentication)), save. You'll get: * `client_id` * `client_secret` (shown **once** — copy it now) *** ## Get your first token ```bash theme={null} curl -X POST https://sandbox-api.opencard.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "Accept: application/json" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_ID" \ -d "client_secret=YOUR_SECRET" \ -d "scope=accounts-read" ``` If you get a token back → you're wired up. 🔌 *** ## Production access Sandbox is free and instant. Production requires: 1. Complete sandbox integration 2. Security review 3. Partnership agreement Email **[support@opencard.io](mailto:support@opencard.io)** when you're ready. *** ## What's next? → [Quickstart](/getting-started/quickstart) — full integration walkthrough # Environments Source: https://developers.opencard.io/introduction/environments Compare OpenCard sandbox and production environments — API base URLs, OAuth token endpoints, signing pages, and registration links for each isolated universe. | | 🧪 Sandbox | 🚀 Production | | - | - | - | | **API base** | `https://sandbox-api.opencard.io` | `https://api.opencard.io` | | **Application API** | `.../api/v1/application` | `.../api/v1/application` | | **OAuth token** | `.../oauth/token` | `.../oauth/token` | | **Register** | [sandbox-api.opencard.io/register](https://sandbox-api.opencard.io/register) | [api.opencard.io/register](https://api.opencard.io/register) | | **Signing pages** | `sandbox-api.opencard.io/accounts/.../sign/...` | `api.opencard.io/accounts/.../sign/...` | Everything is isolated — credentials, data, webhook secrets, signed documents. Separate universes. # Glossary Source: https://developers.opencard.io/introduction/glossary OpenCard glossary — EMS, account, organization, billing, card holder, identity, TPA, PDPC, transaction state, card issuer, receipt provider, webhook defined. | Term | What it means | | - | - | | **EMS** | Expense Management System — software for corporate card spend, expenses, and receipts. | | **Account** | Your Expense Management System partner account. Top of the hierarchy. | | **Organization** | Your end-client. You set `reference_id`. Has webhooks + card holders. | | **Billing** | Invoice recipient + roll-up. Organizations with the same `billing_id` appear as **one line** on the invoice you receive from OpenCard. | | **Card Holder** | Individual whose transactions you receive. You set `reference_id`. | | **Identity** | Physical person in OpenCard (SSN via eID). Has cards. Use `identity_id` for instant onboarding. | | **TPA** | Transaction Processing Authorization. Company-level legal agreement. | | **PDPC** | Personal Data Processing Consent. Individual GDPR consent. | | **Transaction** | One purchase. ID stays same across all states. | | **Transaction State** | Snapshot: `authorized`, `cleared`, `deleted`, `invoiced`. | | **Payment product** | The card a client activates (debit or credit under a payment program). Enable products on your account, then pass the product `id` as `payment_product_id` on the TPA. | | **Card Issuer** | Bank or card program. Delivers transactions via the Issuer API; may also use `receipts.opencard.io` for digital receipt matching. | | **Receipt Provider** | Collects receipts from merchants and delivers them to OpenCard. OpenCard forwards enriched data to Expense Management Systems. | | **Webhook** | HTTP POST from OpenCard when stuff happens. | | **Reference ID** | Your string ID on orgs/cardholders. In every webhook. | | **Challenge** | Webhook verification handshake on setup. | | **Scope** | OAuth permission string like `webhooks-write`. | | **CAID / MID** | Merchant ID in receipt networks. | | **MCC** | Merchant Category Code (ISO 18245). | # How OpenCard works Source: https://developers.opencard.io/introduction/how-it-works The OpenCard hierarchy — account, billing, organization, card holder, transaction, TPA — and how events flow from card issuer to your EMS webhook endpoint. ## The hierarchy (memorize this) ``` Account ← you (the EMS partner) ├── Billing ← invoice recipient; orgs with same billing_id = one line on YOUR OpenCard invoice └── Organization ← your end-client company ├── billing_id → which billing profile (invoice line roll-up) this org belongs to ├── Webhook ← where events get POSTed ├── CardHolder ← employee whose card data you want │ └── Card ← actual credit card (managed by issuer) │ └── Transaction │ └── TransactionState (authorized → cleared → invoiced) └── TPA ← legal agreement (signed by client's authorized signatories) ``` **Account** = your OpenCard partner account. One per EMS. **Billing** = invoice recipient for **your** OpenCard bill. Organizations that share a `billing_id` are rolled up as **one line** on that invoice (not separate invoices per org). Example: three orgs with `billing_id: 1` → one combined line to your finance team. → [Billing](/ems/model/billing) **Organization** = one of your customers. You set `reference_id` to your internal client ID. Most EMSs map 1:1 (one org per client), but you can split one client into multiple orgs if you need different webhook configs or card type filters. → [Organization](/ems/model/organization) **Card Holder** = a person. You set `reference_id` to your internal user ID. Two ways to onboard: **email** (user signs with eID) or **`identity_id`** (instant, person already in OpenCard). → [Card holder](/ems/model/card-holder) · [Onboarding](/ems/card-holders) **TPA** (Transaction Processing Authorization) = legal doc the client's signatories sign saying "yes, our card data can flow to this EMS via OpenCard." → [TPA](/ems/model/tpa) **PDPC** (Personal Data Processing Consent) = GDPR consent the individual cardholder signs saying "yes, my transactions can be shared." ## Two legal docs, two audiences | Doc | Who signs | How | Purpose | | - | - | - | - | | **TPA** | Company's authorized signatories (CEO, board, etc.) | eID **sign** mode — they sign the actual legal document | Company-level permission | | **PDPC** | Individual cardholder (employee) | eID **auth** mode — identity verification + consent checkbox | Person-level GDPR consent | OpenCard handles all of this. For new users you provide an email — we send the link, they sign with eID. For users already in OpenCard you pass `identity_id` and skip the wait entirely. ## The full onboarding sequence ```mermaid theme={null} flowchart TD A[1. Register account] --> B[2. Create OAuth client] B --> C[3. Enable payment products on account] C --> D[4. Create billing profile] D --> E[5. Create TPA for client] E --> F[6. Add TPA signatories] F --> G[7. They sign with eID] G --> K[TPA fully signed → tpa.signed webhook] K --> L[8. Create organization + billing_id] L --> M[9. Configure webhook] M --> N[10. Create card holders] N --> O[11. Cardholders get PDPC email] O --> P[12. They sign PDPC with eID] P --> Q[13. Transactions start flowing] ``` Step-by-step for EMS integrators (APIs, phases, checklist) → [Customer onboarding](/ems/customer-onboarding) ## Runtime: what happens on a purchase 1. **Cardholder buys coffee** ☕ 2. **Issuer** sends `authorized` transaction state to OpenCard 3. OpenCard normalizes the data, checks org/webhook config 4. **Your webhook** gets `card.transaction.authorized` within seconds 5. If merchant is receipt-enabled → OpenCard requests receipt match 6. 3–5 days later issuer sends `cleared` state 7. Your webhook gets `card.transaction.cleared` — **this is the accounting truth** 8. Maybe `receipt.fetched`, `transaction.true.vat`, `transaction.line_items` follow ## What you build vs what OpenCard builds | You build | OpenCard builds | | - | - | | Webhook endpoint + event handlers | Legal signing flows (TPA + PDPC) | | Org/cardholder CRUD in your UI | eID signing flows | | Map `reference_id` to your DB | Issuer communication | | Transaction UI in your expense app | Receipt matching orchestration | | | Webhook delivery + retry + logs | ## Reference IDs — your best friend Every organization and card holder has a `reference_id` **you define**. Put your internal ID there. Every webhook payload includes it. Zero lookup tables needed. ```json theme={null} { "card_holder": { "reference_id": "user_48291" }, "organization": { "reference_id": "client_acme_corp" } } ``` Map straight to your DB. Done. 🎯 ## Client already has another transaction feed? Common during rollout. Your client enables OpenCard with the issuer — they do not have to disable an existing connection first, and you are not responsible for that. If they want one feed only, the client coordinates with the issuer. OpenCard sends **`authorized`** events (many other integrations do not) and replays history on **`card_holder.identified`**. Deduplicate **`cleared`** events with `card_issuer_reference` when merging with your existing store. → [Current card issuer integration](/ems/current-card-issuer-integration) # OpenCard Developers Source: https://developers.opencard.io/introduction/index OpenCard developer hub — real-time card transactions, digital receipts, VAT, and webhooks for Expense Management Systems, card issuers, and receipt providers.
# Build on OpenCard

Real-time card transactions, digital receipts, VAT, and line items — delivered to your systems via webhooks.

Orgs, card holders, webhooks. Receive `card.transaction.*` events in real time. Full API on `api.opencard.io` — TPAs, organizations, card holders, webhooks. Deliver transactions to EMS apps, or fetch digital receipts via `receipts.opencard.io`. Collect merchant receipts and deliver them to OpenCard for Expense Management System enrichment.

Explore further

Data model, event flow, and what OpenCard handles. OAuth2 account\_client credentials and scopes. Sandbox vs production URLs. Transactions, receipts, VAT, lifecycle events. `receipts.opencard.io` — publishers and callback requests. TPA, PDPC, card holder — every term explained.
# About OpenCard Source: https://developers.opencard.io/introduction/welcome What OpenCard is, who uses it — EMS, card issuers, receipt providers — and what transaction, receipt, VAT, and environmental data lands in your webhooks. OpenCard is the pipe between card programs and your app — transactions, receipts, VAT, line items, and environmental impact, pushed to your webhook as stuff happens. No polling. No scraping PDFs. ## Who are you? | You are... | Go here | What you do | | - | - | - | | 🏢 **Expense Management System** | [Quickstart](/getting-started/quickstart) | Create orgs, card holders, webhooks. Receive transaction events. | | 💳 **Card issuer / bank** | [Card Issuers](/card-issuers) | Deliver transactions via the Issuer API, or fetch digital receipts via `receipts.opencard.io`. | | 🧾 **Receipt provider** | [Receipt Providers](/receipt-providers) | Collect receipts, deliver to OpenCard for Expense Management System enrichment. | ## The 30-second version ```mermaid theme={null} sequenceDiagram participant Bank as Card Program participant OC as OpenCard participant You as Your Expense Management System Note over You,OC: Setup (once per client) You->>OC: Create billing profile (invoice recipient + line roll-up) You->>OC: Create TPA + signatories sign with eID You->>OC: Create organization + webhook (linked to billing) You->>OC: Create card holders Note over Bank,You: Runtime (every purchase) Bank->>OC: transaction data OC->>You: webhook card.transaction.* ``` ## What you get pushed to your webhook * `card.transaction.authorized` — purchase just happened * `card.transaction.cleared` — settled, use for accounting * `card.transaction.deleted` — auth reversed * `card.transaction.invoiced` — has invoice number * `receipt.fetched` — digital receipt URL * `transaction.true.vat` — correct VAT from merchant * `transaction.line.items` — purchase line items * `card_holder.*` — lifecycle events * `tpa.signed` — legal agreement done ## Next step → **[Quickstart](/getting-started/quickstart)** — full walkthrough from register to first webhook. # Callback Delivery Source: https://developers.opencard.io/receipt-providers/callback-delivery How receipt providers POST matched receipts to OpenCard — endpoint URL, X-Event headers, HMAC-SHA256 signatures, and payload structure for callback delivery. When you've matched a receipt from your collection to an OpenCard transaction, POST it here: ``` POST https://api.opencard.io/api/v1/service/marcet/callback/{referenceId} ``` `{referenceId}` = OpenCard transaction reference from the match request. *** ## Required headers | Header | Value | | - | - | | `X-Event` | `CallbackRequestResolved` or `CallbackRequestDeleted` | | `X-CallbackRequest-Id` | Callback request ID from OpenCard | | `X-Data-Signature` | `HMAC-SHA256(raw_body, client_secret)` | *** ## Signature calculation ```python theme={null} import hmac, hashlib signature = hmac.new( client_secret.encode(), request_body_bytes, hashlib.sha256 ).hexdigest() ``` ```javascript theme={null} const crypto = require('crypto'); const sig = crypto.createHmac('sha256', clientSecret) .update(rawBody) .digest('hex'); ``` Must match `X-Data-Signature` header exactly. Raw body bytes, not re-serialized JSON. *** ## Resolved payload Content type depends on callback type configured for the request: * `application/xml` — receipt XML (parsed for VAT + line items) * `application/json` — structured receipt data OpenCard extracts: * True VAT rates and amounts → `transaction.true.vat` webhook * Line items → `transaction.line.items` webhook * Receipt file → stored, URL sent via `receipt.fetched` webhook *** ## Response | Code | Meaning | | - | - | | `200` | Accepted | | `403` | Bad signature | | `404` | Unknown callback request ID | Duplicate `CallbackRequestResolved` for already-resolved requests → `200` (idempotent, logged as warning). *** ## What happens after 1. Receipt file downloaded and stored in cloud 2. TrueVAT records created 3. LineItem records created 4. Webhooks fired to EMS (per org webhook config) # How receipt providers work with OpenCard Source: https://developers.opencard.io/receipt-providers/how-it-works How receipt providers fit into OpenCard — collect receipts from merchants, handle match requests, deliver receipt callbacks, and enrich EMS transaction data. You operate a **receipt collection network**. OpenCard connects your receipts to card transactions and passes enriched data to EMS integrators. *** ## Where you sit ```mermaid theme={null} flowchart LR Merchants[Merchants / POS / E-com] -->|receipts| You[Receipt Provider] OC[OpenCard] -->|match request| You You -->|receipt callback| OC OC -->|webhooks| EMS[EMS apps] ``` 1. You continuously **collect receipts** from merchants in your network 2. OpenCard receives a card transaction marked `receiptable: true` 3. OpenCard sends you the transaction details to match against your receipts 4. When you find a match, you **POST the receipt** to OpenCard 5. OpenCard stores the receipt, extracts VAT + line items 6. EMS apps receive `receipt.fetched`, `transaction.true.vat`, and `transaction.line.items` webhooks You only integrate with OpenCard — not with EMS apps or card programs. *** ## What you deliver to OpenCard | You send | OpenCard does | EMS receives | | - | - | - | | Receipt file (PDF, image, XML) | Stores file, generates URL | `receipt.fetched` | | VAT breakdown in receipt | Creates TrueVAT records | `transaction.true.vat` | | Line items in receipt | Creates LineItem records | `transaction.line.items` | *** ## Onboarding checklist * [ ] Receive **client secret** from OpenCard * [ ] Endpoint ready to **receive match requests** from OpenCard * [ ] Implement **outbound callback** to OpenCard (`POST /v1/service/marcet/callback/{referenceId}`) * [ ] Handle `CallbackRequestResolved` and `CallbackRequestDeleted` * [ ] Sign every payload with HMAC-SHA256 → [Callback delivery](/receipt-providers/callback-delivery) for endpoint details. Contact **[support@opencard.io](mailto:support@opencard.io)** to get started. # Receipt Provider Integration Source: https://developers.opencard.io/receipt-providers/index Integration guide for receipt providers — collect receipts from merchants and POS, deliver to OpenCard, and enrich EMS transactions with VAT and line items. Receipt providers are actors that **collect digital receipts** — from merchants, POS systems, e-commerce platforms, or your own network — and **deliver them to OpenCard**. OpenCard attaches the receipt to the matching card transaction, extracts VAT and line items, and forwards everything to EMS apps via webhooks. *** ## The flow ```mermaid theme={null} sequenceDiagram participant RP as Receipt Provider participant OC as OpenCard participant EMS as EMS Note over RP: You collect receipts from merchants OC->>RP: Match request (transaction data) RP->>RP: Find receipt in your collection RP->>OC: POST callback with receipt data OC->>EMS: receipt.fetched OC->>EMS: transaction.true.vat OC->>EMS: transaction.line.items ``` Your job ends at the callback to OpenCard. You never talk to EMS apps directly. *** ## What you implement 1. **Collect receipts** in your merchant network 2. **Receive match requests** from OpenCard when a transaction is receiptable 3. **POST the receipt** back to OpenCard when you have a match → [How it works](/receipt-providers/how-it-works) · [Callback delivery](/receipt-providers/callback-delivery) · [Payload formats](/receipt-providers/payload-formats) *** ## Auth Callbacks to OpenCard are authenticated with **HMAC-SHA256**: ``` X-Data-Signature: HMAC-SHA256(raw_request_body, shared_client_secret) ``` You receive the shared secret from OpenCard during onboarding. *** ## Events | `X-Event` header | Meaning | | - | - | | `CallbackRequestResolved` | Receipt found — send receipt payload | | `CallbackRequestDeleted` | No match / expired — send empty body | Contact **[support@opencard.io](mailto:support@opencard.io)** to become a receipt provider partner. # Payload Formats Source: https://developers.opencard.io/receipt-providers/payload-formats Payload formats for receipt provider callbacks to OpenCard — required headers, HMAC signatures, and XML receipt body for resolved and deleted callback events. When you send a collected receipt to OpenCard, the body format depends on the callback type configured for your integration. *** ## Headers (always required) | Header | Value | | - | - | | `X-Event` | `CallbackRequestResolved` or `CallbackRequestDeleted` | | `X-CallbackRequest-Id` | Callback request ID from OpenCard | | `X-Data-Signature` | `HMAC-SHA256(raw_body, client_secret)` | *** ## CallbackRequestResolved — XML receipt Content-Type: `application/xml` OpenCard parses receipt XML to extract: * True VAT rates and amounts * Line items (if present in receipt) * Receipt file for storage Delivery note XML (partner code transactions) uses a different VAT extraction path. *** ## CallbackRequestResolved — JSON Content-Type: `application/json` Structured receipt data when callback type is JSON. *** ## CallbackRequestDeleted Empty or minimal body. Indicates the match was cancelled or expired. OpenCard marks the callback request as deleted. *** ## Signature calculation ```python theme={null} import hmac, hashlib signature = hmac.new( client_secret.encode(), request_body_bytes, # raw bytes, not re-serialized hashlib.sha256 ).hexdigest() # Set as X-Data-Signature header ``` ```javascript theme={null} const crypto = require('crypto'); const sig = crypto.createHmac('sha256', clientSecret) .update(rawBodyBuffer) .digest('hex'); ``` Hash the **raw request body bytes** exactly as received. Re-serializing JSON will break the signature. *** ## After successful delivery OpenCard fires these webhooks to the EMS (if subscribed): **`receipt.fetched`** ```json theme={null} { "url": "https://api.opencard.io/files/receipts/abc123", "content_type": "application/pdf", "expiration_time": "2022-10-02 11:12:14", "purchase_merchant": "Coop", "original_amount": 109.38, "original_currency": "SEK", "transaction": { "id": "10", "card_issuer": "nordea" }, "payment_product": { "id": 1, "name": "Acme Corporate Card" } } ``` **`transaction.true.vat`** — corrected VAT from merchant receipt **`transaction.line.items`** — purchase line items OpenCard handles EMS delivery — you only send receipts to OpenCard.