neo-pays

Sub-merchants

Generated page, from the OpenAPI specification the gateway serves (GET /v1/openapi.json). Do not edit it by hand: it is rewritten on every build.

Method Path Description
GET /v1/sub-merchants List your sub-merchants
POST /v1/sub-merchants Create a sub-merchant
GET /v1/sub-merchants/{id} Retrieve a sub-merchant
PATCH /v1/sub-merchants/{id}/capabilities Change a sub-merchant's capabilities
POST /v1/sub-merchants/{id}/status Suspend or reinstate a sub-merchant
GET /v1/sub-merchants/{id}/kyb-file Retrieve a sub-merchant's KYB file
POST /v1/sub-merchants/{id}/kyb/documents Upload a KYB document
POST /v1/sub-merchants/{id}/kyb/persons Declare a beneficial owner or representative
POST /v1/sub-merchants/{id}/kyb/verification Open identity verification for a person
GET /v1/sub-merchants/{id}/api-keys List a sub-merchant's API keys
POST /v1/sub-merchants/{id}/api-keys Issue an API key for a sub-merchant
DELETE /v1/sub-merchants/{id}/api-keys/{keyId} Revoke a sub-merchant's API key

GET /v1/sub-merchants

List your sub-merchants

The merchants owned by the calling account. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Responses

  • 200 — The list of sub-merchants. (object[])

POST /v1/sub-merchants

Create a sub-merchant

Creates a merchant owned by the calling account. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

The parent is the CALLER and is never a field in the body: ownership is parent_client_id = caller, so a caller-supplied parent would be a tenancy check chosen by the party it constrains.

The create accepts NO capabilities: the sub-merchant is born with every capability off (refunds and TPE included), and a body that still carries capabilities is refused (422 capabilities_not_accepted) — set them afterwards with PATCH /v1/sub-merchants/{id}/capabilities, the one place they are managed. Nothing is dropped silently.

The pricing tier is not a choice — the child inherits the parent's tariff through the hierarchy. merchant_kind is almost always absent: a child inherits its parent's kind, and a kind that differs from the parent's is refused (422). The field exists so a caller can state what it believes and be corrected, never to choose.

Request body

Field Type Required Description
business_name string yes
trading_name string no
email string no
phone string no
country string no ISO alpha-2; empty = the parent's.
business_type string no
merchant_kind string no enterprise | sme; must equal the parent's.
metadata object no 20 keys at most; key ≤ 64, value ≤ 500 characters.

Responses

  • 201 — The sub-merchant that was created. (object)

GET /v1/sub-merchants/{id}

Retrieve a sub-merchant

The state of one child of the calling account. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Responses

  • 200 — The sub-merchant. (object)

PATCH /v1/sub-merchants/{id}/capabilities

Change a sub-merchant's capabilities

Nine capabilities are delegable, each switchable on or off, each under the parent's matching aggregator_manage_* right: tpe_enabled, cnp_enabled, mobile_money_enabled, internal_transfers_enabled, pi_enabled (BCEAO instant payments, under aggregator_manage_pi — recall has no capability of its own: the can_recall key scope rides pi_enabled), payout_enabled (under aggregator_manage_payout), refund_enabled (under aggregator_manage_refund), collection_accounts_enabled (under aggregator_manage_collection — the child's collection account is then opened automatically once its own KYB is approved) and settlement_schedule (the whole settlement schedule — the switch alone is refused without a cycle — and never the destination account). ⚠️ The child's own flag still decides at every gate: the parent asks, the child's capability and kyc_status answer. ⚠️ internal_transfers_enabled grants the child its OWN transfers; moving a child's money stays under aggregator_manage_payout. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Request body

Field Type Required Description
tpe_enabled boolean no
cnp_enabled boolean no
mobile_money_enabled boolean no
internal_transfers_enabled boolean no
pi_enabled boolean no
payout_enabled boolean no
refund_enabled boolean no
collection_accounts_enabled boolean no
settlement_schedule object no L'horaire de règlement, en bloc.
settlement_schedule.auto_settlement_enabled boolean no
settlement_schedule.settlement_cycle string no
settlement_schedule.settlement_delay_days integer no
settlement_schedule.settlement_day_of_week integer no 0–6, dimanche = 0 (hebdomadaire)
settlement_schedule.settlement_day_of_month integer no 1–28 (mensuel)
settlement_schedule.settlement_time_of_day string no HH:MM
settlement_schedule.settlement_min_minor string no unités mineures

Responses

  • 200 — The updated sub-merchant. (object)

POST /v1/sub-merchants/{id}/status

Suspend or reinstate a sub-merchant

Changes a child's state. The reason is retained. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Request body

Field Type Required Description
status string yes active | suspended.
reason string no

Responses

  • 200 — The updated sub-merchant. (object)

GET /v1/sub-merchants/{id}/kyb-file

Retrieve a sub-merchant's KYB file

The child's compliance file: documents expected, documents received, people declared, and the verdict. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Responses

  • 200 — The KYB file.

POST /v1/sub-merchants/{id}/kyb/documents

Upload a KYB document

⚠️ multipart/form-data, not JSON — the document is a file. 10 MB maximum. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Which documents are expected depends on the child's country of registration; GET /v1/sub-merchants/{id}/kyb-file enumerates the ones still outstanding.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Request body

⚠️ multipart/form-data, not JSON.

