Account Opening
Onboarding a customer and opening a deposit account through ORCA. The workflow covers party search and creation (personal and commercial), account creation with ownership and party-role configuration, related-account linking, initial funding, and post-creation servicing (card issuance, document upload, party-to-party relationships, account updates).
Business context
Digital onboarding platforms need to connect into the core banking systems of their financial institution partners. Each Core (Fiserv, FIS, Jack Henry, etc.) has its own protocols, data formats, and quirks; building a bespoke integration per Core is expensive, brittle, and slow.
ORCA exposes a single normalized REST API based on ISO 20022 that fronts any supported Core. The fintech writes one integration and gains access to multiple Cores. For Account Opening specifically, this means a single API surface for customer search, profile creation, account opening, party-role configuration, related-account linking, funding, and Day-1 servicing — across any Core ORCA supports.
Scope
In-scope for this use case:
- Customer search & creation —
PersonandOrganizationprofiles with duplicate prevention - Account creation & updates — opening new accounts with complex ownership structures, modifying them after creation
- Complex party-role configuration —
AccountOwner,Beneficiary,Trustee,PowerOfAttorney,AuthorizedSigner,Custodian - Related-account linking — overdraft protection, sweep, sub-accounts via
relatedAccounts[] - Customer relationships (groupings) — householding, business control structures via
/relationships - Account funding — initial funding via
/internal-transfersor/accounts/{id}/deposits - Post-creation servicing — debit card issuance, account attribute updates, KYC document upload
- Custom data — flex fields via
supplementaryDatafor institution-specific needs
Out-of-scope (handled elsewhere or not by ORCA):
- Identity verification (KYC) decisioning — ORCA accepts and carries documents and identifiers; the fintech is responsible for actually running KYC/CIP checks before submitting to ORCA
- Credit decisioning — not applicable to deposit account opening; for loans see Loan Boarding
- Orchestration logic on the fintech side — ORCA exposes the API surface; the fintech owns the user journey and orchestration
- Modifications to the ORCA specification — the spec is fixed; supplementary data covers non-native fields
Architecture context
ORCA acts as middleware: the Fintech platform calls ORCA's normalized REST endpoints; ORCA translates each call into the operations required by the Core. The Fintech does not need to know the Core's protocol, identifiers, or data model. ORCA itself is stateless — it does not persist any account, party, or transaction data. The Core is the system of record.
sequenceDiagram
autonumber
participant FT as Fintech
participant ORCA as ORCA API
participant Core as FI Core
FT->>ORCA: GET /persons?ssn.eq=...
ORCA->>Core: Customer lookup
Core-->>ORCA: Match / no match
ORCA-->>FT: 200 [PersonDescriptor[]]
alt Person not found
FT->>ORCA: POST /persons (PersonRequest)
ORCA->>Core: Create customer
Core-->>ORCA: customerId
ORCA-->>FT: 202 {personId}
end
FT->>ORCA: POST /accounts (AccountRequest)
ORCA->>Core: Open deposit account
Core-->>ORCA: accountNumber
ORCA-->>FT: 202 {accountId, accountNumber}
FT->>ORCA: POST /internal-transfers OR POST /accounts/{id}/deposits
ORCA->>Core: Post funding transaction
Core-->>ORCA: transactionId
ORCA-->>FT: 202 {transactionId, status}
opt Post-creation servicing
FT->>ORCA: POST /persons/{personId}/cards
FT->>ORCA: POST /documents
FT->>ORCA: POST /relationships
FT->>ORCA: PATCH /accounts/{accountId}
end
Endpoints used
| Step | Method | Path | Purpose |
|---|---|---|---|
| 1a | GET |
/persons |
Search for an existing person |
| 1a | POST |
/persons |
Create a person |
| 1b | GET |
/organizations |
Search for an existing organization |
| 1b | POST |
/organizations |
Create an organization |
| 2 | POST |
/accounts |
Open the account |
| 3a | POST |
/internal-transfers |
Fund via intra-bank transfer |
| 3b | POST |
/accounts/{accountId}/deposits |
Fund via external deposit |
| 4 | POST |
/persons/{personId}/cards |
Issue a debit card |
| 4 | POST |
/documents |
Upload KYC document |
| 4 | POST |
/relationships |
Create party-to-party relationship |
| 4 | PATCH |
/accounts/{accountId} |
Update account details |
Common headers
All write operations accept these headers. They are optional in the spec but generally required in production deployments.
| Header | Purpose |
|---|---|
idempotencyId |
Client-generated unique identifier to make POST/PATCH safely retryable. |
servicerId |
Identifier of the servicing Financial Institution. |
servicerBranchId |
Branch or center identifier within the servicer. |
ISO 20022 — Servicer
In ISO 20022 terminology, the servicer is the institution that holds and operates the account. For a fintech integrating with ORCA, the servicerId identifies which bank the request applies to in multi-tenant deployments.
Step 1 — Search or create the party
Determine if the customer is a Person (personal account) or Organization (commercial account). Always search first to avoid duplicates.
Path A — Personal account
1a.1 Search for the person
GET /persons?firstName.eq=Jane&lastName.eq=Doe&ssn.eq=123-45-6789 HTTP/1.1
Host: api.portx.io
Authorization: Bearer <jwt>
servicerId: bank-001
[
{
"personId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"name": "Jane Doe",
"structuredName": {
"firstName": "Jane",
"lastName": "Doe"
},
"identifiers": [
{ "schemeName": "SSN", "number": "123-45-6789" }
],
"status": "Active"
}
]
Common query parameters:
| Parameter | Description |
|---|---|
firstName.eq, lastName.eq |
Exact match on the first/last name. |
name.inc |
Substring match on the full name. |
ssn.eq |
Exact SSN match. |
ssn.last4 |
Last four digits of SSN. |
birthDate.eq |
Exact date of birth. |
phoneNumber.eq |
Any phone number on the profile. |
tin.eq |
Tax identification number. |
status.eq |
Filter by party status (e.g., Active). |
If the response array is empty, proceed to creation.
1a.2 Create the person
POST /persons HTTP/1.1
Host: api.portx.io
Authorization: Bearer <jwt>
Content-Type: application/json
idempotencyId: 4f2d1a8b-9c3e-4f5d-8a1b-2c3d4e5f6a7b
servicerId: bank-001
{
"structuredName": {
"firstName": "Jane",
"middleName": "M",
"lastName": "Doe"
},
"name": "Jane M Doe",
"identifiers": [
{
"schemeName": "SSN",
"number": "123-45-6789",
"issuer": "SSA"
}
],
"postalAddresses": [
{
"addressType": "Residential",
"streetName": "Main St",
"buildingNumber": "1234",
"townName": "Austin",
"countrySubDivision": "TX",
"postCode": "78701",
"country": "US"
}
],
"phones": [
{ "number": "+15125550100", "phoneType": "Mobile" }
],
"emails": [
{ "address": "jane.doe@example.com" }
]
}
{
"personId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"structuredName": {
"firstName": "Jane",
"middleName": "M",
"lastName": "Doe"
},
"name": "Jane M Doe",
"identifiers": [
{ "schemeName": "SSN", "number": "123-45-6789", "issuer": "SSA" }
],
"status": "Active",
"audit": {
"creationDate": "2026-05-19T14:23:11Z"
}
}
Key PersonRequest fields:
| Field | Required | Description |
|---|---|---|
structuredName.firstName |
yes | Given name. |
structuredName.lastName |
yes | Surname. |
name |
no | Display name. If absent, ORCA composes it from structuredName. |
identifiers[] |
recommended | Government identifiers. schemeName is typically SSN, ITIN, or DriverLicense. |
postalAddresses[] |
recommended | At least one address is usually required by the Core for KYC. |
phones[], emails[] |
recommended | Contact details. |
taxInformation |
conditional | Required if the Core does TIN validation. |
supplementaryData |
no | Custom key-value pairs ("flex fields") that aren't native to ORCA. |
Path B — Commercial account
1b.1 Search for the organization
GET /organizations?name.inc=Acme&tin.eq=12-3456789 HTTP/1.1
[
{
"organizationId": "org-3a9d2e10-7c4b-4f88-9e21-0a1b2c3d4e5f",
"name": "Acme Industries LLC",
"organizationType": "LLC",
"identifiers": [
{ "schemeName": "TIN", "number": "12-3456789" }
],
"status": "Active"
}
]
Common query parameters:
| Parameter | Description |
|---|---|
name.inc |
Substring match on the legal name. |
tin.eq |
Tax Identification Number exact match. |
accountNumber.eq |
Organization owning a known account. |
1b.2 Create the organization
POST /organizations HTTP/1.1
Content-Type: application/json
idempotencyId: 8a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
{
"name": "Acme Industries LLC",
"legalName": "Acme Industries Limited Liability Company",
"organizationType": "LLC",
"identifiers": [
{
"schemeName": "TIN",
"number": "12-3456789"
}
],
"registrationDate": "2018-03-15",
"nAICSCode": "54",
"sector": "Industrials",
"postalAddresses": [
{
"addressType": "Business",
"streetName": "Industrial Blvd",
"buildingNumber": "500",
"townName": "Austin",
"countrySubDivision": "TX",
"postCode": "78702",
"country": "US"
}
]
}
{
"organizationId": "org-3a9d2e10-7c4b-4f88-9e21-0a1b2c3d4e5f",
"name": "Acme Industries LLC",
"organizationType": "LLC",
"status": "Active"
}
The resulting personId or organizationId is the partyId used throughout the rest of the workflow.
Step 2 — Open the account
A single POST /accounts request can configure ownership, link related accounts, attach multiple parties with distinct roles, and carry custom data through to the Core.
Endpoint
POST /accounts → 202 Accepted with Account in the body.
Key request fields
| Field | Required | Description |
|---|---|---|
accountType |
yes | Checking, Savings, MoneyMarket, CertificateOfDeposit, etc. |
ownershipType |
yes | SingleOwnerAccount, JointAccount, TrustAccount, BusinessAccount. |
parties[] |
yes | Each entry links a partyId with a partyAccountRole. |
productId |
conditional | Core-defined product code. Often required. |
currency |
no | Defaults to USD. |
initialAmount |
no | Opening balance — informational; the funding transaction in Step 3 actually moves the money. |
relatedAccounts[] |
no | Links to existing accounts (e.g., funding source, overdraft protection). |
supplementaryData |
no | Flex fields. |
Party-account roles
The parties[] array accepts these partyAccountRole values (non-exhaustive): AccountOwner, Beneficiary, PowerOfAttorney, Trustee, Trustor, AuthorizedSigner, Custodian.
ISO 20022 — Party roles
A single party can hold different roles across different accounts. Roles are scoped to the accountId, not to the party.
Single-owner example
{
"accountType": "Checking",
"ownershipType": "SingleOwnerAccount",
"productId": "PROD-CHK-STANDARD",
"currency": "USD",
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyAccountRole": "AccountOwner",
"name": "Jane M Doe"
}
]
}
{
"accountId": "acct-001-chk-9a8b7c6d",
"accountNumber": "1234567890",
"accountType": "Checking",
"ownershipType": "SingleOwnerAccount",
"status": "Active",
"routingNumbers": [
{ "number": "021000021", "type": "ABA" }
]
}
Joint account with beneficiary and overdraft link
{
"accountType": "Checking",
"ownershipType": "JointAccount",
"productId": "PROD-CHK-PREMIUM",
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyAccountRole": "AccountOwner",
"name": "Jane M Doe"
},
{
"partyId": "person-8a4b9d03-5c2f-4e3a-8b1d-2c3d4e5f6a71",
"partyAccountRole": "AccountOwner",
"name": "John Q Smith"
},
{
"partyId": "person-9b5c0e14-6d3a-4f4b-9c2e-3d4e5f6a7b82",
"partyAccountRole": "Beneficiary",
"name": "Junior Doe"
}
],
"relatedAccounts": [
{
"accountId": "acct-002-sav-1a2b3c4d",
"accountRelationType": "FundingSource"
}
],
"supplementaryData": {
"referralCode": "SUMMERPROMO25",
"onboardingRepId": "rep-789"
}
}
{
"accountId": "acct-003-chk-1f2e3d4c",
"accountNumber": "1234567891",
"accountType": "Checking",
"ownershipType": "JointAccount",
"status": "Active"
}
accountRelationType values
| Value | Use |
|---|---|
FundingSource |
Default funding source (overdraft, sweep). |
SubAccount |
Sub-account under a master. |
LinkedAccount |
Generic link with no debit/credit semantics. |
EscrowAccount |
Escrow attached to a loan. |
Step 3 — Fund the account
Two funding paths, depending on where the money comes from.
Option A — Internal transfer (intra-bank)
Use when both source and destination accounts are at the same servicer.
POST /internal-transfers → 202 Accepted with InternalTransfer.
The body must contain an entries[] array where the sum of Credit entries equals the sum of Debit entries.
ISO 20022 — Debit / Credit indicator
Debit decreases the account balance; Credit increases it. A balanced internal transfer has at least one of each, and the totals must match.
{
"entries": [
{
"amount": "500.00",
"creditDebitIndicator": "Debit",
"account": { "accountId": "acct-002-sav-1a2b3c4d" }
},
{
"amount": "500.00",
"creditDebitIndicator": "Credit",
"account": { "accountId": "acct-003-chk-1f2e3d4c" }
}
],
"descriptionLines": [
"Initial funding for new checking account"
]
}
{
"transferId": "xfer-5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a01",
"status": "Accepted",
"entries": [
{
"amount": "500.00",
"creditDebitIndicator": "Debit",
"account": { "accountId": "acct-002-sav-1a2b3c4d" }
},
{
"amount": "500.00",
"creditDebitIndicator": "Credit",
"account": { "accountId": "acct-003-chk-1f2e3d4c" }
}
]
}
Option B — Deposit (external source)
Use for cash, paper check, electronic check, or merchandise payment.
POST /accounts/{accountId}/deposits → 202 Accepted with Transaction.
| Field | Required | Description |
|---|---|---|
transactionType |
yes | Cash, Check, Merchandise, MemoPosted. |
amount |
yes | Decimal string with two places (e.g., "250.00"). |
check |
conditional | Required when transactionType is Check. |
exchange |
no | Currency exchange info for non-USD deposits. |
POST /accounts/acct-003-chk-1f2e3d4c/deposits HTTP/1.1
Content-Type: application/json
idempotencyId: 9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f
{
"transactionType": "Cash",
"amount": "250.00"
}
{
"transactionType": "Check",
"amount": "1000.00",
"check": {
"checkType": "CustomerCheque",
"checkNumber": "1024",
"issuer": "Acme Payroll Inc.",
"amount": "1000.00",
"currency": "USD"
}
}
{
"transactionId": "txn-6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b12",
"amount": "250.00",
"creditDebitIndicator": "Credit",
"status": "Booked",
"transactionType": "Cash",
"creationDate": "2026-05-19T14:30:42Z"
}
Step 4 — Post-creation servicing
These operations are optional but common on Day 1.
4.1 Issue a debit card
POST /persons/{personId}/cards
{
"productId": "PROD-DEBIT-VISA",
"cardHolderType": "Primary",
"fundingSourceType": "Debit",
"nameOnCard": "Jane M Doe",
"digitalIndicator": true,
"relatedAccounts": [
{
"accountId": "acct-003-chk-1f2e3d4c",
"accountRelationType": "FundingSource"
}
]
}
See Cards for the full card lifecycle.
4.2 Upload a KYC document
POST /documents (Documents API)
{
"ownerId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"name": "drivers-license.pdf",
"fileType": "application/pdf",
"documentType": "DriverLicense",
"data": "JVBERi0xLjQKJ...",
"linkages": [
{
"resourceType": "party",
"resourceId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"relationship": "primary"
}
],
"metadata": {
"tags": ["KYC", "Identity"]
}
}
The data field is the file content as a base64-encoded string. Maximum size is 20 MiB.
4.3 Customer relationship (party grouping)
POST /relationships creates a customer relationship — a grouping of related parties, accounts, loans, and cards under a single relationshipId. This is how ORCA models concepts like householding, business control structures, and bundled relationships, which a single FI customer would call a "membership" or "household."
The request body is CustomerRelationshipRequest. Each party in parties[] carries a partyRelationType that describes how that party relates to the grouping (e.g. Spouse, ControlPerson, BeneficialOwner, Trustee).
Relationship vs related party
A CustomerRelationship is a top-level grouping that can include multiple parties, accounts, loans, and cards. It's not the same as relatedAccounts[] (which links one account to another) or parties[] on an account (which assigns roles to an account's parties). Use /relationships when you need to model a household, a business hierarchy, or any cross-resource grouping.
Minimal example — link spouses as a household
POST /relationships HTTP/1.1
Host: api.portx.io
Content-Type: application/json
idempotencyId: 7d6c5b4a-3210-fedc-ba98-76543210fedc
servicerId: bank-001
{
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyType": "Person",
"partyName": "Jane M Doe",
"partyRelationType": "Spouse",
"priorityIndicator": true
},
{
"partyId": "person-8a4b9d03-5c2f-4e3a-8b1d-2c3d4e5f6a71",
"partyType": "Person",
"partyName": "John Q Smith",
"partyRelationType": "Spouse"
}
],
"codes": [
{ "codeType": "RelationshipCategory", "value": "Household" }
]
}
{
"relationshipId": "rel-1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"status": "Active",
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyType": "Person",
"partyName": "Jane M Doe",
"partyRelationType": "Spouse",
"priorityIndicator": true
},
{
"partyId": "person-8a4b9d03-5c2f-4e3a-8b1d-2c3d4e5f6a71",
"partyType": "Person",
"partyName": "John Q Smith",
"partyRelationType": "Spouse"
}
]
}
Richer example — business control structure with linked accounts and a card
A commercial relationship typically groups the organization, its control person, its operating accounts, its line of credit, and any business cards.
{
"parties": [
{
"partyId": "org-3a9d2e10-7c4b-4f88-9e21-0a1b2c3d4e5f",
"partyType": "Organization",
"partyName": "Acme Industries LLC",
"partyRelationType": "Owner",
"priorityIndicator": true
},
{
"partyId": "person-4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90",
"partyType": "Person",
"partyName": "Maria Garcia",
"partyRelationType": "ControlPerson"
},
{
"partyId": "person-5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a01",
"partyType": "Person",
"partyName": "Robert Chen",
"partyRelationType": "BeneficialOwner"
}
],
"accounts": [
{
"accountId": "acct-acme-op-001",
"accountNumber": "2000123456",
"accountType": "Checking",
"accountRelationType": "AssociatedAccount",
"accountName": "Acme Operating",
"primaryIndicator": true
}
],
"loans": [
{
"accountId": "loan-acme-loc-001",
"accountNumber": "LN-2000123457",
"accountType": "Loan",
"loanType": "LineOfCredit",
"accountRelationType": "AssociatedAccount",
"accountName": "Acme Working Capital LOC"
}
],
"cards": [
{
"cardId": "card-acme-001",
"cardHolderType": "Primary",
"fundingSourceType": "Credit"
}
],
"codes": [
{ "codeType": "RelationshipCategory", "value": "BusinessControl" }
]
}
Common partyRelationType values for relationship grouping:
| Value | Use |
|---|---|
Spouse, Child, Parent, Sibling |
Household / family grouping |
Owner, ControlPerson, BeneficialOwner, AuthorizedPerson |
Business control structure |
Trustee, Trustor |
Trust account grouping |
PrimaryContact, SecondaryContact |
Designated contacts for an organization |
4.4 Update account details
PATCH /accounts/{accountId} with application/merge-patch+json.
JSON Merge Patch
A merge patch is a partial document where each field replaces the corresponding field in the target. Setting a field to null removes it. See RFC 7396.
PATCH /accounts/acct-003-chk-1f2e3d4c HTTP/1.1
Content-Type: application/merge-patch+json
{
"name": "Doe Family Checking",
"supplementaryData": {
"marketingOptIn": true
}
}
Error handling
All error responses use a common Error envelope:
{
"code": "InvalidRequest",
"message": "Required field 'parties' is missing",
"details": [
{
"code": "MissingField",
"message": "parties is required",
"target": "parties"
}
],
"documentationUrl": "/errors/invalid-request"
}
| Status | When |
|---|---|
400 Bad Request |
Schema validation failure, missing required field, malformed identifier. |
404 Not Found |
Referenced partyId, accountId, or productId doesn't exist. |
409 Conflict |
Idempotency collision with a prior call that had different parameters. |
500 Internal Server Error |
Unexpected ORCA or Core failure. Safe to retry with the same idempotencyId. |
502 Bad Gateway |
Core unreachable or returned an invalid response. |
Edge cases and caveats
Initial balance vs initial funding
AccountRequest.initialAmount is informational only. It doesn't move money. Always pair account creation with an explicit POST /internal-transfers or POST /accounts/{id}/deposits to actually fund the account.
Asynchronous status
POST /accounts, POST /persons, and POST /internal-transfers return 202 Accepted — ORCA has forwarded the request to the Core and the Core has acknowledged it, but the Core may still be propagating internal state. ORCA itself does not persist anything. If downstream operations require confirmed Core-side persistence, poll the resource (GET /accounts/{accountId}) until it reflects the expected state.
Duplicate detection
Run GET /persons or GET /organizations before every POST. The Core typically rejects duplicate SSN/TIN combinations with a 409, and the resulting state can be hard to recover from.
Related accounts must exist
relatedAccounts[] entries fail with 404 if any accountId doesn't exist at the time of the request. Create the linked accounts first.
supplementaryData scope
Flex fields are passed through to the Core. ORCA itself is stateless middleware and does not persist any data — what the Core accepts and stores is what survives. If the target Core does not have a place to put a given flex field, it will be silently dropped or rejected, depending on the Core's behavior. Coordinate with the integration team to confirm which flex fields are persisted by your target Core.
Related building blocks
- Documents API — Full document lifecycle, including share links and bulk export. See the Documents reference.
- Cards — Full card lifecycle is documented in Cards.
- Servicing flows — Post-onboarding money movement is covered in Customer Service.