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.
TPA lifecycle states
| Status | Meaning |
|---|
pending-signatures | Signatories added but not all signed yet |
pending-activation | Fully signed, waiting for cards to be confirmed |
activated | Live. Transactions flow. |
Step 1: Create TPA
POST /api/v1/application/accounts/{accountId}/tpas
Scope: account-tpas-write
{
"card_issuer_id": 1,
"name": "Acme AB",
"country": "SE",
"organization_number": "5561234567",
"language": "sv"
}
| Field | Required | Notes |
|---|
card_issuer_id | ✅ | Which card program (from GET /cardissuers) |
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 |
Server-side magic:
- Looks up company in public registry (Roaring in prod)
- Stores
signature_combinations in public_info (who can sign)
- Sets
seller_party from issuer config
- Creates/finds
client record
- Snapshots legal text from latest
type=tpa template
Response 201:
{
"id": 42,
"account_id": 1,
"card_issuer_id": 1,
"name": "Acme AB",
"country": "SE",
"organization_number": "5561234567",
"activated": false,
"signatures_verified": false,
"signed_document_path": null
}
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 registry:
GET /accounts/{accountId}/publicrecords?country=SE&organization_number=5561234567
Scope: public-records-read
Response includes signature_combinations — groups of people where all members of a group must sign. Example:
{
"signature_combinations": [
[
{ "name": "Anna Andersson", "ssn_hash": "abc123..." },
{ "name": "Erik Eriksson", "ssn_hash": "def456..." }
]
]
}
Add each person as a signatory. When they sign with eID, their ssn_hash is checked against these combinations.
Step 3: Add signatories → email fires 📧
POST /accounts/{accountId}/tpas/{tpaId}/signatories
Scope: account-tpa-signatories-write
{
"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:
token = random 40-char string generated
- Signatory record created with
signed: false
- 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 unsigned after 7+ days, a reminder job re-sends the email (if SCHEDULE_REMIND_TPA_SIGNATORIES=true).
Step 4: Signatory signs with eID ✍️
This is a web flow, not an API call. The signatory:
- Clicks email link →
GET /accounts/{accountId}/tpas/{tpaId}/sign/{token}
- Sees TPA legal text (markdown rendered)
- Clicks “Sign” → Assently CoreID widget loads
- Signs with eID — see eID Signing
- Signs the document (mode: sign — legal text hash is bound to signature)
- Assently POSTs
identityToken back → POST .../sign/{token}
Server validates:
identityToken JWT from Assently
- Extracts:
ssn, country, name, full signature claims
- Sets signatory:
signed=true, signed_at=now(), encrypted ssn + signature
When all required signatories have signed, OpenCard verifies signing rights against signature_combinations from the registry and marks the TPA as signed.
Step 5: TPA fully signed → stuff happens
When setSigned() runs:
- 📄 Signed PDF generated with all signatory eID approval blocks
- 💾 Stored at
legal_documents/{date}/{random}.pdf
- 📧 Email to each signatory with signed PDF attached
- 📧 Card issuer notified (signed PDF delivered)
- 🔔
tpa.signed webhook fires on orgs that subscribed
Webhook payload:
{
"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 card issuer confirms cards exist for this buyer/seller party. This is triggered by issuer-side card processing — 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.