Description
Counterparties & Contacts
The counterparties module manages reusable payment participants, their direct contact records, and the payment-source projections exposed through the normalized counterparty graph.
This module is the orchestration root for counterparty data. It can:
- create and update counterparties;
- create direct contacts or reassign existing contacts by ID;
- link existing bank accounts and crypto wallets by ID;
- create new bank accounts and crypto wallets inline through a recipient-backed flow;
- expose
own_accountson reads.
It does not support mutating own_accounts inline. Any non-empty own_accounts payload is currently rejected with 400.
Target model
| Entity | Role |
|---|---|
counterparty | Orchestration root. The only entity API consumers create, update, and address by ID. It owns contacts, controls which bank accounts and crypto wallets are linked, and exposes own_accounts in read responses. |
recipient | Internal execution entity. A backing row in recipients.recipients that is created lazily (once per counterparty) the first time an inline bank account or crypto wallet is created. It exists solely to satisfy the FK requirements of recipients.bank_accounts and crp.crp_recipients. It is never surfaced in the counterparties API response and must not be addressed directly by callers. |
The separation means:
- Callers think in terms of counterparties and their payment sources.
- The payment-routing layer continues to operate on recipients without any change.
- A counterparty may exist without a backing recipient (e.g. when it only links existing payment sources by
idor has no payment sources at all).
Core graph
Important rules
counterpartyis the participant root entity.contactsstay directly linked to the counterparty.- canonical email and phone data live in
contacts; counterparties do not expose top-levelemailorphonefields. - bank accounts and crypto wallets remain recipient-backed even when created from counterparties endpoints.
- inline bank / crypto creation ensures a backing recipient exists with an empty billing address placeholder before the recipient is inserted.
own_accountsare read-only in this module; mutate them in the dedicated accounts flows.- list endpoints follow the shared CoreBanq
getAllcontract. - read access is RBAC-scoped and customer isolation is enforced by the runtime.
- standalone contact creation requires parent counterparty update access and is executed transactionally.
Endpoints
Create Counterparty
POST /v1/counterparties
Creates a new counterparty within an owner customer scope.
In the same request you may:
- create direct contacts inline or reassign existing contacts by
id; - link existing bank accounts and crypto wallets by
id; - create new bank accounts and crypto wallets inline — those records are still created through a recipient-backed flow;
- persist arbitrary
metadata.
You may not mutate own_accounts here; any non-empty own_accounts array returns 400.
Nested contact objects support only id, type, value, and metadata. Use the standalone contacts API when you need to set validated or preferred.
Request highlights
- required:
name,owner_id,is_visible_to_others - ownership scope: provide
owner_idfor regular counterparties and for inline recipient-backed source creation - optional mirror-link: set
customer_idonly when the counterparty represents that same customer entity; when set,customer_idmust equalowner_idand is not used as the create scope - visibility contract: provide and read
is_visible_to_othersas the authoritative visibility flag;display_typeis no longer returned in counterparties API responses - optional:
type,alias,reference,avatar_id,contacts,bank_accounts,crypto_wallets,metadata - supported
typevalues:business,personal
Example
{
"owner_id": "8c6f6a0f-0b6e-46f2-996a-0f35e72cf3d8",
"name": "Acme Corp",
"is_visible_to_others": true,
"type": "business",
"alias": "acme",
"reference": "REF-001",
"contacts": [
{
"type": "email",
"value": "finance@acme.example",
"metadata": {}
}
],
"bank_accounts": [
{
"iban": "CH9300762011623852957",
"bank": "Acme Treasury Bank",
"is_primary": true,
"metadata": {
"source": "manual"
}
}
],
"metadata": {
"source": "manual"
}
}List Counterparties
GET /v1/counterparties
Returns counterparties in the standard project getAll envelope:
{
"data": [],
"total": 0,
"total_unfiltered": 0,
"has_more": false
}Each counterparty item keeps its public JSON shape stable: nullable scalar fields are returned explicitly as null, and collection fields are returned as empty arrays when there are no linked rows instead of being omitted from the payload.
Supported query pattern:
limitoffsetsortfilter(JSON object)search._textsearch.
Reserved text search supports the shared getAll operators such as .like, .start_with, and .end_with. For counterparties, _text searches both root counterparty fields and nested aggregate data from:
contactsbank_accountscrypto_walletsown_accountsaddressespayers
Common search fields for counterparties:
search.idsearch.namesearch.aliassearch.referencesearch.display_typesearch.typesearch.customer_idsearch.owner_idsearch.is_visible_to_otherssearch.contacts__typesearch.contacts__valuesearch.bank_accounts__ibansearch.bank_accounts__bicsearch.bank_accounts__banksearch.bank_accounts__is_primarysearch.bank_accounts__is_validatedsearch.crypto_wallets__wallet_addresssearch.crypto_wallets__networksearch.crypto_wallets__network_idsearch.crypto_wallets__currency_idsearch.crypto_wallets__is_primarysearch.own_accounts__ibansearch.own_accounts__currencysearch.own_accounts__status
Useful ownership patterns:
- only the mirror-linked “self” counterparty:
search.owner_id.eq=+search.customer_id.eq= - all owner-scoped counterparties except that mirror-linked self row:
search.owner_id.eq=+search.customer_id.ne=
search.customer_id.ne is null-safe for counterparties list filtering. Regular owner-scoped counterparties usually keep customer_id = null, and those rows are treated as “not equal” to a concrete customer UUID instead of being dropped by SQL null comparison semantics.
Get Counterparty by ID
GET /v1/counterparties/{id}
Returns the full counterparty aggregate, including:
contactsbank_accountscrypto_walletsown_accounts(read-only from this module)
Nullable scalar response fields stay present as null, and aggregate collections stay present as empty arrays when no linked records exist. Nested payment sources also keep products_allowed present as an array; it is empty when no product matrix projection has been attached. Address responses include address_hash.
Patch Counterparty
PATCH /v1/counterparties/{id}
Partially updates mutable scalar fields such as:
nametypealiasreferenceavatar_idactive(setfalsefor soft delete / deactivate)is_visible_to_othersmetadata
owner_id is the ownership / access-customer scope and is immutable through this generic PATCH endpoint. Sending owner_id in a patch request is rejected with 400; ownership transfer must be handled by a dedicated flow.
Counterparties responses no longer expose display_type; write requests and read responses use is_visible_to_others. For backward-compatible querying, search.display_type remains available as a derived filter.
Entity arrays use full-replace semantics when they are present in the payload:
contactsbank_accountscrypto_wallets
Rules:
- omit an entity array to leave that entity type unchanged;
- send an empty array to remove all linked entities of that type;
- for bank accounts / crypto wallets, an item with
idkeeps or updates an existing linked record; - for bank accounts, an item without
idbut with an IBAN matching an existing bank account on the same counterparty keeps / updates that existing account; - an item without
idcreates a new recipient-backed record inline; own_accountsremains unsupported on patch and returns400when provided.
IBAN reuse is checked across counterparties that share the same owner_id; counterparties under different owners may use the same IBAN. During patch, the target counterparty itself is excluded from this owner-scope check so unchanged bank-account payloads do not conflict with themselves.
As with create, nested contact payloads do not expose validated / preferred; use the standalone contacts API for those fields.
Contacts API
Create Contact
POST /v1/counterparties/contacts
Creates a contact linked to an existing counterparty.
Required fields:
counterparty_idtypevalue
Optional fields:
validatedpreferredmetadata
The service verifies parent counterparty update access before the insert is allowed and performs the write in a transaction.
When type = phone, metadata is normalized with phone-derived fields such as:
country_numbernumberregion_code
List Contacts
GET /v1/counterparties/contacts
Returns contacts in the same shared getAll response shape.
Common search fields for contacts:
search.idsearch.counterparty_idsearch.typesearch.valuesearch.validatedsearch.preferredsearch.active
Get / Update / Delete Contact
GET /v1/counterparties/contacts/{id}
PUT /v1/counterparties/contacts/{id}
DELETE /v1/counterparties/contacts/{id}
Contacts are addressed as standalone records after creation, but remain logically attached to their parent counterparty.
The update endpoint accepts partial changes for:
typevaluevalidatedpreferredmetadata
If the effective contact type is phone and the value changes, the phone-derived metadata is refreshed automatically.
Related artifacts
- OpenAPI source:
OpenAPI schema - Postman collection:
postman-collections/Corebanq - Counterparties.postman_collection.json
The Postman collection is maintained in the dedicated postman-collections repository, while the module documentation lives next to the module implementation inside modules/counterparties/docs.
Generate line chart data based on flexible query parameters
Returns counterparties visible to the requesting user in the standard Corebanq getAll response shape. For self-vs-others filtering, combine search.owner_id.eq with search.customer_id.eq or search.customer_id.ne; the not-equal variant is null-safe for nullable customer_id.