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 & creationPerson and Organization profiles with duplicate prevention
  • Account creation & updates — opening new accounts with complex ownership structures, modifying them after creation
  • Complex party-role configurationAccountOwner, 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-transfers or /accounts/{id}/deposits
  • Post-creation servicing — debit card issuance, account attribute updates, KYC document upload
  • Custom data — flex fields via supplementaryData for 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 /accounts202 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-transfers202 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}/deposits202 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.