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:

  1. 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.

  2. 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.

  3. Save data.conversion_request.id, then poll GET /customers/{parentId}/banking/conversion-requests/{conversionRequestId} until data.conversion_request.status is Settled. Check GET /customers/{parentId}/banking/balance for enough Available GBP. A submitted conversion or its estimated receive amount is not spendable fiat.

  4. 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-Key and client_reference. Preserve that UUID and the request when retrying. amount_minor is a positive whole number of pence/cents; a quoted integer avoids JavaScript precision loss. amount_minor_string in the response is exact.

    You may also supply origin_exchange_order_id with 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:

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