Field Type Required Description
file file yes The file, 10 MB at most.
doc_type string yes The document's code, as enumerated by kyb-file.
doc_number string no
doc_country string no ISO alpha-2.
issued_at string no

Responses

  • 201 — The document, once stored.

POST /v1/sub-merchants/{id}/kyb/persons

Declare a beneficial owner or representative

Declares a person on the child's file: beneficial owner, legal representative, director. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Request body

Field Type Required Description
person_type string yes The role declared on the file.
phone string no
email string no
role string no
ownership_pct number no Ownership percentage.
is_ubo boolean no Beneficial owner.
is_pep boolean no Politically exposed person.

Responses

  • 201 — The person, once declared.

POST /v1/sub-merchants/{id}/kyb/verification

Open identity verification for a person

Opens a verification session for a person already declared. The compliance portal resolves sessions by contact identifier, which is why phone / email sit beside person_id. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Request body

Field Type Required Description
person_id string yes
phone string no
email string no

Responses

  • 201 — The verification session that was opened.

GET /v1/sub-merchants/{id}/api-keys

List a sub-merchant's API keys

The keys THIS account issued for this sub-merchant — metadata and effective scopes, never a secret. Keys the sub-merchant created itself are not listed: they are not the parent's. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section. These three routes ALSO require the account's aggregator_manage_api_keys right (granted by neo-pays; 403 sub_merchant_api_keys_not_enabled otherwise) and, on the calling key, BOTH the can_manage_sub_merchants and can_manage_sub_merchant_api_keys scopes (403 insufficient_scope otherwise).

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Responses

  • 200 — The keys this account issued for this sub-merchant. (object[])
  • 404 — No sub-merchant of this account has this id.

POST /v1/sub-merchants/{id}/api-keys

Issue an API key for a sub-merchant

Issues an API key that belongs to the SUB-MERCHANT: it authenticates as the child, never as the parent. The secret (secret_key, and signing_secret when signing is required) is returned ONCE, in this response (Cache-Control: no-store); only its hash is kept.

The scopes are bounded by the SUB-MERCHANT's capabilities, not the parent's: a scope the child cannot exercise is refused (409 precondition_failed, naming the capability). can_use_pi and can_recall are therefore grantable as soon as the child has pi_enabled; can_recall implies can_use_pi.

⚠️ An unknown or misspelt scope is refused (422 unknown_scope), never ignored. The reserved scopes — can_use_tontine_pi, can_manage_sub_merchants, can_manage_collection_accounts and can_manage_sub_merchant_api_keys — can NEVER be carried by a sub-merchant key (403 reserved_scope).

A live key requires a sub-merchant in production (is_live); otherwise issue a test key. The issue is recorded on the sub-merchant's audit log, in the parent's name. ⚠️ Aggregator accounts only: an account that is not an aggregator (is_aggregator) receives 403 on every route in this section. These three routes ALSO require the account's aggregator_manage_api_keys right (granted by neo-pays; 403 sub_merchant_api_keys_not_enabled otherwise) and, on the calling key, BOTH the can_manage_sub_merchants and can_manage_sub_merchant_api_keys scopes (403 insufficient_scope otherwise).

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.

Request body

Field Type Required Description
name string no
key_type string no test by default. live requires a sub-merchant in production.
scopes object no Scope name → boolean. An absent scope is false.
scopes.can_initiate_payment boolean no
scopes.can_payout boolean no
scopes.can_refund boolean no
scopes.can_read_transactions boolean no
scopes.can_manage_webhooks boolean no
scopes.can_read_customers boolean no
scopes.can_write_customers boolean no
scopes.can_manage_risk_lists boolean no
scopes.can_read_collection_accounts boolean no
scopes.can_use_pi boolean no
scopes.can_recall boolean no
scopes.can_cnp boolean no
expires_in_days integer no 0 = no expiry.
require_signature boolean no Require request signing (X-NeoPays-Signature) for this key. Permanent for the key's life.

Responses

  • 201 — The issued key, and its secret — shown once. (object)
  • 403 — Right, scope or reserved scope refused.
  • 404 — No sub-merchant of this account has this id.
  • 409 — The sub-merchant cannot exercise a requested scope, or is not in production for a live key.
  • 422 — Unknown scope or invalid value.

DELETE /v1/sub-merchants/{id}/api-keys/{keyId}

Revoke a sub-merchant's API key

Immediate, permanent revocation of a key THIS account issued for this sub-merchant; it is refused from the next call. A key the parent did not issue receives the same 404 as an unknown one. Optional body {"reason": "…"}, kept with the revocation and on the sub-merchant's audit log. ⚠️ Aggregator accounts only: the calling key must carry can_manage_sub_merchants. Unlike issue and list, revoking needs NEITHER the aggregator_manage_api_keys right NOR the can_manage_sub_merchant_api_keys scope: a parent that has lost them can still revoke the keys it issued. When the link ends (unlinked, moved to another parent, closed), those keys are revoked automatically.

Parameters

Name In Required Description
id path yes The sub-merchant's PUBLIC id (a UUID), as returned by create and list. ⚠️ This is not the internal id, which never leaves the platform.
keyId path yes The key's id, as returned by the create and by the list of the sub-merchant's keys.

Request body

Field Type Required Description
reason string no

Responses

  • 204 — Key revoked.
  • 404 — No active key issued by this account for this sub-merchant has this id.