Sub-customers & parent access
A parent uses its own API key to onboard sub-customers, manage their beneficiaries and
withdraw from their fiat balances. Sub-customer in the dashboard and end customer in
the API mean the same thing. The route segment remains end-customers.
This walkthrough follows a personal sub-customer through a GBP withdrawal. The parent makes the API calls from its backend; the child completes their own verification. The bank account, beneficiary, funds and withdrawal belong to the child.
1. Authenticate as the parent
Examples use PARENT_API_KEY for the key and PARENT_ID for the parent ID returned by
/me. Replace the example business references for each new operation.
Keep the parent's API key on your server. Send X-Api-Key: <parent key> on every call;
Authorization: Bearer <parent key> is an alternative. Do not send both, or X-Tenant-Id.
curl 'https://api.kwiikpay.io/api/v1/partner/me' \
-H "X-Api-Key: ${PARENT_API_KEY}"
For a parent-bound key, data.customer_id is the parent ID, data.subject_binding is
subject, and data.delegated_customer_ids lists its direct sub-customers. No separate
child key is needed. Delegation permits only the parent's own direct children in the same
tenant. An unrelated or unknown customer returns the same 404. A sub-customer cannot
create another level of sub-customers.
Creating a child, starting verification and issuing their account require end-customer
onboarding to be enabled for both the tenant and the parent. Missing eligibility
returns 403 sub_customer_onboarding_disabled; contact your account manager to enable
the programme. Scopes alone do not enable it. Existing child reads remain available if
that onboarding permission is later revoked.
| Operation | Required scope |
|---|---|
| Create child, start verification, consolidate or transfer funds | end-customers:write |
| List/read children, documents, internal transfers and nested balance summary | end-customers:read |
| Poll child's onboarding status | onboarding:read |
| Create account; create, replace or disable beneficiary | accounts:write |
| Read accounts and beneficiaries | accounts:read |
| Read child's available banking balance | balances:read |
| Read child's fee configuration | fees:read |
| Submit/list fiat withdrawals | withdrawals:write / withdrawals:read |
| Create/read conversions, when available | exchanges:write / exchanges:read |
Every mutating call requires an Idempotency-Key of 8–256 visible ASCII characters.
Use a different key for each intended operation; preserve the key and exact body for
retries. See Authentication & scopes and
Idempotency.
2. Use the right customer ID
Paths below are relative to https://api.kwiikpay.io/api/v1/partner.
| Purpose | Path | IDs |
|---|---|---|
| Create/list children | /customers/{parentId}/end-customers |
Parent |
| Read child | /customers/{parentId}/end-customers/{childId} |
Parent and child |
| Start verification | /customers/{parentId}/end-customers/{childId}/onboarding |
Parent and child |
| Create/list child accounts | /customers/{parentId}/end-customers/{childId}/accounts |
Parent and child |
| Read verification status | /customers/{childId}/onboarding/status |
Child |
| Manage beneficiaries | /customers/{childId}/banking/beneficiaries |
Child |
| Read available funds | /customers/{childId}/banking/balance |
Child |
| Read fees | /customers/{childId}/fees |
Child |
| Create/list fiat withdrawals | /customers/{childId}/banking/withdrawals |
Child |
The reference uses {customerId} for both kinds of route: on an end-customers route it
means the parent; on the child's banking route it means the child. Keep both IDs. Putting
the parent ID in a banking URL operates on the parent's own resources.
3. Create and verify the child
curl -X POST \
"https://api.kwiikpay.io/api/v1/partner/customers/${PARENT_ID}/end-customers" \
-H "X-Api-Key: ${PARENT_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: child-create-customer-1042' \
-d '{
"subject_type": "Personal",
"external_reference": "customer-1042",
"display_name": "Jane Doe",
"country_code": "GB"
}'
Save data.end_customer.id as SUB_CUSTOMER_ID and verify its parent_customer_id.
The initial lifecycle is Onboarding. Reusing external_reference for the same parent
and subject type returns the existing child; it does not update them. This business
reference is separate from the required HTTP Idempotency-Key header.
For a company, use subject_type: "Business" and supply legal_name; also supply its
registration_number for verification. Listing children returns data.end_customers;
reading one returns data.end_customer.
Start verification for the personal child:
curl -X POST \
"https://api.kwiikpay.io/api/v1/partner/customers/${PARENT_ID}/end-customers/${SUB_CUSTOMER_ID}/onboarding" \
-H "X-Api-Key: ${PARENT_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: child-onboarding-customer-1042' \
-d '{
"contact_email": "jane.doe@example.com",
"return_url": "https://partner.example.com/verification/complete"
}'
contact_email is required for everyone. A business also requires business_profile:
company_name, registration_number, country_code, registered_address and at least
one associated_parties entry. The API reference contains the nested
request schema. Do not send a business profile for a personal child.
Use data.verification_link or initialise the verification SDK with data.token when
present. A null token can mean already verified or held for review; do not initialise
the SDK with null. The session's status is verified or pending.
Poll GET /customers/{childId}/onboarding/status until data.customer_status is Active.
This read refreshes verification; listing children is not a substitute. See
Customers & onboarding for token and lifecycle handling.
4. Issue and fund the child's account
curl -X POST \
"https://api.kwiikpay.io/api/v1/partner/customers/${PARENT_ID}/end-customers/${SUB_CUSTOMER_ID}/accounts" \
-H "X-Api-Key: ${PARENT_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: child-gbp-account-customer-1042' \
-d '{"currency":"GBP"}'
Use the nested sub-customer routes for account issuance and verification sessions.
Generic /customers/{childId}/banking/accounts and /customers/{childId}/onboarding/kyc|kyb/initiate|token
requests return 409 sub_customer_dedicated_route_required; they cannot bypass the parent programme checks.
This issuance route offers GBP and EUR OpenPayd virtual accounts. The parent must already hold a completed account in that currency, and the child's verification must be approved. The request does not select a provider or account type.
Save data.account.id. Poll
GET /customers/{parentId}/end-customers/{childId}/accounts/{accountId} until
data.account.status is Completed and provider_account_id is populated. Fund the
child using the bank details returned for that account.
GET /customers/{childId}/banking/balance returns data.balances with currency,
balance_minor and balance_major for Available funds. The nested
GET /customers/{parentId}/end-customers/{childId}/balance reports total_amount and
asset_scale; that total is not a promise that all funds can be spent. With several
funding accounts, an aggregate balance also does not guarantee enough funds in the
account selected for a payment. The withdrawal checks its actual source and funds.
Credit the child from the parent's balance
A parent can credit a specific GBP or EUR amount to its own child through
POST /customers/{parentId}/end-customers/{childId}/internal-transfers. Use
direction: "parent_to_end_customer". The same amount leaves the parent's Available
balance and enters the child's Available balance in one transaction. No beneficiary is
needed for this internal transfer.
For a crypto deposit received by the parent, follow this sequence:
Receive crypto at the parent's deposit address and wait until it is confirmed and spendable. The balance route is
GET /customers/{parentId}/crypto/wallets/{asset}/balance.Convert the crypto on the parent's account. For example, this requests conversion of 1,000 USDC (six decimal places) to GBP:
curl -X POST \ "https://api.kwiikpay.io/api/v1/partner/customers/${PARENT_ID}/banking/conversion-requests" \ -H "X-Api-Key: ${PARENT_API_KEY}" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: parent-usdc-gbp-conversion-1042' \ -d '{"from_currency":"USDC","to_currency":"GBP","amount_minor":1000000000}'The conversion amount is in the source asset's atomic units. Currency pair, custody, network, liquidity and fee controls still apply. See Conversions.
Save
data.conversion_request.id, then pollGET /customers/{parentId}/banking/conversion-requests/{conversionRequestId}untildata.conversion_request.statusisSettled. CheckGET /customers/{parentId}/banking/balancefor enough Available GBP. A submitted conversion or its estimated receive amount is not spendable fiat.Transfer the exact amount to the child. This example credits GBP 250.00, subject to at least GBP 250.00 being available on the parent:
curl -X POST \ "https://api.kwiikpay.io/api/v1/partner/customers/${PARENT_ID}/end-customers/${SUB_CUSTOMER_ID}/internal-transfers" \ -H "X-Api-Key: ${PARENT_API_KEY}" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 785a23b2-a916-4f29-b8ed-0dcb4412fb09' \ -d '{"direction":"parent_to_end_customer","currency":"GBP","amount_minor":"25000","client_reference":"785a23b2-a916-4f29-b8ed-0dcb4412fb09"}'Generate a new non-empty UUID for each intended transfer and use it in both
Idempotency-Keyandclient_reference. Preserve that UUID and the request when retrying.amount_minoris a positive whole number of pence/cents; a quoted integer avoids JavaScript precision loss.amount_minor_stringin the response is exact.You may also supply
origin_exchange_order_idwith the saved conversion ID. The order must belong to this parent, be settled, and have a fiat target matching the transfer currency. This is an audit link; it does not convert crypto, reserve conversion proceeds, or limit the transfer to that order's proceeds. The transfer uses the parent's currently available balance.
The tenant and parent must both have parent-child internal transfers enabled. This is a separate capability from onboarding permission. Both parties must be active, pass compliance and freeze controls, and have exactly one eligible completed pooled OpenPayd account in the same currency and provider configuration, with an enabled matching transfer policy. Per-account, mirror, adopted and mixed account setups are ineligible. Limits apply to the source. This route transfers GBP/EUR only; it does not allocate deposited crypto directly to the child.
On 200, data.internal_transfer.status is completed: both balance changes and
the durable transfer record have committed. Save id, journal_entry_id and
client_reference. The child can then use those Available funds for its approved
beneficiary withdrawal, including any applicable withdrawal fee.
After a timeout, recover the result with the same parent key:
curl "https://api.kwiikpay.io/api/v1/partner/customers/${PARENT_ID}/end-customers/${SUB_CUSTOMER_ID}/internal-transfers/by-client-reference/785a23b2-a916-4f29-b8ed-0dcb4412fb09" \
-H "X-Api-Key: ${PARENT_API_KEY}"
You can also read …/internal-transfers/{transferId}. A recovery 404 can occur while
the original request is still in flight; retry the original POST with the same UUID
and body, rather than starting a second transfer. A completed identical request
returns the same transfer; different parameters with that UUID return
409 internal_transfer_client_reference_reused. Recovery reads remain available when
write capability is later disabled, subject to current ownership and API-key access.
GET …/internal-transfers?take=50 returns recent transfers in both directions, including
dashboard-created transfers, newest first. take is clamped to 1–200; this is a bounded
recent-history list without page/cursor navigation. For the reverse direction use
end_customer_to_parent; the same controls apply, and origin_exchange_order_id must
be omitted.
Common refusals include 403 parent_child_internal_transfers_disabled,
409 internal_transfer_party_not_active, 409 internal_transfer_accounts_ineligible,
409 internal_transfer_policy_unavailable, 409 internal_transfer_origin_exchange_invalid,
422 internal_transfer_limit_exceeded and 409 consolidation_insufficient_balance.
They have flat error bodies. Unknown or unrelated parent/child IDs return 404.
After a refusal, keep the original instruction with its UUID: retained limit decisions are bound to that instruction. Changing the child, direction, currency, amount or exchange link requires a new UUID after confirming no transfer completed. Identical retries may reuse the original limit decision within the same UTC day, including after limit-configuration changes; they still pass current ownership, capability, lifecycle, compliance, freeze, account eligibility and available-balance checks.
An uncompleted request with a decision from an earlier UTC day returns
409 internal_transfer_limit_decision_stale. Recover by client reference first; after
confirming no transfer completed, submit a new UUID so today's limits apply. Completed
transfers still replay and recover across days without moving money again. A legacy
decision without an instruction fingerprint is refused with
internal_transfer_client_reference_reused; recover first before a fresh UUID.
5. Add a beneficiary and wait for approval
Create the receiving bank account under the child's ID. A personal GBP beneficiary
on OpenPayd needs the account holder's name in attributes as well as bank details:
curl -X POST \
"https://api.kwiikpay.io/api/v1/partner/customers/${SUB_CUSTOMER_ID}/banking/beneficiaries" \
-H "X-Api-Key: ${PARENT_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: child-beneficiary-jane-gbp-001' \
-d '{
"currency": "GBP",
"name": "Jane Doe",
"country": "GB",
"sort_code": "<RECIPIENT_SORT_CODE>",
"account_number": "<RECIPIENT_ACCOUNT_NUMBER>",
"rail": "FASTER_PAYMENTS",
"attributes": {
"beneficiary_first_name": "Jane",
"beneficiary_last_name": "Doe"
}
}'
Replace the example identity and bank placeholders with the actual recipient's details.
An OpenPayd company beneficiary uses attributes.beneficiary_company_name. EUR uses an
IBAN; other rails have additional requirements. See Beneficiaries
before building a recipient form.
Creation returns data.beneficiaries, an array, with one result per selected provider.
Inspect and retain each result's ID and status. Neither the first entry nor HTTP 200
proves that a payable beneficiary exists. Select one in the withdrawal's currency.
Poll GET /customers/{childId}/banking/beneficiaries/{beneficiaryId} until:
data.beneficiary.statusisCompleted, with a provider reference.data.beneficiary.approval_statusisApproved.- For requested OpenPayd registrations,
data.beneficiary.bank_registration.statusisRegistered.
Approval registers the recipient under the child's OpenPayd linked business, even when the
parent submits the request. Send the recipient's real company or personal names, not the
parent's name unless the parent is actually the recipient. Registration must finish before
withdrawal; the approval webhook alone does not establish bank readiness. Use the returned
supported_schemes when selecting the withdrawal rail.
New beneficiaries need staff approval; the parent cannot approve one through this
API. A parent's or sibling's beneficiary cannot receive this child's withdrawal.
GET .../beneficiaries lists the child's payees; PATCH .../{beneficiaryId} replaces
one and returns new records; DELETE .../{beneficiaryId} disables one. Use the replacement's
ID and wait for its readiness and approval before paying it. OpenPayd rows whose bank
registration has started cannot be replaced through PATCH; contact support to change them.
6. Withdraw from the child
The parent supplies the child ID and an approved beneficiary belonging to that child.
This example pays GBP 100.00: amount is a positive whole number of minor units.
curl -X POST \
"https://api.kwiikpay.io/api/v1/partner/customers/${SUB_CUSTOMER_ID}/banking/withdrawals" \
-H "X-Api-Key: ${PARENT_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: child-withdrawal-order-1042' \
-d '{
"beneficiary_id": "<APPROVED_CHILD_BENEFICIARY_UUID>",
"currency": "GBP",
"amount": 10000,
"rail": "FASTER_PAYMENTS",
"description": "Invoice ABC"
}'
description is the reference on the recipient's statement. Follow the
rail and reference rules. If the child's disclosure policy
requires it, include transparency as documented on the withdrawal request; its nested
fields use camelCase. Declare the actual relationship and underlying parties. The use of
a parent API key does not itself determine that declaration.
The child's available funds cover the amount plus its effective fee; the parent's
balance is not a fallback. Read the child's fee configuration at
GET /customers/{childId}/fees. The withdrawal applies the effective fee and checks funds
at submission. Both parent and child must be active and pass applicable compliance checks.
Beneficiary approval, source-account eligibility, limits and client-money restrictions
still apply. SWIFT has separate availability and accepted-fee requirements; the example
above uses Faster Payments.
Read the child's deposits and payer details
Use the child's id with the parent's key and the deposits:read scope:
curl "https://api.kwiikpay.io/api/v1/partner/customers/${SUB_CUSTOMER_ID}/banking/deposits?page=1&limit=100" \
-H "X-Api-Key: ${PARENT_API_KEY}"
curl "https://api.kwiikpay.io/api/v1/partner/customers/${SUB_CUSTOMER_ID}/banking/deposits/${DEPOSIT_ID}" \
-H "X-Api-Key: ${PARENT_API_KEY}"
The list uses data.deposits; the detail uses data.deposit. On fiat deposits, sender
is the stored provider-reported payer name and reference is the incoming payment reference.
Either can be null when not captured. provider_reference is the provider transaction id.
amount is in minor units: divide by 10^asset_scale.
For crypto, use /crypto/deposits and /crypto/deposits/{depositId}. reference is the
transaction hash and sender is null. A parent's key cannot read another parent's child.
The parent's webhook may receive the child's deposit.received, with the child in
data.customer_id. Its customer_email describes that child when available, never the payer
or the parent. Re-read a historical deposit to recover metadata added by reconciliation;
replaying an old webhook returns the original payload. See reconciliation.
7. Track the outcome and retry safely
Save data.withdrawal.id and inspect data.withdrawal.status. HTTP 200 can contain a
failed withdrawal. Even "Withdrawal created successfully" is not proof of payment.
| Status | Meaning | Action |
|---|---|---|
PendingProvider |
Submitted; awaiting the bank's outcome | Track the same withdrawal |
Completed |
Paid | Mark the payment complete |
Failed |
Payment failed | Record the failure and check its outcome before any new payment |
ManualReview |
Review required. A limit review can occur before funds are reserved; an ambiguous provider submission can retain a hold | Track the same withdrawal and available balance; wait for resolution and do not submit a replacement |
A staff approval does not override current customer, parent, compliance or beneficiary controls. If dispatch is still blocked, retain the original withdrawal ID; staff can retry that same withdrawal after the block is resolved. Approval alone does not mean the withdrawal was submitted.
curl \
"https://api.kwiikpay.io/api/v1/partner/customers/${SUB_CUSTOMER_ID}/banking/withdrawals?page=1&limit=100" \
-H "X-Api-Key: ${PARENT_API_KEY}"
Treat any other status as unfinished until a final outcome is confirmed.
Find the saved ID in data.withdrawals; follow data.pagination when needed. The list
contains only the child's fiat withdrawals. A webhook endpoint registered using the
parent-bound key can receive attributable events for the parent and its direct children.
Subscribe to the relevant withdrawal events and verify signatures as described in
Webhooks. Keep polling available for reconciliation.
Before sending, persist the child ID, beneficiary ID, complete body and idempotency key. If the response is lost, times out or leaves the outcome uncertain, retry the same URL with the same key and unchanged body. Do not regenerate the key, change the amount or create a replacement while the first payment's outcome is unknown. A retry is not a fresh status read; use the withdrawal list for current status.
| Refusal | What to check |
|---|---|
403 api_key_not_authorized |
Required API-key scope |
403 sub_customer_onboarding_disabled |
Tenant and parent onboarding eligibility |
404 |
Parent/child ownership or resource ID |
422 beneficiary_not_found |
Beneficiary belongs to this child |
409 beneficiary_not_ready / beneficiary_not_approved |
Provider readiness and staff approval |
409 source_account_not_ready |
Eligible completed child account at the beneficiary's provider and currency |
409 parent_not_active |
Parent's active lifecycle |
403 compliance_subject_blocked / 409 compliance_subject_manual_review |
Compliance restriction or review |
503 parent_compliance_unavailable |
Retry later with the original request; parent compliance could not be checked |
409 withdrawal_client_money_blocked / source_account_pot_missing |
Funding-account restrictions or incomplete setup |
Error bodies differ between middleware, validation and policy refusals; see
Errors. A generic 409 fiat_withdrawal_failed, timeout or server error
does not prove that no payment exists. Preserve the attempt while reconciling it.
8. Other parent interactions
- Exchange: use
/customers/{childId}/banking/conversion-requestswith the parent key and exchange scopes. It uses the child's balances and requires an enabled pair, ready source/destination accounts or wallets and applicable controls. See Conversions; not every pair is available. - Move funds to the parent:
POST /customers/{parentId}/end-customers/{childId}/consolidatetakescurrencyandamount_minor. New consolidations support GBP/EUR and apply the same tenant/parent transfer capability, both-party lifecycle and compliance, freeze, limits and matching OpenPayd master-pool policy as internal transfers. Both sides need compatible completed accounts. It shares the child's daily transfer allowance with/internal-transfers. Preserve the originalIdempotency-Key(up to 256 visible ASCII characters, without spaces); a completed legacy request can be recovered using that key even after eligibility changes. An uncompleted prior-day limit decision requires recovery first, then a new key only if no consolidation completed. It is not a prerequisite for paying a child's external beneficiary. - Verification documents: list/download through
/customers/{parentId}/end-customers/{childId}/documentswithend-customers:read. Downloads return file bytes, not a JSON envelope. - Dashboard: open Sub-customers → select the customer → Fiat withdrawals → Withdraw fiat. It operates on the same child-owned accounts, beneficiaries and withdrawals.