From 18ea3c42b0d0a75891ff45ce0058d610a61bebde Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 5 Aug 2026 17:25:04 -0300 Subject: [PATCH 1/9] docs(repo): clarify delegated profile request context --- ...posal-headless-profiles-and-pricing-plans.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/proposal-headless-profiles-and-pricing-plans.md b/docs/proposal-headless-profiles-and-pricing-plans.md index fdeecce3c..9d85ac795 100644 --- a/docs/proposal-headless-profiles-and-pricing-plans.md +++ b/docs/proposal-headless-profiles-and-pricing-plans.md @@ -84,8 +84,21 @@ Authorization must verify all of the following: 3. The requested corridor is enabled for the manager when the operation is corridor-bound. 4. The requested operation is part of the explicit control list below. -The request context must retain both `actorProfileId` and `subjectProfileId`. It must not -replace the authenticated manager ID globally or introduce a generic impersonation mode. +The operation executes for the child, so existing services may need to treat the child as +the effective profile for ownership and provider resolution. The manager must still be +preserved as the authenticated actor used to authorize and audit the delegated operation. + +The minimum safe implementation reuses the existing child-oriented services with two +separate request-context values: + +```text +authenticatedManagerProfileId = managerId +effectiveUserId = childId +``` + +Authorization uses the authenticated manager and its direct relationship to the child. +Existing ownership and provider resolution use the effective child. Both values remain +available for audit attribution; the effective child must never erase the manager actor. Manager sessions are not required for the first implementation. Supporting only secret API credentials keeps the delegated surface smaller; session support can be added if a From 30dbda209bb85a26ce25861aed6a7b4f9fb04e8e Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 5 Aug 2026 17:25:17 -0300 Subject: [PATCH 2/9] docs(repo): propose unified KYC and KYB API --- docs/README.md | 1 + docs/proposal-unified-kyc-kyb-api.md | 122 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 docs/proposal-unified-kyc-kyb-api.md diff --git a/docs/README.md b/docs/README.md index fca7a0954..87195a104 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,7 @@ The smaller set of general project documents stays directly in `docs/`: | [`product-dashboard.md`](product-dashboard.md) | Current dashboard product scope and acknowledged gaps | | [`proposal-headless-profiles-and-pricing-plans.md`](proposal-headless-profiles-and-pricing-plans.md) | Active proposal for delegated management of headless customer profiles | | [`proposal-mcp-server.md`](proposal-mcp-server.md) | Active, non-authoritative discussion draft | +| [`proposal-unified-kyc-kyb-api.md`](proposal-unified-kyc-kyb-api.md) | Early proposal for API-driven, provider-neutral customer verification | The root [`README.md`](../README.md) is human onboarding, [`MAP.md`](../MAP.md) is repository wayfinding, and `CLAUDE.md` files contain instructions for coding agents. diff --git a/docs/proposal-unified-kyc-kyb-api.md b/docs/proposal-unified-kyc-kyb-api.md new file mode 100644 index 000000000..c8689b466 --- /dev/null +++ b/docs/proposal-unified-kyc-kyb-api.md @@ -0,0 +1,122 @@ +# Proposal: Unified KYC and KYB API + +Status: proposed, early discussion draft. This document currently seeks agreement on +scope, invariants, and delivery order. Exact routes, schemas, and provider-specific field +contracts remain open. Last updated: 2026-08-05. + +Related material: + +- [`Proposal: Managed Headless Profiles`](proposal-headless-profiles-and-pricing-plans.md) +- [`Identity, Customer, and Partner Model`](architecture-identity-model.md) +- [`Avenia KYB Level 1 - API`](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) +- [`Avenia KYB Level 1 - Web SDK`](https://integration-guide.avenia.io/docs/KYB/kybLevel1) + +## Objective + +Allow a customer, or an authorized manager acting for a managed headless customer, to +complete corridor-supported KYC or KYB through the Vortex API without requiring the +Vortex dashboard, widget, or a provider-hosted onboarding UI. + +In parallel, replace the current collection of provider-named onboarding endpoints with +the smallest practical common API. The API should select the provider from the corridor +and customer type, expose Vortex-owned verification resources and canonical statuses, +and retain provider-specific input only where the underlying checks genuinely differ. + +The first delivery item is Avenia's new API-based Level 1 KYB flow. Today, Vortex starts +Avenia company KYB through the Web SDK endpoint and sends the customer to separate hosted +company and representative URLs. The new provider flow lets Vortex submit company data, +UBOs, and documents server-to-server and track the resulting attempt. + +## Initial scope + +- Focus on Avenia and Alfredpay. +- Start with Avenia Level 1 KYB for the BRL corridor. +- Support both self-service profiles and manager-to-child delegated operations as the + managed-headless-profile authorization work becomes available. +- Keep the resource model and provider boundary suitable for another future integrator. +- Do not redesign Monerium or other provider flows in this proposal. +- Do not let callers approve a case, override a provider decision, or write canonical + compliance status directly. + +"API-driven" means that an integrator can collect data in its own experience and perform +the workflow through Vortex API operations. Pre-signed document uploads and unavoidable +identity/liveness steps may still involve a provider-controlled URL, but the flow must not +depend on a Vortex UI. + +## Existing foundation + +The persistence model is already mostly provider-neutral: + +```text +profile + -> customer entity + -> provider customer + -> KYC/KYB case +``` + +`provider_customers` owns the durable corridor/provider account, while `kyc_cases` owns a +verification attempt and its canonical `started`, `pending`, `in_review`, `approved`, or +`rejected` status. This model should be reused rather than introducing a second onboarding +or compliance identity. + +The current API is less unified than the storage model: + +- most Avenia and all Alfredpay KYC/KYB routes require a Supabase browser session; +- route names, request shapes, document handling, retries, and status responses expose + provider workflow details; +- the dashboard orchestrates separate provider XState machines and polls the aggregated + `GET /v1/onboarding/status` read model; +- no current request context distinguishes a manager actor from a managed child subject. + +## Required invariants + +- The operation subject is always a profile and its customer entity. Provider accounts + and cases belong to that subject, never to a manager. +- A self-service request may use the profile's accepted authentication methods. A + delegated request must preserve both `actorProfileId` and `subjectProfileId` and pass the + direct manager-child and corridor checks defined by the headless-profiles proposal. +- The server derives the provider from corridor and customer type. A caller cannot select + an arbitrary provider account or provider case belonging to another subject. +- Public responses use Vortex case identifiers and canonical status. Provider identifiers + stay internal unless a specific continuation step requires an opaque reference. +- Provider-specific data is represented explicitly rather than forced into a misleading + lowest-common-denominator schema. +- Document operations are scoped to the subject, provider customer, case, and expected + document type before Vortex issues an upload target or forwards content. +- Provider-confirmed state remains authoritative. Client completion events cannot mark a + case approved. + +## Delivery order + +1. Implement Avenia Level 1 KYB through its API flow: create or reuse the company + subaccount, create and upload company and UBO documents, register UBOs, submit the KYB + attempt, and synchronize its result into the existing provider customer and KYB case. +2. Use that vertical slice to define the common Vortex case lifecycle and operations for + starting, continuing, submitting, reading, and retrying verification. +3. Make those operations available to self-service API credentials and to manager + credentials acting on an authorized managed child, without changing resource ownership. +4. Adapt Alfredpay's API-based KYC/KYB flows to the same lifecycle while retaining its + corridor-specific forms, document sets, and hosted-flow exceptions. +5. Migrate first-party UI consumers, then retire provider-named public onboarding routes + only after compatibility requirements are known. + +The first item must not wait for the complete cross-provider API design. It should reuse +the current canonical tables and status rules so the Avenia work becomes the first adapter +behind the unified API rather than a parallel compliance model. + +## First open decisions + +- What is the smallest common resource shape: one provider customer with a current case, + or an explicit append-only list of attempts? +- Which operation vocabulary fits both providers without hiding meaningful differences? +- Should Vortex proxy document bytes, issue provider pre-signed upload URLs, or support + both patterns behind one document resource? +- How does a caller select a managed child in the route while keeping that selector a + narrow delegated capability rather than general impersonation? +- Which profile data, including contact email, must be supplied for null-email headless + customers when a provider requires it? +- Which status changes should produce API webhooks so headless callers do not have to + poll indefinitely? + +The next revision should answer these questions before fixing exact endpoint paths or +request schemas. From 6e63cdbad59e5852e9af4392d449dbbd598d20a1 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Wed, 5 Aug 2026 17:34:10 -0300 Subject: [PATCH 3/9] docs(repo): define staged verification flow --- docs/proposal-unified-kyc-kyb-api.md | 51 +++++++++++++++++++++------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/proposal-unified-kyc-kyb-api.md b/docs/proposal-unified-kyc-kyb-api.md index c8689b466..9ee5cb60e 100644 --- a/docs/proposal-unified-kyc-kyb-api.md +++ b/docs/proposal-unified-kyc-kyb-api.md @@ -65,16 +65,44 @@ The current API is less unified than the storage model: - route names, request shapes, document handling, retries, and status responses expose provider workflow details; - the dashboard orchestrates separate provider XState machines and polls the aggregated - `GET /v1/onboarding/status` read model; -- no current request context distinguishes a manager actor from a managed child subject. + `GET /v1/onboarding/status` read model. + +Authentication, delegated authorization, and manager-to-child ownership are defined by +the [managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md) +and are not repeated here. This proposal defines the verification workflow applied after +the operation profile has been resolved. + +## Tentative generic flow + +The ideal API exposes the workflow as discoverable stages instead of requiring an +integrator to know a provider's sequence in advance: + +1. **Discover requirements.** The caller requests the requirements for a KYC or KYB by + corridor and customer type. Vortex derives the provider and returns an overview of the + required data fields, document types, and any liveness or selfie requirement. The + requirements are provider- and country-specific even though their envelope is common. +2. **Create the attempt and submit initial data.** The caller creates a verification case + with the structured data already available, such as personal or company name, address, + tax information, representatives, or beneficial owners. The exact fields follow the + requirements returned for that corridor. +3. **Upload documents when required.** The caller creates and uploads each required + document using the mechanism supported by Vortex for that provider. Vortex should + return a stable identifier for each uploaded document when the upstream provider gives + us one. If the provider only identifies a combined upload, Vortex should return at + least one identifier for that document batch. +4. **Complete liveness or selfie evidence when required.** The case may return a liveness + continuation step or accept a selfie document upload, depending on the provider and + country. +5. **Submit and track the case.** Once all required stages are complete, Vortex submits or + finalizes the provider attempt and exposes its canonical status until it is approved, + rejected, or requires another supported action. + +Not every provider needs every stage. The requirements response determines which stages +apply and gives API clients enough information to build their own collection experience +without embedding Vortex's dashboard workflow. + +## API principles -## Required invariants - -- The operation subject is always a profile and its customer entity. Provider accounts - and cases belong to that subject, never to a manager. -- A self-service request may use the profile's accepted authentication methods. A - delegated request must preserve both `actorProfileId` and `subjectProfileId` and pass the - direct manager-child and corridor checks defined by the headless-profiles proposal. - The server derives the provider from corridor and customer type. A caller cannot select an arbitrary provider account or provider case belonging to another subject. - Public responses use Vortex case identifiers and canonical status. Provider identifiers @@ -111,10 +139,7 @@ behind the unified API rather than a parallel compliance model. - Which operation vocabulary fits both providers without hiding meaningful differences? - Should Vortex proxy document bytes, issue provider pre-signed upload URLs, or support both patterns behind one document resource? -- How does a caller select a managed child in the route while keeping that selector a - narrow delegated capability rather than general impersonation? -- Which profile data, including contact email, must be supplied for null-email headless - customers when a provider requires it? +- Which contact data must be supplied as case data when a provider requires it? - Which status changes should produce API webhooks so headless callers do not have to poll indefinitely? From 8aa735cd60af03acfc10ce895b870d07c3655fdb Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 6 Aug 2026 10:45:29 +0200 Subject: [PATCH 4/9] docs(repo): define lean unified verification first iteration --- docs/proposal-unified-kyc-kyb-api.md | 333 +++++++++++++++++---------- 1 file changed, 206 insertions(+), 127 deletions(-) diff --git a/docs/proposal-unified-kyc-kyb-api.md b/docs/proposal-unified-kyc-kyb-api.md index 9ee5cb60e..c38c81c62 100644 --- a/docs/proposal-unified-kyc-kyb-api.md +++ b/docs/proposal-unified-kyc-kyb-api.md @@ -1,147 +1,226 @@ # Proposal: Unified KYC and KYB API -Status: proposed, early discussion draft. This document currently seeks agreement on -scope, invariants, and delivery order. Exact routes, schemas, and provider-specific field -contracts remain open. Last updated: 2026-08-05. +Status: proposed. This document defines the required first delivery and separates it from +later cross-provider generalization. Exact route names and Avenia field names remain +implementation decisions. Last updated: 2026-08-06. Related material: -- [`Proposal: Managed Headless Profiles`](proposal-headless-profiles-and-pricing-plans.md) -- [`Identity, Customer, and Partner Model`](architecture-identity-model.md) -- [`Avenia KYB Level 1 - API`](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) -- [`Avenia KYB Level 1 - Web SDK`](https://integration-guide.avenia.io/docs/KYB/kybLevel1) +- [Proposal: Managed Headless Profiles](proposal-headless-profiles-and-pricing-plans.md) +- [Identity, Customer, and Partner Model](architecture-identity-model.md) +- [Avenia KYB Level 1 - API](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) +- [Avenia KYB Level 1 - Web SDK](https://integration-guide.avenia.io/docs/KYB/kybLevel1) ## Objective Allow a customer, or an authorized manager acting for a managed headless customer, to -complete corridor-supported KYC or KYB through the Vortex API without requiring the -Vortex dashboard, widget, or a provider-hosted onboarding UI. - -In parallel, replace the current collection of provider-named onboarding endpoints with -the smallest practical common API. The API should select the provider from the corridor -and customer type, expose Vortex-owned verification resources and canonical statuses, -and retain provider-specific input only where the underlying checks genuinely differ. - -The first delivery item is Avenia's new API-based Level 1 KYB flow. Today, Vortex starts -Avenia company KYB through the Web SDK endpoint and sends the customer to separate hosted -company and representative URLs. The new provider flow lets Vortex submit company data, -UBOs, and documents server-to-server and track the resulting attempt. - -## Initial scope - -- Focus on Avenia and Alfredpay. -- Start with Avenia Level 1 KYB for the BRL corridor. -- Support both self-service profiles and manager-to-child delegated operations as the - managed-headless-profile authorization work becomes available. -- Keep the resource model and provider boundary suitable for another future integrator. -- Do not redesign Monerium or other provider flows in this proposal. -- Do not let callers approve a case, override a provider decision, or write canonical - compliance status directly. - -"API-driven" means that an integrator can collect data in its own experience and perform -the workflow through Vortex API operations. Pre-signed document uploads and unavoidable -identity/liveness steps may still involve a provider-controlled URL, but the flow must not -depend on a Vortex UI. +complete supported KYC or KYB through the Vortex API without depending on the Vortex +dashboard or widget. -## Existing foundation +The public API exposes Vortex-owned verification and document resources. Vortex derives +the provider from the operation subject, corridor, and customer type, while keeping +provider identifiers and provider-specific orchestration internal. + +The first delivery is Avenia Level 1 KYB for the BRL corridor. It replaces the current +Avenia Web SDK handoff with server-to-server submission of company data, beneficial +owners, and documents. It is the first adapter behind the common API, not a second +compliance model. + +## First-iteration scope + +The first iteration includes: -The persistence model is already mostly provider-neutral: +- Avenia Level 1 KYB for BRL business customers; +- self-service profiles authenticated through the existing supported credential or + session path; +- manager credentials acting on an authorized managed child when the managed-profile + authorization work is available; +- a fixed and documented Avenia BRL KYB request schema; +- Vortex-owned verification-case and document identifiers; +- create, upload, submit, read, and retry behavior; +- polling the case resource for status. + +The first iteration does not include Alfredpay migration, a dynamic form-schema engine, +public verification webhooks, child API credential issuance, or a general workflow +engine. + +"API-driven" means that an integrator may collect data in its own experience and perform +the supported workflow through Vortex API operations. An unavoidable identity, liveness, +or provider-consent step may still return an opaque provider-controlled continuation URL, +but the flow does not require a Vortex UI. + +## Existing foundation -```text -profile - -> customer entity - -> provider customer - -> KYC/KYB case -``` +The existing persistence model remains authoritative: -`provider_customers` owns the durable corridor/provider account, while `kyc_cases` owns a -verification attempt and its canonical `started`, `pending`, `in_review`, `approved`, or -`rejected` status. This model should be reused rather than introducing a second onboarding -or compliance identity. + profile + -> customer entity + -> provider customer + -> KYC/KYB cases -The current API is less unified than the storage model: +provider_customers owns the durable provider account. Each kyc_cases row represents one +verification attempt and retains the canonical started, pending, in_review, approved, or +rejected status. Provider status may continue to be stored internally as status_external. -- most Avenia and all Alfredpay KYC/KYB routes require a Supabase browser session; -- route names, request shapes, document handling, retries, and status responses expose - provider workflow details; -- the dashboard orchestrates separate provider XState machines and polls the aggregated - `GET /v1/onboarding/status` read model. +The first iteration does not add separate verification-case, attempt, action, or workflow +definition tables. A retry creates a new kyc_cases row instead of overwriting the previous +attempt. Historical decisions therefore remain available for compliance, support, and +audit. Authentication, delegated authorization, and manager-to-child ownership are defined by -the [managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md) -and are not repeated here. This proposal defines the verification workflow applied after -the operation profile has been resolved. - -## Tentative generic flow - -The ideal API exposes the workflow as discoverable stages instead of requiring an -integrator to know a provider's sequence in advance: - -1. **Discover requirements.** The caller requests the requirements for a KYC or KYB by - corridor and customer type. Vortex derives the provider and returns an overview of the - required data fields, document types, and any liveness or selfie requirement. The - requirements are provider- and country-specific even though their envelope is common. -2. **Create the attempt and submit initial data.** The caller creates a verification case - with the structured data already available, such as personal or company name, address, - tax information, representatives, or beneficial owners. The exact fields follow the - requirements returned for that corridor. -3. **Upload documents when required.** The caller creates and uploads each required - document using the mechanism supported by Vortex for that provider. Vortex should - return a stable identifier for each uploaded document when the upstream provider gives - us one. If the provider only identifies a combined upload, Vortex should return at - least one identifier for that document batch. -4. **Complete liveness or selfie evidence when required.** The case may return a liveness - continuation step or accept a selfie document upload, depending on the provider and - country. -5. **Submit and track the case.** Once all required stages are complete, Vortex submits or - finalizes the provider attempt and exposes its canonical status until it is approved, - rejected, or requires another supported action. - -Not every provider needs every stage. The requirements response determines which stages -apply and gives API clients enough information to build their own collection experience -without embedding Vortex's dashboard workflow. - -## API principles - -- The server derives the provider from corridor and customer type. A caller cannot select - an arbitrary provider account or provider case belonging to another subject. -- Public responses use Vortex case identifiers and canonical status. Provider identifiers - stay internal unless a specific continuation step requires an opaque reference. -- Provider-specific data is represented explicitly rather than forced into a misleading - lowest-common-denominator schema. -- Document operations are scoped to the subject, provider customer, case, and expected - document type before Vortex issues an upload target or forwards content. -- Provider-confirmed state remains authoritative. Client completion events cannot mark a - case approved. +the [managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md). +This proposal begins after the operation subject has been resolved. + +## First-iteration resource contract + +### Verification case + +Every public verification attempt uses a Vortex case identifier. Provider customer and +provider case identifiers remain internal. + +The case stores or resolves at least: + + id + customer_entity_id + provider_customer_id + customer_type + corridor + requirements_version + status + status_external + created_at + updated_at + +For the initial flow, requirements_version is a fixed value such as +avenia-brl-kyb-l1-v1. It is pinned when the case is created so a provider requirement +change cannot silently alter an in-flight attempt. + +The public response keeps the existing canonical status and adds simple client guidance: + + { + "id": "kyc_...", + "status": "started", + "requirementsVersion": "avenia-brl-kyb-l1-v1", + "nextAction": "upload_documents", + "errorCode": null + } + +nextAction and errorCode use a small documented set of stable Vortex values. They do not +expose provider error strings as the public contract. Allowed status transitions and the +meaning of each action or error code must be documented and tested. + +### Document + +Vortex creates its own document identifier before issuing an upload target or forwarding +content. The identifier does not depend on whether Avenia exposes a document or batch ID. + +The minimum document resource records: + + id + kyc_case_id + subject_reference + document_type + status + provider_reference + created_at + updated_at + +subject_reference identifies the company, beneficial owner, or representative within the +case. Provider references remain internal. + +Before accepting or forwarding a document, Vortex verifies the authenticated subject, +case ownership, expected subject reference, and expected document type. Upload targets +must be short-lived and constrained by file size and content type. Document bytes and +sensitive identity data must not be written to ordinary logs. If Vortex proxies or stores +arbitrary file bytes, it also validates the real file type and scans the upload before +provider submission. + +The adapter may use a provider pre-signed upload URL or a Vortex-controlled upload path. +That implementation choice does not change the public Vortex document resource. + +## First-iteration flow + +1. **Create an attempt.** The caller supplies the BRL corridor, business customer type, + and versioned Avenia Level 1 KYB data. Vortex derives the customer entity, provider, + and provider customer from the operation subject. +2. **Create and upload documents.** The caller creates Vortex document resources for the + company and relevant beneficial owners or representatives, then uploads through the + returned targets. +3. **Submit the attempt.** Vortex checks that the required data and documents exist, sends + the provider operations in the required order, and moves the case to the appropriate + canonical status. +4. **Track the attempt.** The caller reads the Vortex case until it is approved, rejected, + or exposes another supported nextAction. +5. **Retry when allowed.** Corrected data or documents create a new attempt. The previous + case remains unchanged and the API returns the new Vortex case identifier. + +The create-attempt, document-forwarding, and submit operations must tolerate client +retries without creating duplicate provider-side effects. The implementation may use an +Idempotency-Key or an equivalent resource-state guard; a general API-wide idempotency +framework is not required for this delivery. + +## Authorization and state invariants + +- The server derives the provider from the operation subject, corridor, and customer + type. A caller cannot select an arbitrary provider customer or provider case. +- A delegated operation uses the child as the resource owner and retains the manager as + the audited actor. +- Document operations are scoped to the operation subject, customer entity, provider + customer, case, subject reference, and expected document type. +- Provider-confirmed state is authoritative. A caller cannot approve a case, override a + provider decision, or write canonical compliance state directly. +- The actor, subject, credential, and case identifier are available to audit records. +- Provider-required consent or attestation is collected in the provider-specific payload + and recorded with the submitting actor and timestamp. A general consent subsystem is + not required for the first iteration. +- Retained identity data follows the existing security and legal-retention rules. The API + returns only the fields needed by the integrator and does not echo unmasked identity + numbers or document content by default. ## Delivery order -1. Implement Avenia Level 1 KYB through its API flow: create or reuse the company - subaccount, create and upload company and UBO documents, register UBOs, submit the KYB - attempt, and synchronize its result into the existing provider customer and KYB case. -2. Use that vertical slice to define the common Vortex case lifecycle and operations for - starting, continuing, submitting, reading, and retrying verification. -3. Make those operations available to self-service API credentials and to manager - credentials acting on an authorized managed child, without changing resource ownership. -4. Adapt Alfredpay's API-based KYC/KYB flows to the same lifecycle while retaining its - corridor-specific forms, document sets, and hosted-flow exceptions. -5. Migrate first-party UI consumers, then retire provider-named public onboarding routes - only after compatibility requirements are known. - -The first item must not wait for the complete cross-provider API design. It should reuse -the current canonical tables and status rules so the Avenia work becomes the first adapter -behind the unified API rather than a parallel compliance model. - -## First open decisions - -- What is the smallest common resource shape: one provider customer with a current case, - or an explicit append-only list of attempts? -- Which operation vocabulary fits both providers without hiding meaningful differences? -- Should Vortex proxy document bytes, issue provider pre-signed upload URLs, or support - both patterns behind one document resource? -- Which contact data must be supplied as case data when a provider requires it? -- Which status changes should produce API webhooks so headless callers do not have to - poll indefinitely? - -The next revision should answer these questions before fixing exact endpoint paths or -request schemas. +1. Implement the Avenia Level 1 KYB adapter using the existing provider customer and + append-only KYC case records. +2. Define the fixed avenia-brl-kyb-l1-v1 input, document requirements, status transitions, + next actions, error codes, and retry rules. +3. Expose the minimal create, document, submit, and read operations for self-service + subjects. +4. Allow the same operations through the managed-profile actor/subject authorization path + without changing child ownership. +5. Add focused ownership, cross-manager, idempotency, retry-history, document-scope, and + provider-state tests. +6. Migrate the first-party Avenia Level 1 UI consumer after the API contract is stable. + +## Follow-up capabilities + +The following are intentionally deferred until a concrete second flow requires them: + +- adapt Alfredpay and additional providers to the common lifecycle; +- discover dynamic provider and country requirements through a versioned schema; +- split a stable verification aggregate from append-only attempt records; +- separate workflow state, provider decision, remediation actions, and capability status; +- publish verification webhooks instead of relying on polling; +- introduce an API-wide idempotency and standardized error framework; +- add a generic company, owner, controller, and representative relationship model; +- automate consent, retention, export, and redaction workflows; +- migrate remaining first-party UI flows and retire provider-named public routes. + +The Avenia vertical slice should keep provider orchestration behind a narrow adapter so +these capabilities can be added without changing subject ownership or exposing provider +identifiers. + +## Acceptance criteria + +- A BRL business customer can complete Avenia Level 1 KYB through API operations without + a Vortex dashboard or widget. +- An authorized manager can perform the same flow for its managed child, with the manager + recorded as actor and the child retained as owner. +- Provider identity is derived server-side and provider identifiers stay internal. +- Every attempt and document has a stable Vortex identifier. +- Requirements are pinned to a documented version for the lifetime of an attempt. +- Retrying a failed or rejected flow creates a new case without overwriting history. +- Retried create, document, and submit requests do not duplicate provider-side effects. +- Public status, next-action, and error values are stable and provider-neutral. +- Provider state remains authoritative. +- Document uploads are subject-scoped, type-constrained, short-lived, and excluded from + ordinary logs. From f1def3a551e856e456aaf4a6ca7092904aaaa9c1 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 6 Aug 2026 11:46:05 +0200 Subject: [PATCH 5/9] docs(repo): restore unified verification proposal scope --- docs/proposal-unified-kyc-kyb-api.md | 346 +++++++++++---------------- 1 file changed, 140 insertions(+), 206 deletions(-) diff --git a/docs/proposal-unified-kyc-kyb-api.md b/docs/proposal-unified-kyc-kyb-api.md index c38c81c62..c92b6fd9a 100644 --- a/docs/proposal-unified-kyc-kyb-api.md +++ b/docs/proposal-unified-kyc-kyb-api.md @@ -1,226 +1,160 @@ # Proposal: Unified KYC and KYB API -Status: proposed. This document defines the required first delivery and separates it from -later cross-provider generalization. Exact route names and Avenia field names remain -implementation decisions. Last updated: 2026-08-06. +Status: proposed, early discussion draft. This document currently seeks agreement on +scope, invariants, and delivery order. Exact routes, schemas, and provider-specific field +contracts remain open. Last updated: 2026-08-06. Related material: -- [Proposal: Managed Headless Profiles](proposal-headless-profiles-and-pricing-plans.md) -- [Identity, Customer, and Partner Model](architecture-identity-model.md) -- [Avenia KYB Level 1 - API](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) -- [Avenia KYB Level 1 - Web SDK](https://integration-guide.avenia.io/docs/KYB/kybLevel1) +- [`Proposal: Managed Headless Profiles`](proposal-headless-profiles-and-pricing-plans.md) +- [`Identity, Customer, and Partner Model`](architecture-identity-model.md) +- [`Avenia KYB Level 1 - API`](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) +- [`Avenia KYB Level 1 - Web SDK`](https://integration-guide.avenia.io/docs/KYB/kybLevel1) ## Objective Allow a customer, or an authorized manager acting for a managed headless customer, to -complete supported KYC or KYB through the Vortex API without depending on the Vortex -dashboard or widget. - -The public API exposes Vortex-owned verification and document resources. Vortex derives -the provider from the operation subject, corridor, and customer type, while keeping -provider identifiers and provider-specific orchestration internal. - -The first delivery is Avenia Level 1 KYB for the BRL corridor. It replaces the current -Avenia Web SDK handoff with server-to-server submission of company data, beneficial -owners, and documents. It is the first adapter behind the common API, not a second -compliance model. - -## First-iteration scope - -The first iteration includes: - -- Avenia Level 1 KYB for BRL business customers; -- self-service profiles authenticated through the existing supported credential or - session path; -- manager credentials acting on an authorized managed child when the managed-profile - authorization work is available; -- a fixed and documented Avenia BRL KYB request schema; -- Vortex-owned verification-case and document identifiers; -- create, upload, submit, read, and retry behavior; -- polling the case resource for status. - -The first iteration does not include Alfredpay migration, a dynamic form-schema engine, -public verification webhooks, child API credential issuance, or a general workflow -engine. - -"API-driven" means that an integrator may collect data in its own experience and perform -the supported workflow through Vortex API operations. An unavoidable identity, liveness, -or provider-consent step may still return an opaque provider-controlled continuation URL, -but the flow does not require a Vortex UI. +complete corridor-supported KYC or KYB through the Vortex API without requiring the +Vortex dashboard, widget, or a provider-hosted onboarding UI. + +In parallel, replace the current collection of provider-named onboarding endpoints with +the smallest practical common API. The API should select the provider from the corridor +and customer type, expose Vortex-owned verification resources and canonical statuses, +and retain provider-specific input only where the underlying checks genuinely differ. + +The first delivery item is Avenia's new API-based Level 1 KYB flow. Today, Vortex starts +Avenia company KYB through the Web SDK endpoint and sends the customer to separate hosted +company and representative URLs. The new provider flow lets Vortex submit company data, +UBOs, and documents server-to-server and track the resulting attempt. + +Avenia is the first vertical slice, not the scope of the unified API. The common envelope +and lifecycle must continue to accommodate Alfredpay and future integrators without making +their callers depend on Avenia-specific routing or identifiers. + +## Initial scope + +- Focus on Avenia and Alfredpay. +- Start with Avenia Level 1 KYB for the BRL corridor. +- Support both self-service profiles and manager-to-child delegated operations as the + managed-headless-profile authorization work becomes available. +- Keep the resource model and provider boundary suitable for another future integrator. +- Do not redesign Monerium or other provider flows in this proposal. +- Do not let callers approve a case, override a provider decision, or write canonical + compliance status directly. + +"API-driven" means that an integrator can collect data in its own experience and perform +the workflow through Vortex API operations. Pre-signed document uploads and unavoidable +identity/liveness steps may still involve a provider-controlled URL, but the flow must not +depend on a Vortex UI. ## Existing foundation -The existing persistence model remains authoritative: +The persistence model is already mostly provider-neutral: + +```text +profile + -> customer entity + -> provider customer + -> KYC/KYB case +``` - profile - -> customer entity - -> provider customer - -> KYC/KYB cases +`provider_customers` owns the durable corridor/provider account, while `kyc_cases` owns a +verification attempt and its canonical `started`, `pending`, `in_review`, `approved`, or +`rejected` status. This model should be reused rather than introducing a second onboarding +or compliance identity. -provider_customers owns the durable provider account. Each kyc_cases row represents one -verification attempt and retains the canonical started, pending, in_review, approved, or -rejected status. Provider status may continue to be stored internally as status_external. +The current API is less unified than the storage model: -The first iteration does not add separate verification-case, attempt, action, or workflow -definition tables. A retry creates a new kyc_cases row instead of overwriting the previous -attempt. Historical decisions therefore remain available for compliance, support, and -audit. +- most Avenia and all Alfredpay KYC/KYB routes require a Supabase browser session; +- route names, request shapes, document handling, retries, and status responses expose + provider workflow details; +- the dashboard orchestrates separate provider XState machines and polls the aggregated + `GET /v1/onboarding/status` read model. Authentication, delegated authorization, and manager-to-child ownership are defined by -the [managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md). -This proposal begins after the operation subject has been resolved. - -## First-iteration resource contract - -### Verification case - -Every public verification attempt uses a Vortex case identifier. Provider customer and -provider case identifiers remain internal. - -The case stores or resolves at least: - - id - customer_entity_id - provider_customer_id - customer_type - corridor - requirements_version - status - status_external - created_at - updated_at - -For the initial flow, requirements_version is a fixed value such as -avenia-brl-kyb-l1-v1. It is pinned when the case is created so a provider requirement -change cannot silently alter an in-flight attempt. - -The public response keeps the existing canonical status and adds simple client guidance: - - { - "id": "kyc_...", - "status": "started", - "requirementsVersion": "avenia-brl-kyb-l1-v1", - "nextAction": "upload_documents", - "errorCode": null - } - -nextAction and errorCode use a small documented set of stable Vortex values. They do not -expose provider error strings as the public contract. Allowed status transitions and the -meaning of each action or error code must be documented and tested. - -### Document - -Vortex creates its own document identifier before issuing an upload target or forwarding -content. The identifier does not depend on whether Avenia exposes a document or batch ID. - -The minimum document resource records: - - id - kyc_case_id - subject_reference - document_type - status - provider_reference - created_at - updated_at - -subject_reference identifies the company, beneficial owner, or representative within the -case. Provider references remain internal. - -Before accepting or forwarding a document, Vortex verifies the authenticated subject, -case ownership, expected subject reference, and expected document type. Upload targets -must be short-lived and constrained by file size and content type. Document bytes and -sensitive identity data must not be written to ordinary logs. If Vortex proxies or stores -arbitrary file bytes, it also validates the real file type and scans the upload before -provider submission. - -The adapter may use a provider pre-signed upload URL or a Vortex-controlled upload path. -That implementation choice does not change the public Vortex document resource. - -## First-iteration flow - -1. **Create an attempt.** The caller supplies the BRL corridor, business customer type, - and versioned Avenia Level 1 KYB data. Vortex derives the customer entity, provider, - and provider customer from the operation subject. -2. **Create and upload documents.** The caller creates Vortex document resources for the - company and relevant beneficial owners or representatives, then uploads through the - returned targets. -3. **Submit the attempt.** Vortex checks that the required data and documents exist, sends - the provider operations in the required order, and moves the case to the appropriate - canonical status. -4. **Track the attempt.** The caller reads the Vortex case until it is approved, rejected, - or exposes another supported nextAction. -5. **Retry when allowed.** Corrected data or documents create a new attempt. The previous - case remains unchanged and the API returns the new Vortex case identifier. - -The create-attempt, document-forwarding, and submit operations must tolerate client -retries without creating duplicate provider-side effects. The implementation may use an -Idempotency-Key or an equivalent resource-state guard; a general API-wide idempotency -framework is not required for this delivery. - -## Authorization and state invariants - -- The server derives the provider from the operation subject, corridor, and customer - type. A caller cannot select an arbitrary provider customer or provider case. -- A delegated operation uses the child as the resource owner and retains the manager as - the audited actor. -- Document operations are scoped to the operation subject, customer entity, provider - customer, case, subject reference, and expected document type. -- Provider-confirmed state is authoritative. A caller cannot approve a case, override a - provider decision, or write canonical compliance state directly. -- The actor, subject, credential, and case identifier are available to audit records. -- Provider-required consent or attestation is collected in the provider-specific payload - and recorded with the submitting actor and timestamp. A general consent subsystem is - not required for the first iteration. -- Retained identity data follows the existing security and legal-retention rules. The API - returns only the fields needed by the integrator and does not echo unmasked identity - numbers or document content by default. +the [managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md) +and are not repeated here. This proposal defines the verification workflow applied after +the operation profile has been resolved. + +## Tentative generic flow + +The ideal API exposes the workflow as discoverable stages instead of requiring an +integrator to know a provider's sequence in advance: + +1. **Discover requirements.** The caller requests the requirements for a KYC or KYB by + corridor and customer type. Vortex derives the provider and returns an overview of the + required data fields, document types, and any liveness or selfie requirement. The + requirements are provider- and country-specific even though their envelope is common. + The response includes a stable requirements version. +2. **Create the attempt and submit initial data.** The caller creates a verification case + with the structured data already available, such as personal or company name, address, + tax information, representatives, or beneficial owners. The exact fields follow the + requirements returned for that corridor, and the case pins that requirements version. +3. **Upload documents when required.** The caller creates and uploads each required + document using the mechanism supported by Vortex for that provider. Vortex creates and + returns its own stable identifier for each document or document batch before upload; + upstream identifiers are stored only as internal mappings. +4. **Complete liveness or selfie evidence when required.** The case may return a liveness + continuation step or accept a selfie document upload, depending on the provider and + country. +5. **Submit and track the case.** Once all required stages are complete, Vortex submits or + finalizes the provider attempt and exposes its canonical status until it is approved, + rejected, or requires another supported action. + +Not every provider needs every stage. The requirements response determines which stages +apply and gives API clients enough information to build their own collection experience +without embedding Vortex's dashboard workflow. + +## API principles + +- The server derives the provider from corridor and customer type. A caller cannot select + an arbitrary provider account or provider case belonging to another subject. +- Public responses use Vortex case identifiers and canonical status. Provider identifiers + stay internal unless a specific continuation step requires an opaque reference. +- Provider-specific data is represented explicitly rather than forced into a misleading + lowest-common-denominator schema. +- Document operations are scoped to the subject, provider customer, case, and expected + document type before Vortex issues an upload target or forwards content. +- Provider-confirmed state remains authoritative. Client completion events cannot mark a + case approved. +- Delegated operations retain both the manager actor and child subject for authorization + and audit while keeping the child as the resource owner. +- Case creation, document submission, and final submission define retry-safe behavior so + a client timeout cannot silently create duplicate provider-side effects. ## Delivery order -1. Implement the Avenia Level 1 KYB adapter using the existing provider customer and - append-only KYC case records. -2. Define the fixed avenia-brl-kyb-l1-v1 input, document requirements, status transitions, - next actions, error codes, and retry rules. -3. Expose the minimal create, document, submit, and read operations for self-service - subjects. -4. Allow the same operations through the managed-profile actor/subject authorization path - without changing child ownership. -5. Add focused ownership, cross-manager, idempotency, retry-history, document-scope, and - provider-state tests. -6. Migrate the first-party Avenia Level 1 UI consumer after the API contract is stable. - -## Follow-up capabilities - -The following are intentionally deferred until a concrete second flow requires them: - -- adapt Alfredpay and additional providers to the common lifecycle; -- discover dynamic provider and country requirements through a versioned schema; -- split a stable verification aggregate from append-only attempt records; -- separate workflow state, provider decision, remediation actions, and capability status; -- publish verification webhooks instead of relying on polling; -- introduce an API-wide idempotency and standardized error framework; -- add a generic company, owner, controller, and representative relationship model; -- automate consent, retention, export, and redaction workflows; -- migrate remaining first-party UI flows and retire provider-named public routes. - -The Avenia vertical slice should keep provider orchestration behind a narrow adapter so -these capabilities can be added without changing subject ownership or exposing provider -identifiers. - -## Acceptance criteria - -- A BRL business customer can complete Avenia Level 1 KYB through API operations without - a Vortex dashboard or widget. -- An authorized manager can perform the same flow for its managed child, with the manager - recorded as actor and the child retained as owner. -- Provider identity is derived server-side and provider identifiers stay internal. -- Every attempt and document has a stable Vortex identifier. -- Requirements are pinned to a documented version for the lifetime of an attempt. -- Retrying a failed or rejected flow creates a new case without overwriting history. -- Retried create, document, and submit requests do not duplicate provider-side effects. -- Public status, next-action, and error values are stable and provider-neutral. -- Provider state remains authoritative. -- Document uploads are subject-scoped, type-constrained, short-lived, and excluded from - ordinary logs. +1. Implement Avenia Level 1 KYB through its API flow: create or reuse the company + subaccount, create and upload company and UBO documents, register UBOs, submit the KYB + attempt, and synchronize its result into the existing provider customer and KYB case. +2. Use that vertical slice to define the common Vortex case lifecycle and operations for + starting, continuing, submitting, reading, and retrying verification. +3. Make those operations available to self-service API credentials and to manager + credentials acting on an authorized managed child, without changing resource ownership. +4. Adapt Alfredpay's API-based KYC/KYB flows to the same lifecycle while retaining its + corridor-specific forms, document sets, and hosted-flow exceptions. +5. Migrate first-party UI consumers, then retire provider-named public onboarding routes + only after compatibility requirements are known. + +The first item must not wait for the complete cross-provider API design. It should reuse +the current canonical tables and status rules so the Avenia work becomes the first adapter +behind the unified API rather than a parallel compliance model. + +The first vertical slice does not need to settle every cross-provider resource or webhook +decision. It must preserve the common envelope, pin the requirements version, use Vortex +resource identifiers, keep provider identifiers internal, and make external side effects +safe to retry. + +## First open decisions + +- What is the smallest common resource shape: one provider customer with a current case, + or an explicit append-only list of attempts? +- Which operation vocabulary fits both providers without hiding meaningful differences? +- Should Vortex proxy document bytes, issue provider pre-signed upload URLs, or support + both patterns behind one document resource? +- Which contact data must be supplied as case data when a provider requires it? +- Which status changes should produce API webhooks so headless callers do not have to + poll indefinitely? + +The next revision should answer these questions before fixing exact endpoint paths or +request schemas. From 1507d0926441f6eeb887779495e76b71548517e6 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 6 Aug 2026 10:47:03 -0300 Subject: [PATCH 6/9] feat(shared): map Avenia KYB Level 1 API --- .../src/services/brla/brlaApiService.test.ts | 107 ++++++++++++ .../src/services/brla/brlaApiService.ts | 31 +++- packages/shared/src/services/brla/mappings.ts | 40 ++++- .../shared/src/services/brla/schemas.test.ts | 42 ++++- packages/shared/src/services/brla/schemas.ts | 43 +++++ packages/shared/src/services/brla/types.ts | 157 ++++++++++++++++-- 6 files changed, 395 insertions(+), 25 deletions(-) create mode 100644 packages/shared/src/services/brla/brlaApiService.test.ts diff --git a/packages/shared/src/services/brla/brlaApiService.test.ts b/packages/shared/src/services/brla/brlaApiService.test.ts new file mode 100644 index 000000000..521cf69ce --- /dev/null +++ b/packages/shared/src/services/brla/brlaApiService.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, mock, test } from "bun:test"; +import * as forge from "node-forge"; +import { BrlaApiService } from "./brlaApiService"; +import { Endpoint } from "./mappings"; +import { AveniaDocumentType, type AveniaKybLevel1Payload, type AveniaUboPayload } from "./types"; + +function serviceWithMockedRequest() { + const service = Object.create(BrlaApiService.prototype) as BrlaApiService; + const sendRequest = mock(async () => ({ id: "provider-id" })); + Object.assign(service, { sendRequest }); + return { sendRequest, service }; +} + +const ubo: AveniaUboPayload = { + city: "Sao Paulo", + country: "BRA", + countryOfTaxId: "BRA", + dateOfBirth: "1988-07-22", + documentCountry: "BRA", + fullName: "UBO NAME", + hasControl: "CEO", + percentageOfOwnership: "100", + state: "SP", + streetLine1: "Rua Aurora 456", + taxIdNumber: "11182159111", + uploadedIdentificationId: "document-1", + zipCode: "01209-001" +}; + +const kyb: AveniaKybLevel1Payload = { + businessActivityDescription: "Software development", + certificateOfIncorporationDocumentId: "document-2", + companyCity: "Sao Paulo", + companyCountry: "BRA", + companyLegalName: "ACME LTDA", + companyRegistrationNumber: "42731085000167", + companyState: "SP", + companyStreetLine1: "Av Paulista 1000", + companyZipCode: "01310-100", + countryTaxResidence: "BRA", + estimatedAnnualRevenueUsd: "less_than_100k", + estimatedMonthlyVolumeUsd: "2000", + numberOfEmployees: "1-10", + reasonForAccountOpening: "receive_payments_for_goods_and_services", + sourceOfFundsAndIncome: "sales_of_goods_and_services", + taxIdentificationDocumentId: "document-3", + taxIdentificationNumberTin: "42.731.085/0001-67", + uboIds: ["ubo-1"] +}; + +describe("BrlaApiService Avenia KYB Level 1 mappings", () => { + test("substitutes and encodes provider path parameters before signing the request", async () => { + const service = Object.create(BrlaApiService.prototype) as BrlaApiService; + const keyPair = forge.pki.rsa.generateKeyPair(1024); + Object.assign(service, { apiKey: "test-key", privateKey: forge.pki.privateKeyToPem(keyPair.privateKey) }); + const originalFetch = globalThis.fetch; + const fetchMock = mock(async () => new Response(JSON.stringify({ attempt: {} }), { status: 200 })); + globalThis.fetch = fetchMock as typeof fetch; + + try { + await service.sendRequest(Endpoint.GetKybAttempt, "GET", "subAccountId=sub-1", undefined, "attempt/1"); + expect(String(fetchMock.mock.calls[0][0])).toEndWith( + "/v2/kyc/attempts/attempt%2F1?subAccountId=sub-1" + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + + test("maps document readiness and UBO creation to subaccount-scoped endpoints", async () => { + const { sendRequest, service } = serviceWithMockedRequest(); + + await service.getUploadedDocument("document/1", "sub account"); + await service.createUbo(ubo, "sub account"); + + expect(sendRequest.mock.calls[0]).toEqual([ + Endpoint.GetDocument, + "GET", + "subAccountId=sub%20account", + undefined, + "document/1" + ]); + expect(sendRequest.mock.calls[1]).toEqual([Endpoint.Ubos, "POST", "subAccountId=sub%20account", ubo]); + }); + + test("maps API KYB submission and subaccount-scoped attempt polling", async () => { + const { sendRequest, service } = serviceWithMockedRequest(); + + await service.submitKybLevel1(kyb, "sub-1"); + await service.getKybAttemptStatus("attempt-1", "sub-1"); + + expect(sendRequest.mock.calls[0]).toEqual([Endpoint.Level1Api, "POST", "subAccountId=sub-1", kyb]); + expect(sendRequest.mock.calls[1]).toEqual([ + Endpoint.GetKybAttempt, + "GET", + "subAccountId=sub-1", + undefined, + "attempt-1" + ]); + }); + + test("includes the corporate and UBO identification document types", () => { + expect(AveniaDocumentType.CERTIFICATE_OF_INCORPORATION).toBe("CERTIFICATE-OF-INCORPORATION"); + expect(AveniaDocumentType.COMPANY_TAX_IDENTIFICATION_DOCUMENT).toBe("COMPANY-TAX-IDENTIFICATION-DOCUMENT"); + expect(AveniaDocumentType.RESIDENCE_PERMIT).toBe("RESIDENCE-PERMIT"); + }); +}); diff --git a/packages/shared/src/services/brla/brlaApiService.ts b/packages/shared/src/services/brla/brlaApiService.ts index 45897bbc8..dcb35ed00 100644 --- a/packages/shared/src/services/brla/brlaApiService.ts +++ b/packages/shared/src/services/brla/brlaApiService.ts @@ -9,13 +9,17 @@ import { AveniaAccountInfoResponse, AveniaAccountType, AveniaDocumentGetResponse, + AveniaDocumentResponse, AveniaDocumentType, AveniaKybAttemptStatusResponse, + AveniaKybLevel1Payload, AveniaPayinTicket, AveniaPaymentMethod, AveniaPayoutTicket, AveniaQuoteResponse, AveniaSwapTicket, + AveniaUboPayload, + AveniaUboResponse, BlockchainSendMethod, BrlaCurrency, GetKycAttemptResponse, @@ -121,7 +125,10 @@ export class BrlaApiService { let requestUri = endpoint as string; if (pathParam) { - requestUri += `/${pathParam}`; + const encodedPathParam = encodeURIComponent(pathParam); + requestUri = requestUri.includes("{") + ? requestUri.replace(/\{[^}]+\}/, encodedPathParam) + : `${requestUri}/${encodedPathParam}`; } if (queryParams) { requestUri += `?${queryParams}`; @@ -228,6 +235,16 @@ export class BrlaApiService { return await this.sendRequest(Endpoint.Documents, "GET", query, undefined); } + public async getUploadedDocument(documentId: string, subAccountId: string): Promise { + const query = `subAccountId=${encodeURIComponent(subAccountId)}`; + return await this.sendRequest(Endpoint.GetDocument, "GET", query, undefined, documentId); + } + + public async createUbo(payload: AveniaUboPayload, subAccountId: string): Promise { + const query = `subAccountId=${encodeURIComponent(subAccountId)}`; + return await this.sendRequest(Endpoint.Ubos, "POST", query, payload); + } + public async createPayInQuote( quoteParams: PayInQuoteParams, options: { useCache?: boolean } = {} @@ -371,7 +388,12 @@ export class BrlaApiService { public async submitKycLevel1(payload: KycLevel1Payload): Promise { const query = `subAccountId=${encodeURIComponent(payload.subAccountId)}`; - return await this.sendRequest(Endpoint.KycLevel1, "POST", query, payload); + return await this.sendRequest(Endpoint.Level1Api, "POST", query, payload); + } + + public async submitKybLevel1(payload: AveniaKybLevel1Payload, subAccountId: string): Promise { + const query = `subAccountId=${encodeURIComponent(subAccountId)}`; + return await this.sendRequest(Endpoint.Level1Api, "POST", query, payload); } public async getKycAttempts(subAccountId: string): Promise { @@ -396,8 +418,9 @@ export class BrlaApiService { * @param attemptId The KYB attempt ID * @returns The KYB attempt status */ - public async getKybAttemptStatus(attemptId: string): Promise { - return await this.sendRequest(Endpoint.GetKybAttempt, "GET", undefined, undefined, attemptId); + public async getKybAttemptStatus(attemptId: string, subAccountId?: string): Promise { + const query = subAccountId ? `subAccountId=${encodeURIComponent(subAccountId)}` : undefined; + return await this.sendRequest(Endpoint.GetKybAttempt, "GET", query, undefined, attemptId); } public async getAccountBalance(subAccountId: string): Promise { diff --git a/packages/shared/src/services/brla/mappings.ts b/packages/shared/src/services/brla/mappings.ts index 94529058e..ff8ca821c 100644 --- a/packages/shared/src/services/brla/mappings.ts +++ b/packages/shared/src/services/brla/mappings.ts @@ -4,12 +4,16 @@ import { AveniaAccountInfoResponse, AveniaAccountType, AveniaDocumentGetResponse, + AveniaDocumentResponse, AveniaKybAttemptStatusResponse, + AveniaKybLevel1Payload, AveniaPayinTicket, AveniaPayoutTicket, AveniaQuoteResponse, AveniaSubaccount, AveniaSwapTicket, + AveniaUboPayload, + AveniaUboResponse, DocumentUploadRequest, DocumentUploadResponse, GetKycAttemptResponse, @@ -28,12 +32,14 @@ export enum Endpoint { GetSubaccount = "/v2/account/sub-accounts", AccountLimits = "/v2/account/limits", PixInfo = "/v2/account/bank-accounts/brl/pix-info", - KycLevel1 = "/v2/kyc/new-level-1/api", + Level1Api = "/v2/kyc/new-level-1/api", KybLevel1WebSdk = "/v2/kyc/new-level-1/web-sdk", FixedRateQuote = "/v2/account/quote/fixed-rate", Tickets = "/v2/account/tickets", AccountInfo = "/v2/account/account-info", Documents = "/v2/documents", + GetDocument = "/v2/documents/{documentId}", + Ubos = "/v2/account/ubos", GetKycAttempt = "/v2/kyc/attempts", GetKybAttempt = "/v2/kyc/attempts/{attemptId}", Balances = "/v2/account/balances" @@ -82,9 +88,9 @@ export interface EndpointMapping { response: undefined; }; }; - [Endpoint.KycLevel1]: { + [Endpoint.Level1Api]: { POST: { - body: KycLevel1Payload; + body: KycLevel1Payload | AveniaKybLevel1Payload; response: KycLevel1Response; }; GET: { @@ -154,6 +160,34 @@ export interface EndpointMapping { response: undefined; }; }; + [Endpoint.GetDocument]: { + POST: { + body: undefined; + response: undefined; + }; + GET: { + body: undefined; + response: AveniaDocumentResponse; + }; + PATCH: { + body: undefined; + response: undefined; + }; + }; + [Endpoint.Ubos]: { + POST: { + body: AveniaUboPayload; + response: AveniaUboResponse; + }; + GET: { + body: undefined; + response: undefined; + }; + PATCH: { + body: undefined; + response: undefined; + }; + }; [Endpoint.GetKycAttempt]: { POST: { body: undefined; diff --git a/packages/shared/src/services/brla/schemas.test.ts b/packages/shared/src/services/brla/schemas.test.ts index bc2c61b5c..72b937cdd 100644 --- a/packages/shared/src/services/brla/schemas.test.ts +++ b/packages/shared/src/services/brla/schemas.test.ts @@ -3,11 +3,15 @@ import { aveniaAccountBalanceSchema, aveniaAccountInfoSchema, aveniaAccountLimitsSchema, + aveniaDocumentResponseSchema, + aveniaKybAttemptStatusSchema, + aveniaLevel1ResponseSchema, aveniaPayinTicketsSchema, aveniaPayoutTicketSchema, aveniaPixInputTicketSchema, aveniaPixKeyDataSchema, - aveniaQuoteResponseSchema + aveniaQuoteResponseSchema, + aveniaUboResponseSchema } from "./schemas"; function validQuoteBody() { @@ -137,3 +141,39 @@ describe("aveniaAccountInfoSchema", () => { expect(() => aveniaAccountInfoSchema.parse(body)).toThrow(); }); }); + +describe("Avenia KYB Level 1 response schemas", () => { + test("accepts document readiness and identifier responses", () => { + expect(() => + aveniaDocumentResponseSchema.parse({ + document: { + documentType: "CERTIFICATE-OF-INCORPORATION", + id: "document-1", + ready: true, + uploadStatusFront: "PROCESSED" + } + }) + ).not.toThrow(); + expect(() => aveniaDocumentResponseSchema.parse({ document: { id: "document-1", ready: true } })).toThrow(); + expect(() => aveniaUboResponseSchema.parse({ id: "ubo-1" })).not.toThrow(); + expect(() => aveniaLevel1ResponseSchema.parse({ id: "attempt-1" })).not.toThrow(); + }); + + test("accepts the documented completed KYB attempt and pending attempts without a result", () => { + const attempt = { + createdAt: "2026-03-19T22:09:52.629984Z", + id: "attempt-1", + levelName: "kyb-level-1", + result: "APPROVED", + resultMessage: "", + retryable: false, + status: "COMPLETED", + updatedAt: "2026-03-19T22:09:52.629984Z" + }; + expect(() => aveniaKybAttemptStatusSchema.parse({ attempt })).not.toThrow(); + expect(() => + aveniaKybAttemptStatusSchema.parse({ attempt: { ...attempt, result: undefined, status: "PENDING" } }) + ).not.toThrow(); + expect(() => aveniaKybAttemptStatusSchema.parse({ attempt: { ...attempt, status: "APPROVED" } })).toThrow(); + }); +}); diff --git a/packages/shared/src/services/brla/schemas.ts b/packages/shared/src/services/brla/schemas.ts index 9ae7e3c9a..cebb07c28 100644 --- a/packages/shared/src/services/brla/schemas.ts +++ b/packages/shared/src/services/brla/schemas.ts @@ -2,7 +2,10 @@ import { z } from "zod"; import { AveniaAccountBalanceResponse, AveniaAccountInfoResponse, + AveniaDocument, + AveniaDocumentType, AveniaFeeType, + AveniaKybAttemptStatusResponse, AveniaOperationFee, AveniaPayinTicket, AveniaPayoutTicket, @@ -10,6 +13,10 @@ import { AveniaSubaccountAccountInfo, AveniaSubaccountWallet, AveniaTicketStatus, + AveniaUboResponse, + KycAttemptResult, + KycAttemptStatus, + KycLevel1Response, Limit, PixInputTicketOutput, PixKeyData, @@ -136,3 +143,39 @@ export const aveniaAccountInfoSchema = z.looseObject({ }) ) }) satisfies z.ZodType; + +/** A document after Avenia has processed the bytes uploaded to its pre-signed URL. */ +export const aveniaDocumentResponseSchema = z.looseObject({ + document: z.looseObject({ + documentType: z.enum(AveniaDocumentType), + id: z.string().min(1), + ready: z.boolean(), + uploadStatusFront: z.string().min(1) + }) +}) satisfies z.ZodType<{ + document: Pick; +}>; + +/** The identifier returned by UBO creation. */ +export const aveniaUboResponseSchema = z.looseObject({ + id: z.string().min(1) +}) satisfies z.ZodType; + +/** The attempt identifier returned by API-based KYC and KYB Level 1 submissions. */ +export const aveniaLevel1ResponseSchema = z.looseObject({ + id: z.string().min(1) +}) satisfies z.ZodType; + +/** A KYB attempt returned by GET /v2/kyc/attempts/{attemptId}. */ +export const aveniaKybAttemptStatusSchema = z.looseObject({ + attempt: z.looseObject({ + createdAt: z.string().min(1), + id: z.string().min(1), + levelName: z.string().min(1), + result: z.enum(KycAttemptResult).optional(), + resultMessage: z.string(), + retryable: z.boolean(), + status: z.enum(KycAttemptStatus), + updatedAt: z.string().min(1) + }) +}) satisfies z.ZodType; diff --git a/packages/shared/src/services/brla/types.ts b/packages/shared/src/services/brla/types.ts index ff3e6cd93..0554be9f9 100644 --- a/packages/shared/src/services/brla/types.ts +++ b/packages/shared/src/services/brla/types.ts @@ -348,6 +348,122 @@ export interface KycLevel1Response { id: string; } +export type AveniaUboControlRole = + | "CEO" + | "CFO" + | "COO" + | "CTO" + | "President" + | "Vice President" + | "Director" + | "Managing Director" + | "Managing Partner" + | "General Partner" + | "Partner" + | "Secretary" + | "Treasurer" + | "Chairman" + | "Board Member" + | "Authorized Signatory" + | "General Counsel" + | "Owner" + | "Founder" + | "Manager" + | "Member" + | "Comptroller" + | "Chief Compliance Officer"; + +export interface AveniaUboPayload { + fullName: string; + dateOfBirth: string; + countryOfTaxId: string; + taxIdNumber: string; + email?: string; + phone?: string; + percentageOfOwnership: string; + hasControl?: AveniaUboControlRole; + uploadedIdentificationId: string; + uploadedSelfieId?: string; + documentCountry: string; + streetLine1: string; + streetLine2?: string; + streetLine3?: string; + city: string; + state: string; + zipCode: string; + country: string; +} + +export interface AveniaUboResponse { + id: string; +} + +export type AveniaKybReasonForAccountOpening = + | "charitable_donations" + | "ecommerce_retail_payments" + | "investment_purposes" + | "other" + | "payments_to_friends_or_family_abroad" + | "payroll" + | "personal_or_living_expenses" + | "protect_wealth" + | "purchase_goods_and_services" + | "receive_payments_for_goods_and_services" + | "tax_optimization" + | "third_party_money_transmission" + | "treasury_management"; + +export type AveniaKybSourceOfFunds = + | "business_loans" + | "grants" + | "inter_company_funds" + | "investment_proceeds" + | "legal_settlement" + | "owners_capital" + | "pension_retirement" + | "sale_of_assets" + | "sales_of_goods_and_services" + | "third_party_funds" + | "treasury_reserves"; + +export type AveniaKybNumberOfEmployees = "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001+"; + +export type AveniaKybAnnualRevenue = + | "less_than_100k" + | "100k_to_1m" + | "1m_to_10m" + | "10m_to_50m" + | "50m_to_100m" + | "more_than_100m"; + +export interface AveniaKybLevel1Payload { + uboIds: string[]; + companyLegalName: string; + companyRegistrationNumber: string; + taxIdentificationNumberTin: string; + businessActivityDescription: string; + reasonForAccountOpening: AveniaKybReasonForAccountOpening; + sourceOfFundsAndIncome: AveniaKybSourceOfFunds; + numberOfEmployees: AveniaKybNumberOfEmployees; + estimatedAnnualRevenueUsd: AveniaKybAnnualRevenue; + estimatedMonthlyVolumeUsd: string; + countryTaxResidence: string; + countrySubdivisionTaxResidence?: string; + companyStreetLine1: string; + companyStreetLine2?: string; + companyStreetLine3?: string; + companyCity: string; + companyState: string; + companyZipCode: string; + companyCountry: string; + certificateOfIncorporationDocumentId: string; + taxIdentificationDocumentId: string; + website?: string; + socialMedia?: string; + emailPixKey?: string; + sandboxReject?: boolean; +} + export interface KybLevel1Response { attemptId: string; authorizedRepresentativeUrl: string; @@ -364,7 +480,7 @@ export interface AveniaKybAttemptStatusResponse { attempt: { id: string; levelName: string; - submissionData: Record; + submissionData?: Record; status: KycAttemptStatus; result?: KycAttemptResult; resultMessage: string; @@ -378,8 +494,11 @@ export enum AveniaDocumentType { ID = "ID", DRIVERS_LICENSE = "DRIVERS-LICENSE", PASSPORT = "PASSPORT", + RESIDENCE_PERMIT = "RESIDENCE-PERMIT", SELFIE = "SELFIE", - SELFIE_FROM_LIVENESS = "SELFIE-FROM-LIVENESS" + SELFIE_FROM_LIVENESS = "SELFIE-FROM-LIVENESS", + CERTIFICATE_OF_INCORPORATION = "CERTIFICATE-OF-INCORPORATION", + COMPANY_TAX_IDENTIFICATION_DOCUMENT = "COMPANY-TAX-IDENTIFICATION-DOCUMENT" } export interface DocumentUploadRequest { @@ -395,6 +514,24 @@ export interface DocumentUploadResponse { validateLivenessToken?: string; } +export interface AveniaDocument { + id: string; + documentType: AveniaDocumentType; + uploadURLFront?: string; + uploadStatusFront: string; + uploadErrorFront?: string; + uploadURLBack?: string; + uploadStatusBack?: string; + uploadErrorBack?: string; + ready: boolean; + createdAt?: string; + updatedAt?: string; +} + +export interface AveniaDocumentResponse { + document: AveniaDocument; +} + export enum KycAttemptStatus { PENDING = "PENDING", PROCESSING = "PROCESSING", @@ -430,21 +567,7 @@ export interface CreateAveniaSubaccountRequest { } export interface AveniaDocumentGetResponse { - documents: [ - { - id: string; - documentType: string; - uploadURLFront: string; - uploadStatusFront: string; - uploadErrorFront: string; - uploadURLBack: string; - uploadStatusBack: string; - uploadErrorBack: string; - ready: true; - createdAt: Date; - updatedAt: Date; - } - ]; + documents: AveniaDocument[]; } export interface AveniaAccountBalanceResponse { From f10d2afa73bda44972a4f0ae8660cbbc8c805c5d Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Thu, 6 Aug 2026 16:30:38 -0300 Subject: [PATCH 7/9] feat(api): add Avenia API-based KYB Level 1 flow --- .../api/controllers/brla.controller.test.ts | 541 +++++++++++++++++- .../src/api/controllers/brla.controller.ts | 295 +++++++++- .../src/api/middlewares/validators.test.ts | 96 +++- apps/api/src/api/middlewares/validators.ts | 182 ++++++ apps/api/src/api/routes/v1/brla.route.ts | 28 +- .../avenia/avenia-customer.service.ts | 8 +- .../api/services/avenia/avenia-kyb.service.ts | 199 +++++++ .../database/kyb-submission-migration.test.ts | 16 + .../061-add-kyb-submission-state.ts | 51 ++ apps/api/src/models/kycCase.model.ts | 33 ++ .../tests/contracts/avenia.contract.test.ts | 3 + docs/security-spec/05-integrations/brla.md | 8 +- .../shared/src/endpoints/brla.endpoints.ts | 2 +- .../src/services/brla/brlaApiService.test.ts | 34 +- .../src/services/brla/brlaApiService.ts | 29 +- packages/shared/src/services/brla/schemas.ts | 61 +- packages/shared/src/services/brla/types.ts | 7 +- 17 files changed, 1526 insertions(+), 67 deletions(-) create mode 100644 apps/api/src/api/services/avenia/avenia-kyb.service.ts create mode 100644 apps/api/src/database/kyb-submission-migration.test.ts create mode 100644 apps/api/src/database/migrations/061-add-kyb-submission-state.ts diff --git a/apps/api/src/api/controllers/brla.controller.test.ts b/apps/api/src/api/controllers/brla.controller.test.ts index f6255fbcd..61058c9c7 100644 --- a/apps/api/src/api/controllers/brla.controller.test.ts +++ b/apps/api/src/api/controllers/brla.controller.test.ts @@ -1,20 +1,25 @@ import {AveniaAccountType, AveniaDocumentType, BrlaApiError, BrlaApiService, KycAttemptResult, KycAttemptStatus} from "@vortexfi/shared"; import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test"; import httpStatus from "http-status"; +import sequelize from "../../config/database"; import logger from "../../config/logger"; import CustomerEntity from "../../models/customerEntity.model"; import KycCase from "../../models/kycCase.model"; import PartnerManagedProfile from "../../models/partnerManagedProfile.model"; import ProviderCustomer, {VerificationStatus} from "../../models/providerCustomer.model"; import User from "../../models/user.model"; +import { hashAveniaKybSubmission } from "../services/avenia/avenia-kyb.service"; import { createSubaccount, + createKybDocument, + createKybUbo, fetchSubaccountKycStatus, getAveniaUser, getKybAttemptStatus, getUploadUrls, initiateKybLevel1, - recordInitialKycAttempt + recordInitialKycAttempt, + submitKybLevel1Api } from "./brla.controller"; function createResponse() { @@ -453,7 +458,16 @@ describe("Avenia company KYB", () => { const originalEntityFindOrCreate = CustomerEntity.findOrCreate; const originalKycCaseFindOne = KycCase.findOne; const originalKycCaseCreate = KycCase.create; + const originalKycCaseUpdate = KycCase.update; const originalGetInstance = BrlaApiService.getInstance; + const originalTransaction = sequelize.transaction; + let staticCaseUpdate: ReturnType; + + beforeEach(() => { + staticCaseUpdate = mock(async () => [1]); + KycCase.update = staticCaseUpdate as unknown as typeof KycCase.update; + sequelize.transaction = mock(async callback => callback({} as never)) as unknown as typeof sequelize.transaction; + }); afterEach(() => { ProviderCustomer.findOne = originalProviderFindOne; @@ -462,7 +476,9 @@ describe("Avenia company KYB", () => { CustomerEntity.findOrCreate = originalEntityFindOrCreate; KycCase.findOne = originalKycCaseFindOne; KycCase.create = originalKycCaseCreate; + KycCase.update = originalKycCaseUpdate; BrlaApiService.getInstance = originalGetInstance; + sequelize.transaction = originalTransaction; }); it("binds the initiated provider attempt to the owned KYB case", async () => { @@ -617,7 +633,7 @@ describe("Avenia company KYB", () => { expect(initiateMock).not.toHaveBeenCalled(); }); - it("still re-issues KYB links when the live probe is unavailable", async () => { + it("fails closed when the live attempt cannot be checked before re-initiation", async () => { mockEntityPerProfile(); const customerUpdate = mock(async () => undefined); ProviderCustomer.findOne = mock(async () => ({ @@ -652,8 +668,102 @@ describe("Avenia company KYB", () => { const res = createResponse(); await initiateKybLevel1({ query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, res as any); - expect(res.statusCode).toBe(httpStatus.OK); - expect(initiateMock).toHaveBeenCalled(); + expect(res.statusCode).toBe(httpStatus.BAD_GATEWAY); + expect(initiateMock).not.toHaveBeenCalled(); + }); + + it("does not start the hosted flow while an API submission needs reconciliation", async () => { + mockEntityPerProfile(); + ProviderCustomer.findOne = mock(async () => ({ + customerEntityId: "entity-user-1", + customerType: "business", + id: "customer-1", + providerSubaccountId: "subaccount-1", + status: VerificationStatus.Pending + })) as unknown as typeof ProviderCustomer.findOne; + KycCase.findOne = mock(async () => ({ + id: "case-1", + providerCaseId: null, + submissionStatus: "unknown" + })) as unknown as typeof KycCase.findOne; + const initiateMock = mock(async () => ({ attemptId: "attempt-2" })); + BrlaApiService.getInstance = mock( + () => ({ initiateKybLevel1: initiateMock }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await initiateKybLevel1({ query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, res as any); + + expect(res.statusCode).toBe(httpStatus.CONFLICT); + expect(initiateMock).not.toHaveBeenCalled(); + }); + + it("does not replace an accepted API attempt with a hosted attempt", async () => { + mockEntityPerProfile(); + ProviderCustomer.findOne = mock(async () => ({ + customerEntityId: "entity-user-1", + customerType: "business", + id: "customer-1", + providerSubaccountId: "subaccount-1", + status: VerificationStatus.Pending + })) as unknown as typeof ProviderCustomer.findOne; + KycCase.findOne = mock(async () => ({ + id: "case-1", + providerCaseId: "api-attempt", + statusExternal: KycAttemptStatus.PENDING, + submissionRequestHash: "api-request-hash", + submissionStatus: "submitted" + })) as unknown as typeof KycCase.findOne; + const providerStatus = mock(async () => ({ attempt: {} })); + const initiateMock = mock(async () => ({ attemptId: "hosted-attempt" })); + BrlaApiService.getInstance = mock( + () => ({ getKybAttemptStatus: providerStatus, initiateKybLevel1: initiateMock }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await initiateKybLevel1({ query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, res as any); + + expect(res.statusCode).toBe(httpStatus.CONFLICT); + expect(providerStatus).not.toHaveBeenCalled(); + expect(initiateMock).not.toHaveBeenCalled(); + }); + + it("does not replace a non-retryable rejected hosted attempt", async () => { + mockEntityPerProfile(); + ProviderCustomer.findOne = mock(async () => ({ + customerEntityId: "entity-user-1", + customerType: "business", + id: "customer-1", + providerSubaccountId: "subaccount-1", + status: VerificationStatus.Rejected + })) as unknown as typeof ProviderCustomer.findOne; + KycCase.findOne = mock(async () => ({ + id: "case-1", + providerCaseId: "attempt-1", + statusExternal: KycAttemptStatus.COMPLETED, + submissionStatus: "submitted" + })) as unknown as typeof KycCase.findOne; + const initiateMock = mock(async () => ({ attemptId: "attempt-2" })); + BrlaApiService.getInstance = mock( + () => + ({ + getKybAttemptStatus: mock(async () => ({ + attempt: { + id: "attempt-1", + result: KycAttemptResult.REJECTED, + retryable: false, + status: KycAttemptStatus.COMPLETED + } + })), + initiateKybLevel1: initiateMock + }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await initiateKybLevel1({ query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, res as any); + + expect(res.statusCode).toBe(httpStatus.CONFLICT); + expect(initiateMock).not.toHaveBeenCalled(); }); it("still rejects re-initiation once Avenia is processing the attempt", async () => { @@ -714,6 +824,7 @@ describe("Avenia company KYB", () => { ProviderCustomer.findByPk = mock(async () => ({ customerEntityId: "entity-user-1-individual", provider: "avenia", + providerSubaccountId: "subaccount-1", status: VerificationStatus.Approved })) as unknown as typeof ProviderCustomer.findByPk; @@ -721,7 +832,11 @@ describe("Avenia company KYB", () => { await getKybAttemptStatus({ query: { attemptId: "attempt-1" }, userId: "user-1" } as any, res as any); expect(res.statusCode).toBe(httpStatus.OK); - expect(res.body).toEqual({ result: KycAttemptResult.APPROVED, status: KycAttemptStatus.COMPLETED }); + expect(res.body).toEqual({ + result: KycAttemptResult.APPROVED, + retryable: false, + status: KycAttemptStatus.COMPLETED + }); expect(strayCreate).not.toHaveBeenCalled(); }); @@ -730,6 +845,7 @@ describe("Avenia company KYB", () => { const caseUpdate = mock(async () => undefined); KycCase.findOne = mock(async () => ({ customerEntityId: "entity-user-1", + id: "case-1", providerCustomerId: "customer-1", update: caseUpdate })) as unknown as typeof KycCase.findOne; @@ -737,6 +853,7 @@ describe("Avenia company KYB", () => { ProviderCustomer.findByPk = mock(async () => ({ customerEntityId: "entity-user-1", provider: "avenia", + providerSubaccountId: "subaccount-1", update: customerUpdate })) as unknown as typeof ProviderCustomer.findByPk; BrlaApiService.getInstance = mock( @@ -761,13 +878,57 @@ describe("Avenia company KYB", () => { const res = createResponse(); await getKybAttemptStatus({ query: { attemptId: "attempt-1" }, userId: "user-1" } as any, res as any); - expect(res.body).toEqual({ result: KycAttemptResult.APPROVED, status: KycAttemptStatus.COMPLETED }); + expect(res.body).toEqual({ + result: KycAttemptResult.APPROVED, + retryable: false, + status: KycAttemptStatus.COMPLETED + }); expect(customerUpdate).toHaveBeenCalledWith( - expect.objectContaining({ status: VerificationStatus.Approved, statusExternal: KycAttemptStatus.COMPLETED }) + expect.objectContaining({ status: VerificationStatus.Approved, statusExternal: KycAttemptStatus.COMPLETED }), + expect.anything() ); - expect(caseUpdate).toHaveBeenCalledWith( - expect.objectContaining({ status: VerificationStatus.Approved, statusExternal: KycAttemptStatus.COMPLETED }) + expect(staticCaseUpdate).toHaveBeenCalledWith( + expect.objectContaining({ status: VerificationStatus.Approved, statusExternal: KycAttemptStatus.COMPLETED }), + expect.objectContaining({ where: expect.objectContaining({ id: "case-1", providerCaseId: "attempt-1" }) }) + ); + }); + + it("does not let an old attempt poll overwrite its replacement", async () => { + mockEntityPerProfile(); + KycCase.findOne = mock(async () => ({ + customerEntityId: "entity-user-1", + id: "case-1", + providerCaseId: "attempt-old", + providerCustomerId: "customer-1" + })) as unknown as typeof KycCase.findOne; + const customerUpdate = mock(async () => undefined); + ProviderCustomer.findByPk = mock(async () => ({ + customerEntityId: "entity-user-1", + provider: "avenia", + providerSubaccountId: "subaccount-1", + update: customerUpdate + })) as unknown as typeof ProviderCustomer.findByPk; + staticCaseUpdate.mockImplementation(async () => [0]); + BrlaApiService.getInstance = mock( + () => + ({ + getKybAttemptStatus: mock(async () => ({ + attempt: { + id: "attempt-old", + result: KycAttemptResult.REJECTED, + resultMessage: "rejected", + retryable: true, + status: KycAttemptStatus.COMPLETED + } + })) + }) as unknown as BrlaApiService ); + + const res = createResponse(); + await getKybAttemptStatus({ query: { attemptId: "attempt-old" }, userId: "user-1" } as any, res as any); + + expect(res.statusCode).toBe(httpStatus.CONFLICT); + expect(customerUpdate).not.toHaveBeenCalled(); }); }); @@ -1050,3 +1211,365 @@ describe("getUploadUrls", () => { expect(uploadUrlsMock).not.toHaveBeenCalled(); }); }); + +describe("Avenia API KYB", () => { + const originals = { + caseCreate: KycCase.create, + caseFindOrCreate: KycCase.findOrCreate, + caseFindOne: KycCase.findOne, + caseUpdate: KycCase.update, + getInstance: BrlaApiService.getInstance, + providerFindOne: ProviderCustomer.findOne, + transaction: sequelize.transaction + }; + + const validSubmission = { + businessActivityDescription: "Software development", + certificateOfIncorporationDocumentId: "certificate-1", + companyCity: "Sao Paulo", + companyCountry: "BRA", + companyLegalName: "ACME LTDA", + companyRegistrationNumber: "42731085000167", + companyState: "SP", + companyStreetLine1: "Av Paulista 1000", + companyZipCode: "01310-100", + countryTaxResidence: "BRA", + estimatedAnnualRevenueUsd: "less_than_100k" as const, + estimatedMonthlyVolumeUsd: "2000", + numberOfEmployees: "1-10" as const, + reasonForAccountOpening: "receive_payments_for_goods_and_services" as const, + sourceOfFundsAndIncome: "sales_of_goods_and_services" as const, + taxIdentificationDocumentId: "tax-document-1", + taxIdentificationNumberTin: "42731085000167", + uboIds: ["ubo-1"] + }; + + beforeEach(() => { + logger.error = mock(() => logger) as typeof logger.error; + CustomerEntity.findAll = mock(async () => [{ id: "entity-user-1" }]) as unknown as typeof CustomerEntity.findAll; + }); + + afterEach(() => { + KycCase.create = originals.caseCreate; + KycCase.findOrCreate = originals.caseFindOrCreate; + KycCase.findOne = originals.caseFindOne; + KycCase.update = originals.caseUpdate; + BrlaApiService.getInstance = originals.getInstance; + ProviderCustomer.findOne = originals.providerFindOne; + sequelize.transaction = originals.transaction; + }); + + function mockBusinessAccount(update = mock(async () => undefined)) { + ProviderCustomer.findOne = mock(async () => ({ + customerEntityId: "entity-user-1", + customerType: "business", + id: "customer-1", + providerSubaccountId: "subaccount-1", + status: VerificationStatus.Pending, + statusExternal: null, + update + })) as unknown as typeof ProviderCustomer.findOne; + return update; + } + + function documentResponse(id: string) { + const documentType = + id === "certificate-1" + ? AveniaDocumentType.CERTIFICATE_OF_INCORPORATION + : id === "tax-document-1" + ? AveniaDocumentType.COMPANY_TAX_IDENTIFICATION_DOCUMENT + : AveniaDocumentType.PASSPORT; + return { + document: { documentType, id, ready: true, uploadStatusFront: "PROCESSED" } + }; + } + + function mockInitialSubmission(options: { submitError?: Error } = {}) { + const customerUpdate = mockBusinessAccount(); + const caseUpdate = mock(async () => undefined); + KycCase.findOrCreate = mock(async () => [ + { + id: "case-1", + providerCaseId: null, + submissionStatus: "not_started", + update: caseUpdate + }, + false + ]) as unknown as typeof KycCase.findOrCreate; + const claim = mock(async () => [1]); + KycCase.update = claim as unknown as typeof KycCase.update; + sequelize.transaction = mock(async callback => callback({} as never)) as unknown as typeof sequelize.transaction; + const submit = options.submitError + ? mock(async () => { + throw options.submitError; + }) + : mock(async () => ({ id: "attempt-1" })); + BrlaApiService.getInstance = mock( + () => + ({ + getUploadedDocument: mock(async (id: string) => documentResponse(id)), + submitKybLevel1: submit + }) as unknown as BrlaApiService + ); + return { caseUpdate, claim, customerUpdate, submit }; + } + + it("creates a company document for a profile-bound secret key", async () => { + mockBusinessAccount(); + const createDocument = mock(async () => ({ id: "document-1", uploadURLFront: "https://upload.example" })); + BrlaApiService.getInstance = mock( + () => ({ getDocumentUploadUrls: createDocument }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await createKybDocument( + { + body: { documentType: AveniaDocumentType.CERTIFICATE_OF_INCORPORATION }, + credential: { profileId: "user-1" }, + query: { subAccountId: "subaccount-1" } + } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.CREATED); + expect(createDocument).toHaveBeenCalledWith( + AveniaDocumentType.CERTIFICATE_OF_INCORPORATION, + false, + "subaccount-1" + ); + }); + + it("does not expose another profile's subaccount to document creation", async () => { + CustomerEntity.findAll = mock(async () => [{ id: "entity-attacker" }]) as unknown as typeof CustomerEntity.findAll; + mockBusinessAccount(); + const createDocument = mock(async () => ({ id: "document-1" })); + BrlaApiService.getInstance = mock( + () => ({ getDocumentUploadUrls: createDocument }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await createKybDocument( + { + body: { documentType: AveniaDocumentType.CERTIFICATE_OF_INCORPORATION }, + query: { subAccountId: "subaccount-1" }, + userId: "attacker" + } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.FORBIDDEN); + expect(createDocument).not.toHaveBeenCalled(); + }); + + it("rejects UBO creation until its identification document is ready", async () => { + mockBusinessAccount(); + const createUbo = mock(async () => ({ id: "ubo-1" })); + BrlaApiService.getInstance = mock( + () => + ({ + createUbo, + getUploadedDocument: mock(async () => ({ + document: { + documentType: AveniaDocumentType.PASSPORT, + id: "identity-1", + ready: false, + uploadStatusFront: "PROCESSING" + } + })) + }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await createKybUbo( + { + body: { uploadedIdentificationId: "identity-1" }, + query: { subAccountId: "subaccount-1" }, + userId: "user-1" + } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.CONFLICT); + expect(createUbo).not.toHaveBeenCalled(); + }); + + it("submits ready company documents and persists the pending attempt", async () => { + const { caseUpdate, claim, customerUpdate, submit } = mockInitialSubmission(); + + const res = createResponse(); + await submitKybLevel1Api( + { body: validSubmission, query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.OK); + expect(submit).toHaveBeenCalledWith(validSubmission, "subaccount-1"); + expect(claim).toHaveBeenCalledWith( + expect.objectContaining({ submissionStatus: "submitting" }), + expect.objectContaining({ + where: { id: "case-1", providerCaseId: null, submissionStatus: "not_started" } + }) + ); + expect(customerUpdate).toHaveBeenCalledWith( + expect.objectContaining({ status: VerificationStatus.Pending, statusExternal: KycAttemptStatus.PENDING }), + expect.anything() + ); + expect(caseUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + failureReasons: [], + providerCaseId: "attempt-1", + status: VerificationStatus.Pending, + submissionStatus: "submitted" + }), + expect.anything() + ); + }); + + it("allows a new attempt only after a provider-confirmed retryable rejection", async () => { + mockBusinessAccount(); + const caseUpdate = mock(async () => undefined); + KycCase.findOrCreate = mock(async () => [ + { + id: "case-1", + providerCaseId: "attempt-old", + submissionStatus: "submitted", + update: caseUpdate + }, + false + ]) as unknown as typeof KycCase.findOrCreate; + KycCase.update = mock(async () => [1]) as unknown as typeof KycCase.update; + sequelize.transaction = mock(async callback => callback({} as never)) as unknown as typeof sequelize.transaction; + const submit = mock(async () => ({ id: "attempt-new" })); + BrlaApiService.getInstance = mock( + () => + ({ + getKybAttemptStatus: mock(async () => ({ + attempt: { + id: "attempt-old", + result: KycAttemptResult.REJECTED, + retryable: true, + status: KycAttemptStatus.COMPLETED + } + })), + getUploadedDocument: mock(async (id: string) => documentResponse(id)), + submitKybLevel1: submit + }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await submitKybLevel1Api( + { body: validSubmission, query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.OK); + expect(caseUpdate).toHaveBeenCalledWith( + expect.objectContaining({ providerCaseId: "attempt-new", rejectedAt: null }), + expect.anything() + ); + }); + + it("rejects resubmission after a non-retryable provider decision", async () => { + mockBusinessAccount(); + KycCase.findOrCreate = mock(async () => [ + { + id: "case-1", + providerCaseId: "attempt-old", + submissionStatus: "submitted" + }, + false + ]) as unknown as typeof KycCase.findOrCreate; + const submit = mock(async () => ({ id: "attempt-new" })); + BrlaApiService.getInstance = mock( + () => + ({ + getKybAttemptStatus: mock(async () => ({ + attempt: { + id: "attempt-old", + result: KycAttemptResult.REJECTED, + retryable: false, + status: KycAttemptStatus.COMPLETED + } + })), + submitKybLevel1: submit + }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await submitKybLevel1Api( + { body: validSubmission, query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.CONFLICT); + expect(submit).not.toHaveBeenCalled(); + }); + + it("reconciles an accepted attempt after the original response was lost", async () => { + const customerUpdate = mockBusinessAccount(); + const caseUpdate = mock(async () => undefined); + KycCase.findOrCreate = mock(async () => [ + { + id: "case-1", + providerCaseId: null, + submissionRequestHash: hashAveniaKybSubmission(validSubmission), + submissionStartedAt: new Date("2026-08-06T12:00:00.000Z"), + submissionStatus: "unknown", + set: mock(() => undefined), + update: caseUpdate + }, + false + ]) as unknown as typeof KycCase.findOrCreate; + sequelize.transaction = mock(async callback => callback({} as never)) as unknown as typeof sequelize.transaction; + const submit = mock(async () => ({ id: "duplicate-attempt" })); + BrlaApiService.getInstance = mock( + () => + ({ + getKycAttempts: mock(async () => ({ + attempts: [ + { + createdAt: "2026-08-06T12:00:01.000Z", + id: "recovered-attempt", + levelName: "kyb-level-1" + } + ] + })), + submitKybLevel1: submit + }) as unknown as BrlaApiService + ); + + const res = createResponse(); + await submitKybLevel1Api( + { body: validSubmission, query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.OK); + expect(res.body).toEqual({ id: "recovered-attempt" }); + expect(submit).not.toHaveBeenCalled(); + expect(customerUpdate).toHaveBeenCalled(); + expect(caseUpdate).toHaveBeenCalledWith( + expect.objectContaining({ providerCaseId: "recovered-attempt", submissionStatus: "submitted" }), + expect.anything() + ); + }); + + it("marks an ambiguous provider submission failure unknown instead of replayable", async () => { + const providerError = new BrlaApiError({ + endpoint: "/v2/kyc/new-level-1/api", + method: "POST", + responseBody: "connection reset", + status: 0 + }); + const { caseUpdate } = mockInitialSubmission({ submitError: providerError }); + + const res = createResponse(); + await submitKybLevel1Api( + { body: validSubmission, query: { subAccountId: "subaccount-1" }, userId: "user-1" } as any, + res as any + ); + + expect(res.statusCode).toBe(httpStatus.BAD_GATEWAY); + expect(caseUpdate).toHaveBeenCalledWith({ submissionStatus: "unknown" }); + }); +}); diff --git a/apps/api/src/api/controllers/brla.controller.ts b/apps/api/src/api/controllers/brla.controller.ts index 5f409af2c..020c1bd4f 100644 --- a/apps/api/src/api/controllers/brla.controller.ts +++ b/apps/api/src/api/controllers/brla.controller.ts @@ -1,8 +1,13 @@ import { AveniaAccountType, + AveniaDocumentResponse, AveniaDocumentType, AveniaKYCDataUpload, AveniaKYCDataUploadRequest, + AveniaKybLevel1Payload, + AveniaUboPayload, + AveniaUboResponse, + BrlaApiError, BrlaApiService, BrlaCreateSubaccountRequest, BrlaCreateSubaccountResponse, @@ -19,6 +24,8 @@ import { BrlaPostRecordInitialKycAttemptRequest, BrlaValidatePixKeyRequest, BrlaValidatePixKeyResponse, + DocumentUploadRequest, + DocumentUploadResponse, isValidCnpj, isValidCpf, KybAttemptStatusResponse, @@ -33,6 +40,9 @@ import { } from "@vortexfi/shared"; import { Request, Response } from "express"; import httpStatus from "http-status"; +import { Op } from "sequelize"; +import { ZodError } from "zod"; +import sequelize from "../../config/database"; import logger from "../../config/logger"; import KycCase from "../../models/kycCase.model"; import ProviderCustomer, { VerificationStatus } from "../../models/providerCustomer.model"; @@ -48,6 +58,16 @@ import { updateAveniaKycOutcome, upsertAveniaKycCase } from "../services/avenia/avenia-customer.service"; +import { + AVENIA_IDENTITY_DOCUMENT_TYPES, + assertAveniaKybCanSubmit, + claimAveniaKybSubmission, + getOrCreateAveniaKybCase, + hashAveniaKybSubmission, + reconcileAveniaKybSubmission, + requireReadyAveniaDocument, + resolveOwnedAveniaBusinessAccount +} from "../services/avenia/avenia-kyb.service"; import { resolveAveniaAccountForUser } from "../services/avenia-account"; import { findCustomerEntityIdsForProfile, getOrCreateCustomerEntityForProfile } from "../services/customer-entity.service"; @@ -82,6 +102,16 @@ function handleApiError(error: unknown, res: Response, apiMethod: string): void return; } + if (error instanceof BrlaApiError && error.status !== 400) { + res.status(httpStatus.BAD_GATEWAY).json({ error: "Avenia request failed" }); + return; + } + + if (error instanceof ZodError) { + res.status(httpStatus.BAD_GATEWAY).json({ error: "Avenia returned an invalid response" }); + return; + } + if (error instanceof Error && error.message.includes("status '400'")) { const splitError = error.message.split("Error: ", 2); if (splitError.length > 1) { @@ -708,6 +738,163 @@ export const newKyc = async ( } }; +async function resolveAveniaKybAccount( + req: Pick, + subAccountId: string | undefined +): Promise { + const effectiveUserId = getEffectiveUserId(req); + if (!effectiveUserId) { + throw new APIError({ message: "This endpoint requires authentication.", status: httpStatus.BAD_REQUEST }); + } + return resolveOwnedAveniaBusinessAccount(effectiveUserId, subAccountId); +} + +export const createKybDocument = async ( + req: Request, + res: Response +): Promise => { + try { + const record = await resolveAveniaKybAccount(req, req.query.subAccountId); + const response = await BrlaApiService.getInstance().getDocumentUploadUrls( + req.body.documentType, + req.body.isDoubleSided ?? false, + record.providerSubaccountId as string + ); + res.status(httpStatus.CREATED).json(response); + } catch (error) { + handleApiError(error, res, "createKybDocument"); + } +}; + +export const getKybDocument = async ( + req: Request<{ documentId: string }, unknown, unknown, { subAccountId?: string }>, + res: Response +): Promise => { + try { + const record = await resolveAveniaKybAccount(req, req.query.subAccountId); + const response: AveniaDocumentResponse = await BrlaApiService.getInstance().getUploadedDocument( + req.params.documentId, + record.providerSubaccountId as string + ); + if (response.document.id !== req.params.documentId) { + throw new APIError({ message: "Avenia returned a mismatched document", status: httpStatus.BAD_GATEWAY }); + } + const { document } = response; + res.status(httpStatus.OK).json({ + document: { + documentType: document.documentType, + id: document.id, + ready: document.ready, + ...(document.uploadErrorBack ? { uploadErrorBack: document.uploadErrorBack } : {}), + ...(document.uploadErrorFront ? { uploadErrorFront: document.uploadErrorFront } : {}), + ...(document.uploadStatusBack ? { uploadStatusBack: document.uploadStatusBack } : {}), + uploadStatusFront: document.uploadStatusFront + } + }); + } catch (error) { + handleApiError(error, res, "getKybDocument"); + } +}; + +export const createKybUbo = async ( + req: Request, + res: Response +): Promise => { + try { + const record = await resolveAveniaKybAccount(req, req.query.subAccountId); + const brlaApiService = BrlaApiService.getInstance(); + const subAccountId = record.providerSubaccountId as string; + await requireReadyAveniaDocument( + brlaApiService, + subAccountId, + req.body.uploadedIdentificationId, + AVENIA_IDENTITY_DOCUMENT_TYPES + ); + if (req.body.uploadedSelfieId) { + await requireReadyAveniaDocument(brlaApiService, subAccountId, req.body.uploadedSelfieId, [ + AveniaDocumentType.SELFIE_FROM_LIVENESS + ]); + } + const response = await brlaApiService.createUbo(req.body, subAccountId); + res.status(httpStatus.CREATED).json(response); + } catch (error) { + handleApiError(error, res, "createKybUbo"); + } +}; + +export const submitKybLevel1Api = async ( + req: Request, + res: Response +): Promise => { + try { + const record = await resolveAveniaKybAccount(req, req.query.subAccountId); + const subAccountId = record.providerSubaccountId as string; + const brlaApiService = BrlaApiService.getInstance(); + const kycCase = await getOrCreateAveniaKybCase(record); + const requestHash = hashAveniaKybSubmission(req.body); + const reconciledAttemptId = await reconcileAveniaKybSubmission(brlaApiService, record, kycCase, subAccountId, requestHash); + if (reconciledAttemptId) { + res.status(httpStatus.OK).json({ id: reconciledAttemptId }); + return; + } + await assertAveniaKybCanSubmit(brlaApiService, record, kycCase, subAccountId); + await Promise.all([ + requireReadyAveniaDocument(brlaApiService, subAccountId, req.body.certificateOfIncorporationDocumentId, [ + AveniaDocumentType.CERTIFICATE_OF_INCORPORATION + ]), + requireReadyAveniaDocument(brlaApiService, subAccountId, req.body.taxIdentificationDocumentId, [ + AveniaDocumentType.COMPANY_TAX_IDENTIFICATION_DOCUMENT + ]) + ]); + + await claimAveniaKybSubmission(kycCase, requestHash); + let response: KycLevel1Response; + try { + response = await brlaApiService.submitKybLevel1(req.body, subAccountId); + } catch (error) { + const knownRejection = error instanceof BrlaApiError && error.status === httpStatus.BAD_REQUEST; + await kycCase.update({ + submissionStatus: knownRejection && !kycCase.providerCaseId ? "not_started" : knownRejection ? "submitted" : "unknown" + }); + throw error; + } + + const now = new Date(); + try { + await sequelize.transaction(async transaction => { + await record.update( + { + lastFailureReasons: [], + status: VerificationStatus.Pending, + statusExternal: KycAttemptStatus.PENDING + }, + { transaction } + ); + await kycCase.update( + { + approvedAt: null, + failureReasons: [], + providerCaseId: response.id, + rejectedAt: null, + status: VerificationStatus.Pending, + statusExternal: KycAttemptStatus.PENDING, + submissionStatus: "submitted", + submittedAt: now + }, + { transaction } + ); + }); + } catch (error) { + logger.error("Failed to persist the accepted Avenia KYB attempt", { attemptId: response.id, error }); + await kycCase.update({ submissionStatus: "unknown" }); + throw error; + } + res.status(httpStatus.OK).json(response); + } catch (error) { + handleApiError(error, res, "submitKybLevel1Api"); + } +}; + /** * Initiates KYB Level 1 verification process using the Web SDK * @@ -761,29 +948,37 @@ export const initiateKybLevel1 = async ( const existingKybCase = await KycCase.findOne({ where: { providerCustomerId: record.id, type: "kyb" } }); + const brlaApiService = BrlaApiService.getInstance(); + const kycCase = existingKybCase ?? (await getOrCreateAveniaKybCase(record)); + const requestHash = hashAveniaKybSubmission({ flow: "web-sdk" }); + if (kycCase.submissionRequestHash && kycCase.submissionRequestHash !== requestHash) { + res.status(httpStatus.CONFLICT).json({ error: "An API-based KYB attempt is already bound to this company" }); + return; + } + await reconcileAveniaKybSubmission(brlaApiService, record, kycCase, subAccountId, requestHash); // A PENDING attempt means the user never completed Avenia's hosted steps. The hosted URLs are // not stored, so re-initiation is the only way to surface them again — allow it and rebind the // case to the fresh attempt. Only an attempt Avenia is processing (or has decided) blocks. if ( - existingKybCase?.providerCaseId && + kycCase.providerCaseId && record.status !== VerificationStatus.Rejected && - existingKybCase.statusExternal !== KycAttemptStatus.EXPIRED && - existingKybCase.statusExternal !== KycAttemptStatus.PENDING + kycCase.statusExternal !== KycAttemptStatus.EXPIRED && + kycCase.statusExternal !== KycAttemptStatus.PENDING ) { res.status(httpStatus.CONFLICT).json({ error: "A KYB attempt is already in progress" }); return; } - const brlaApiService = BrlaApiService.getInstance(); - // The stored status can lag (the hosted steps may have just been finished in another tab): // probe the live attempt before re-initiating so a processing/approved attempt is not // orphaned by rebinding the case to a fresh one. A rejected decision stays re-initiable - // (that is the retry path), and a failing probe must not lock the user out of resuming. - if (existingKybCase?.providerCaseId) { + // (that is the retry path). A failed probe must fail closed because rebinding could orphan + // an API or hosted attempt that Avenia already accepted. + if (kycCase.providerCaseId) { try { - const { attempt } = await brlaApiService.getKybAttemptStatus(existingKybCase.providerCaseId); - const decidedRejected = attempt.status === KycAttemptStatus.COMPLETED && attempt.result === KycAttemptResult.REJECTED; + const { attempt } = await brlaApiService.getKybAttemptStatus(kycCase.providerCaseId, subAccountId); + const decidedRejected = + attempt.status === KycAttemptStatus.COMPLETED && attempt.result === KycAttemptResult.REJECTED && attempt.retryable; const resumable = attempt.status === KycAttemptStatus.PENDING || attempt.status === KycAttemptStatus.EXPIRED || decidedRejected; if (!resumable) { @@ -791,11 +986,24 @@ export const initiateKybLevel1 = async ( return; } } catch { - // Re-initiation is the only path back to the hosted steps; keep it available if the probe fails. + throw new APIError({ + message: "Unable to verify the existing KYB attempt before re-initiation", + status: httpStatus.BAD_GATEWAY + }); } } - const response = await brlaApiService.initiateKybLevel1(subAccountId); + await claimAveniaKybSubmission(kycCase, requestHash); + let response: KybLevel1Response; + try { + response = await brlaApiService.initiateKybLevel1(subAccountId); + } catch (error) { + const knownRejection = error instanceof BrlaApiError && error.status === httpStatus.BAD_REQUEST; + await kycCase.update({ + submissionStatus: knownRejection && !kycCase.providerCaseId ? "not_started" : knownRejection ? "submitted" : "unknown" + }); + throw error; + } // The attempt starts PENDING at Avenia — nothing is submitted until the user finishes the hosted // steps — so our status stays pending (dashboard keeps offering Continue). in_review is set only // once Avenia reports PROCESSING. @@ -852,18 +1060,25 @@ export const getKybAttemptStatus = async ( } const record = kycCase.providerCustomerId ? await ProviderCustomer.findByPk(kycCase.providerCustomerId) : null; - if (!record || !ownedEntityIds.includes(record.customerEntityId) || record.provider !== "avenia") { + if ( + !record || + !record.providerSubaccountId || + !ownedEntityIds.includes(record.customerEntityId) || + record.provider !== "avenia" + ) { res.status(httpStatus.NOT_FOUND).json({ error: "KYB account not found" }); return; } if (record.status === VerificationStatus.Approved) { - res.status(httpStatus.OK).json({ result: KycAttemptResult.APPROVED, status: KycAttemptStatus.COMPLETED }); + res + .status(httpStatus.OK) + .json({ result: KycAttemptResult.APPROVED, retryable: false, status: KycAttemptStatus.COMPLETED }); return; } const brlaApiService = BrlaApiService.getInstance(); - const response = await brlaApiService.getKybAttemptStatus(attemptId); + const response = await brlaApiService.getKybAttemptStatus(attemptId, record.providerSubaccountId); const attempt = response.attempt; if (attempt.id !== attemptId) { throw new APIError({ message: "Avenia returned a mismatched KYB attempt", status: httpStatus.BAD_GATEWAY }); @@ -885,22 +1100,54 @@ export const getKybAttemptStatus = async ( ...(approved ? { approvedAt: new Date(), rejectedAt: null } : {}), ...(rejected ? { approvedAt: null, rejectedAt: new Date() } : {}) }; + const nonTerminalStatuses = [VerificationStatus.Pending, VerificationStatus.Started, VerificationStatus.InReview]; + const updateWhere = { + id: kycCase.id, + providerCaseId: attemptId, + status: { [Op.in]: nonTerminalStatuses }, + ...(attempt.status === KycAttemptStatus.PENDING + ? { [Op.or]: [{ statusExternal: null }, { statusExternal: KycAttemptStatus.PENDING }] } + : attempt.status === KycAttemptStatus.PROCESSING + ? { + [Op.or]: [ + { statusExternal: null }, + { statusExternal: { [Op.in]: [KycAttemptStatus.PENDING, KycAttemptStatus.PROCESSING] } } + ] + } + : {}) + }; - await record.update({ - lastFailureReasons: failureReason ? [failureReason] : [], - status: normalizedStatus, - statusExternal: attempt.status - }); - await kycCase.update({ - failureReasons: failureReason ? [failureReason] : [], - status: normalizedStatus, - statusExternal: attempt.status, - ...lifecycle + const persisted = await sequelize.transaction(async transaction => { + const [updatedCases] = await KycCase.update( + { + failureReasons: failureReason ? [failureReason] : [], + status: normalizedStatus, + statusExternal: attempt.status, + ...lifecycle + }, + { transaction, where: updateWhere } + ); + if (updatedCases !== 1) { + return false; + } + await record.update( + { + lastFailureReasons: failureReason ? [failureReason] : [], + status: normalizedStatus, + statusExternal: attempt.status + }, + { transaction } + ); + return true; }); + if (!persisted) { + throw new APIError({ message: "This KYB attempt is no longer current", status: httpStatus.CONFLICT }); + } res.status(httpStatus.OK).json({ ...(failureReason ? { failureReason } : {}), ...(attempt.result ? { result: attempt.result } : {}), + retryable: attempt.retryable, status: attempt.status }); } catch (error) { diff --git a/apps/api/src/api/middlewares/validators.test.ts b/apps/api/src/api/middlewares/validators.test.ts index d8c647b2b..117798d80 100644 --- a/apps/api/src/api/middlewares/validators.test.ts +++ b/apps/api/src/api/middlewares/validators.test.ts @@ -1,9 +1,15 @@ -import { Networks, QuoteError, RampDirection } from "@vortexfi/shared"; +import { AveniaDocumentType, Networks, QuoteError, RampDirection } from "@vortexfi/shared"; import { describe, expect, it, mock } from "bun:test"; import type { NextFunction, Request, Response } from "express"; import httpStatus from "http-status"; import { APIError } from "../errors/api-error"; -import { validateCreateBestQuoteInput, validateKycSubmission } from "./validators"; +import { + validateAveniaKybDocument, + validateAveniaKybLevel1, + validateAveniaKybUbo, + validateCreateBestQuoteInput, + validateKycSubmission +} from "./validators"; function buildRes() { const res: Partial & { statusCode?: number; body?: unknown } = {}; @@ -94,6 +100,92 @@ describe("validateCreateBestQuoteInput - networks whitelist", () => { }); }); +describe("Avenia API KYB validators", () => { + it("rejects double-sided corporate documents", () => { + const req = { + body: { documentType: AveniaDocumentType.CERTIFICATE_OF_INCORPORATION, isDoubleSided: true } + } as Request; + const res = buildRes(); + const next = mock(() => undefined) as unknown as NextFunction; + + validateAveniaKybDocument(req, res, next); + + expect(res.statusCode).toBe(httpStatus.BAD_REQUEST); + expect(next).not.toHaveBeenCalled(); + }); + + it("accepts double-sided UBO identification documents", () => { + const req = { + body: { documentType: AveniaDocumentType.ID, isDoubleSided: true } + } as Request; + const res = buildRes(); + const next = mock(() => undefined) as unknown as NextFunction; + + validateAveniaKybDocument(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(res.statusCode).toBeUndefined(); + }); + + it("rejects final submission without a UBO", () => { + const req = { + body: { + businessActivityDescription: "Software development", + certificateOfIncorporationDocumentId: "certificate-1", + companyCity: "Sao Paulo", + companyCountry: "BRA", + companyLegalName: "ACME LTDA", + companyRegistrationNumber: "42731085000167", + companyState: "SP", + companyStreetLine1: "Av Paulista 1000", + companyZipCode: "01310-100", + countryTaxResidence: "BRA", + estimatedAnnualRevenueUsd: "less_than_100k", + estimatedMonthlyVolumeUsd: "2000", + numberOfEmployees: "1-10", + reasonForAccountOpening: "receive_payments_for_goods_and_services", + sourceOfFundsAndIncome: "sales_of_goods_and_services", + taxIdentificationDocumentId: "tax-document-1", + taxIdentificationNumberTin: "42731085000167", + uboIds: [] + } + } as unknown as Request; + const res = buildRes(); + const next = mock(() => undefined) as unknown as NextFunction; + + validateAveniaKybLevel1(req, res, next); + + expect(res.statusCode).toBe(httpStatus.BAD_REQUEST); + expect(next).not.toHaveBeenCalled(); + }); + + it("rejects an underage UBO before calling Avenia", () => { + const req = { + body: { + city: "Sao Paulo", + country: "BRA", + countryOfTaxId: "BRA", + dateOfBirth: new Date().toISOString().slice(0, 10), + documentCountry: "BRA", + fullName: "Test Owner", + percentageOfOwnership: "100", + state: "SP", + streetLine1: "Av Paulista 1000", + taxIdNumber: "08786985906", + uploadedIdentificationId: "identity-1", + zipCode: "01310-100" + } + } as unknown as Request; + const res = buildRes(); + const next = mock(() => undefined) as unknown as NextFunction; + + validateAveniaKybUbo(req, res, next); + + expect(res.statusCode).toBe(httpStatus.BAD_REQUEST); + expect(next).not.toHaveBeenCalled(); + }); +}); + describe("validateKycSubmission", () => { it("forwards structured API errors for invalid Argentina submissions", () => { const req = { diff --git a/apps/api/src/api/middlewares/validators.ts b/apps/api/src/api/middlewares/validators.ts index 804851c1d..068d15332 100644 --- a/apps/api/src/api/middlewares/validators.ts +++ b/apps/api/src/api/middlewares/validators.ts @@ -1,5 +1,8 @@ import { + AveniaDocumentType, AveniaKYCDataUploadRequest, + AveniaKybLevel1Payload, + AveniaUboPayload, CreateAveniaSubaccountRequest, CreateBestQuoteRequest, CreateQuoteRequest, @@ -9,6 +12,7 @@ import { getCaseSensitiveNetwork, isSupportedFiatCurrency, isValidAveniaAccountType, + isValidCpf, isValidCurrencyForDirection, isValidDirection, isValidKYCDocType, @@ -26,6 +30,7 @@ import { } from "@vortexfi/shared"; import { Request, RequestHandler, Response } from "express"; import httpStatus from "http-status"; +import { z } from "zod"; import logger from "../../config/logger"; import { CONTACT_SHEET_HEADER_VALUES } from "../controllers/contact.controller"; import { EMAIL_SHEET_HEADER_VALUES } from "../controllers/email.controller"; @@ -625,3 +630,180 @@ export const validateStartKyc2: RequestHandler = (req, res, next) => { next(); }; + +const nonEmptyString = z.string().trim().min(1); +const isoAlpha3 = z.string().regex(/^[A-Z]{3}$/, "Must be an ISO 3166-1 alpha-3 country code"); + +function isAdultDate(value: string): boolean { + const [year, month, day] = value.split("-").map(Number); + const date = new Date(Date.UTC(year, month - 1, day)); + if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) { + return false; + } + const minimumBirthDate = new Date(); + minimumBirthDate.setUTCFullYear(minimumBirthDate.getUTCFullYear() - 18); + return date <= minimumBirthDate; +} + +const aveniaDocumentUploadSchema = z + .object({ + documentType: z.enum(AveniaDocumentType), + isDoubleSided: z.boolean().optional() + }) + .strict() + .superRefine((value, context) => { + const identificationTypes = new Set([ + AveniaDocumentType.ID, + AveniaDocumentType.DRIVERS_LICENSE, + AveniaDocumentType.PASSPORT, + AveniaDocumentType.RESIDENCE_PERMIT + ]); + if (value.isDoubleSided && !identificationTypes.has(value.documentType)) { + context.addIssue({ code: "custom", message: "Only identification documents may be double-sided" }); + } + }); + +const aveniaUboSchema: z.ZodType = z + .object({ + city: nonEmptyString, + country: isoAlpha3, + countryOfTaxId: isoAlpha3, + dateOfBirth: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .refine(isAdultDate, "UBO must be at least 18 years old"), + documentCountry: isoAlpha3, + email: z.email().optional(), + fullName: nonEmptyString.max(256), + hasControl: z + .enum([ + "CEO", + "CFO", + "COO", + "CTO", + "President", + "Vice President", + "Director", + "Managing Director", + "Managing Partner", + "General Partner", + "Partner", + "Secretary", + "Treasurer", + "Chairman", + "Board Member", + "Authorized Signatory", + "General Counsel", + "Owner", + "Founder", + "Manager", + "Member", + "Comptroller", + "Chief Compliance Officer" + ]) + .optional(), + percentageOfOwnership: nonEmptyString.refine(value => { + const percentage = Number(value); + return Number.isFinite(percentage) && percentage >= 0 && percentage <= 100; + }, "percentageOfOwnership must be between 0 and 100"), + phone: z + .string() + .regex(/^\+[1-9]\d{7,14}$/, "Phone must use E.164 format") + .optional(), + state: nonEmptyString, + streetLine1: nonEmptyString.max(256), + streetLine2: z.string().optional(), + streetLine3: z.string().optional(), + taxIdNumber: nonEmptyString, + uploadedIdentificationId: nonEmptyString, + uploadedSelfieId: nonEmptyString.optional(), + zipCode: nonEmptyString + }) + .strict() + .superRefine((value, context) => { + if (value.countryOfTaxId === "BRA" && !isValidCpf(value.taxIdNumber)) { + context.addIssue({ code: "custom", message: "taxIdNumber must be a valid CPF for BRA", path: ["taxIdNumber"] }); + } + if (value.countryOfTaxId === "USA" && !/^\d{9}$/.test(value.taxIdNumber)) { + context.addIssue({ code: "custom", message: "taxIdNumber must contain 9 digits for USA", path: ["taxIdNumber"] }); + } + }); + +const aveniaKybLevel1Schema: z.ZodType = z + .object({ + businessActivityDescription: nonEmptyString.max(2000), + certificateOfIncorporationDocumentId: nonEmptyString, + companyCity: nonEmptyString.max(256), + companyCountry: nonEmptyString, + companyLegalName: nonEmptyString, + companyRegistrationNumber: nonEmptyString, + companyState: nonEmptyString, + companyStreetLine1: nonEmptyString.max(256), + companyStreetLine2: z.string().optional(), + companyStreetLine3: z.string().optional(), + companyZipCode: nonEmptyString.max(256), + countrySubdivisionTaxResidence: nonEmptyString.optional(), + countryTaxResidence: z.union([isoAlpha3, z.literal("N/A")]), + emailPixKey: z.email().optional(), + estimatedAnnualRevenueUsd: z.enum([ + "less_than_100k", + "100k_to_1m", + "1m_to_10m", + "10m_to_50m", + "50m_to_100m", + "more_than_100m" + ]), + estimatedMonthlyVolumeUsd: z.string().regex(/^[1-9]\d*$/, "Must be a positive integer"), + numberOfEmployees: z.enum(["1-10", "11-50", "51-200", "201-500", "501-1000", "1001+"]), + reasonForAccountOpening: z.enum([ + "charitable_donations", + "ecommerce_retail_payments", + "investment_purposes", + "other", + "payments_to_friends_or_family_abroad", + "payroll", + "personal_or_living_expenses", + "protect_wealth", + "purchase_goods_and_services", + "receive_payments_for_goods_and_services", + "tax_optimization", + "third_party_money_transmission", + "treasury_management" + ]), + sandboxReject: z.boolean().optional(), + socialMedia: z.url().optional(), + sourceOfFundsAndIncome: z.enum([ + "business_loans", + "grants", + "inter_company_funds", + "investment_proceeds", + "legal_settlement", + "owners_capital", + "pension_retirement", + "sale_of_assets", + "sales_of_goods_and_services", + "third_party_funds", + "treasury_reserves" + ]), + taxIdentificationDocumentId: nonEmptyString, + taxIdentificationNumberTin: nonEmptyString, + uboIds: z.array(nonEmptyString).min(1).max(50), + website: z.url().optional() + }) + .strict(); + +function validateAveniaKybBody(schema: z.ZodType): RequestHandler { + return (req, res, next) => { + const parsed = schema.safeParse(req.body); + if (!parsed.success) { + res.status(httpStatus.BAD_REQUEST).json({ details: z.prettifyError(parsed.error), error: "Invalid request" }); + return; + } + req.body = parsed.data; + next(); + }; +} + +export const validateAveniaKybDocument = validateAveniaKybBody(aveniaDocumentUploadSchema); +export const validateAveniaKybUbo = validateAveniaKybBody(aveniaUboSchema); +export const validateAveniaKybLevel1 = validateAveniaKybBody(aveniaKybLevel1Schema); diff --git a/apps/api/src/api/routes/v1/brla.route.ts b/apps/api/src/api/routes/v1/brla.route.ts index db0bc4c2b..83136be3b 100644 --- a/apps/api/src/api/routes/v1/brla.route.ts +++ b/apps/api/src/api/routes/v1/brla.route.ts @@ -2,7 +2,13 @@ import { RequestHandler, Router } from "express"; import * as brlaController from "../../controllers/brla.controller"; import { optionalPartnerOrUserAuth, requirePartnerOrUserAuth } from "../../middlewares/dualAuth"; import { optionalAuth, requireAuth } from "../../middlewares/supabaseAuth"; -import { validateStartKyc2, validateSubaccountCreation } from "../../middlewares/validators"; +import { + validateAveniaKybDocument, + validateAveniaKybLevel1, + validateAveniaKybUbo, + validateStartKyc2, + validateSubaccountCreation +} from "../../middlewares/validators"; const router: Router = Router({ mergeParams: true }); @@ -37,7 +43,25 @@ router.route("/newKyc").post(requireAuth, brlaController.newKyc); router.route("/kyb/new-level-1/web-sdk").post(requireAuth, brlaController.initiateKybLevel1); -router.route("/kyb/attempt-status").get(requireAuth, brlaController.getKybAttemptStatus); +router + .route("/kyb/documents") + .post(validateAveniaKybDocument, requirePartnerOrUserAuth(), brlaController.createKybDocument as unknown as RequestHandler); + +router + .route("/kyb/documents/:documentId") + .get(requirePartnerOrUserAuth(), brlaController.getKybDocument as unknown as RequestHandler); + +router + .route("/kyb/ubos") + .post(validateAveniaKybUbo, requirePartnerOrUserAuth(), brlaController.createKybUbo as unknown as RequestHandler); + +router + .route("/kyb/new-level-1/api") + .post(validateAveniaKybLevel1, requirePartnerOrUserAuth(), brlaController.submitKybLevel1Api as unknown as RequestHandler); + +router + .route("/kyb/attempt-status") + .get(requirePartnerOrUserAuth(), brlaController.getKybAttemptStatus as unknown as RequestHandler); router.route("/kyc/record-attempt").post(requireAuth, brlaController.recordInitialKycAttempt); diff --git a/apps/api/src/api/services/avenia/avenia-customer.service.ts b/apps/api/src/api/services/avenia/avenia-customer.service.ts index f8b79744b..591e044c9 100644 --- a/apps/api/src/api/services/avenia/avenia-customer.service.ts +++ b/apps/api/src/api/services/avenia/avenia-customer.service.ts @@ -53,7 +53,12 @@ export async function upsertAveniaKycCase( const existing = await KycCase.findOne({ where: { providerCustomerId: record.id } }); if (existing) { - await existing.update({ ...(providerCaseId ? { providerCaseId } : {}), status, statusExternal, ...lifecycle }); + await existing.update({ + ...(providerCaseId ? { providerCaseId, submissionStatus: "submitted" as const } : {}), + status, + statusExternal, + ...lifecycle + }); return; } await KycCase.create({ @@ -64,6 +69,7 @@ export async function upsertAveniaKycCase( providerCustomerId: record.id, status, statusExternal, + submissionStatus: providerCaseId ? "submitted" : "not_started", type: record.customerType === "business" ? "kyb" : "kyc", ...lifecycle }); diff --git a/apps/api/src/api/services/avenia/avenia-kyb.service.ts b/apps/api/src/api/services/avenia/avenia-kyb.service.ts new file mode 100644 index 000000000..d3b2785c7 --- /dev/null +++ b/apps/api/src/api/services/avenia/avenia-kyb.service.ts @@ -0,0 +1,199 @@ +import crypto from "node:crypto"; +import { + AveniaDocument, + AveniaDocumentType, + AveniaKybLevel1Payload, + BrlaApiService, + KycAttemptResult, + KycAttemptStatus +} from "@vortexfi/shared"; +import httpStatus from "http-status"; +import sequelize from "../../../config/database"; +import KycCase from "../../../models/kycCase.model"; +import ProviderCustomer, { VerificationStatus } from "../../../models/providerCustomer.model"; +import { APIError } from "../../errors/api-error"; +import { findCustomerEntityIdsForProfile } from "../customer-entity.service"; +import { findAveniaCustomerBySubaccountId } from "./avenia-customer.service"; + +export async function resolveOwnedAveniaBusinessAccount( + profileId: string, + subAccountId: string | undefined +): Promise { + if (!subAccountId) { + throw new APIError({ message: "Missing subAccountId", status: httpStatus.BAD_REQUEST }); + } + + const record = await findAveniaCustomerBySubaccountId(subAccountId); + if (!record) { + throw new APIError({ message: "Subaccount not found", status: httpStatus.NOT_FOUND }); + } + const ownedEntityIds = await findCustomerEntityIdsForProfile(profileId); + if (!ownedEntityIds.includes(record.customerEntityId)) { + throw new APIError({ message: "This subaccount is not linked to your user profile.", status: httpStatus.FORBIDDEN }); + } + if (record.customerType !== "business") { + throw new APIError({ message: "KYB Level 1 is only available for COMPANY accounts.", status: httpStatus.BAD_REQUEST }); + } + return record; +} + +export async function getOrCreateAveniaKybCase(record: ProviderCustomer): Promise { + const [kycCase] = await KycCase.findOrCreate({ + defaults: { + customerEntityId: record.customerEntityId, + level: "level_1", + provider: "avenia", + status: record.status, + statusExternal: record.statusExternal, + type: "kyb" + }, + where: { providerCustomerId: record.id } + }); + return kycCase; +} + +export async function requireReadyAveniaDocument( + brlaApiService: BrlaApiService, + subAccountId: string, + documentId: string, + allowedTypes: AveniaDocumentType[] +): Promise { + const { document } = await brlaApiService.getUploadedDocument(documentId, subAccountId); + if (document.id !== documentId) { + throw new APIError({ message: "Avenia returned a mismatched document", status: httpStatus.BAD_GATEWAY }); + } + if (!allowedTypes.includes(document.documentType)) { + throw new APIError({ message: "Document type does not match this KYB field", status: httpStatus.BAD_REQUEST }); + } + if (!document.ready) { + throw new APIError({ message: "Document is not ready", status: httpStatus.CONFLICT }); + } + return document; +} + +export async function assertAveniaKybCanSubmit( + brlaApiService: BrlaApiService, + record: ProviderCustomer, + kycCase: KycCase, + subAccountId: string +): Promise { + if (record.status === VerificationStatus.Approved) { + throw new APIError({ message: "This company is already approved", status: httpStatus.CONFLICT }); + } + if (kycCase.submissionStatus === "submitting") { + throw new APIError({ message: "A KYB submission is already in progress", status: httpStatus.CONFLICT }); + } + if (kycCase.submissionStatus === "unknown") { + throw new APIError({ + message: "The previous KYB submission outcome is unknown and must be reconciled before retrying", + status: httpStatus.CONFLICT + }); + } + if (!kycCase.providerCaseId) { + return; + } + + const { attempt } = await brlaApiService.getKybAttemptStatus(kycCase.providerCaseId, subAccountId); + if (attempt.id !== kycCase.providerCaseId) { + throw new APIError({ message: "Avenia returned a mismatched KYB attempt", status: httpStatus.BAD_GATEWAY }); + } + const retryableRejection = + attempt.status === KycAttemptStatus.COMPLETED && attempt.result === KycAttemptResult.REJECTED && attempt.retryable; + if (!retryableRejection) { + throw new APIError({ + message: + attempt.status === KycAttemptStatus.COMPLETED && attempt.result === KycAttemptResult.REJECTED + ? "This KYB rejection is not retryable" + : "A KYB attempt is already in progress or has been approved", + status: httpStatus.CONFLICT + }); + } +} + +export function hashAveniaKybSubmission(payload: AveniaKybLevel1Payload | Record): string { + return crypto.createHash("sha256").update(JSON.stringify(payload), "utf8").digest("hex"); +} + +export async function reconcileAveniaKybSubmission( + brlaApiService: BrlaApiService, + record: ProviderCustomer, + kycCase: KycCase, + subAccountId: string, + requestHash: string +): Promise { + if (kycCase.submissionStatus !== "submitting" && kycCase.submissionStatus !== "unknown") { + return null; + } + if (kycCase.submissionRequestHash && kycCase.submissionRequestHash !== requestHash) { + throw new APIError({ + message: "The previous KYB submission used different input and must be reconciled first", + status: httpStatus.CONFLICT + }); + } + if (!kycCase.submissionStartedAt) { + throw new APIError({ message: "The previous KYB submission must be reconciled first", status: httpStatus.CONFLICT }); + } + + const { attempts } = await brlaApiService.getKycAttempts(subAccountId); + const startedAt = kycCase.submissionStartedAt.getTime(); + const matchingAttempts = attempts.filter( + attempt => attempt.levelName === "kyb-level-1" && new Date(attempt.createdAt).getTime() >= startedAt + ); + if (matchingAttempts.length > 1) { + throw new APIError({ message: "Multiple KYB attempts require manual reconciliation", status: httpStatus.CONFLICT }); + } + if (matchingAttempts.length === 1) { + const attemptId = matchingAttempts[0].id; + await sequelize.transaction(async transaction => { + await record.update( + { lastFailureReasons: [], status: VerificationStatus.Pending, statusExternal: KycAttemptStatus.PENDING }, + { transaction } + ); + await kycCase.update( + { + failureReasons: [], + providerCaseId: attemptId, + status: VerificationStatus.Pending, + statusExternal: KycAttemptStatus.PENDING, + submissionStatus: "submitted", + submittedAt: kycCase.submissionStartedAt + }, + { transaction } + ); + }); + kycCase.set({ + providerCaseId: attemptId, + statusExternal: KycAttemptStatus.PENDING, + submissionStatus: "submitted" + }); + return attemptId; + } + + throw new APIError({ + message: "No matching KYB attempt was found; manual reconciliation is required before retrying", + status: httpStatus.CONFLICT + }); +} + +export async function claimAveniaKybSubmission(kycCase: KycCase, requestHash: string): Promise { + const [claimed] = await KycCase.update( + { submissionRequestHash: requestHash, submissionStartedAt: new Date(), submissionStatus: "submitting" }, + { + where: { + id: kycCase.id, + providerCaseId: kycCase.providerCaseId, + submissionStatus: kycCase.submissionStatus + } + } + ); + if (claimed !== 1) { + throw new APIError({ message: "A KYB submission is already in progress", status: httpStatus.CONFLICT }); + } +} + +export const AVENIA_IDENTITY_DOCUMENT_TYPES = [ + AveniaDocumentType.ID, + AveniaDocumentType.DRIVERS_LICENSE, + AveniaDocumentType.PASSPORT, + AveniaDocumentType.RESIDENCE_PERMIT +]; diff --git a/apps/api/src/database/kyb-submission-migration.test.ts b/apps/api/src/database/kyb-submission-migration.test.ts new file mode 100644 index 000000000..9eadc166c --- /dev/null +++ b/apps/api/src/database/kyb-submission-migration.test.ts @@ -0,0 +1,16 @@ +import { expect, mock, test } from "bun:test"; +import type { QueryInterface } from "sequelize"; +import { up } from "./migrations/061-add-kyb-submission-state"; + +test("fails before schema changes when duplicate Avenia cases exist", async () => { + const addColumn = mock(async () => undefined); + const queryInterface = { + addColumn, + sequelize: { + query: mock(async () => [[{ provider_customer_id: "customer-1" }], undefined]) + } + } as unknown as QueryInterface; + + await expect(up(queryInterface)).rejects.toThrow("duplicate rows exist"); + expect(addColumn).not.toHaveBeenCalled(); +}); diff --git a/apps/api/src/database/migrations/061-add-kyb-submission-state.ts b/apps/api/src/database/migrations/061-add-kyb-submission-state.ts new file mode 100644 index 000000000..261400c0e --- /dev/null +++ b/apps/api/src/database/migrations/061-add-kyb-submission-state.ts @@ -0,0 +1,51 @@ +import { DataTypes, QueryInterface } from "sequelize"; + +export async function up(queryInterface: QueryInterface): Promise { + const [duplicateCases] = await queryInterface.sequelize.query( + `SELECT provider_customer_id + FROM kyc_cases + WHERE provider = 'avenia' AND provider_customer_id IS NOT NULL + GROUP BY provider_customer_id + HAVING COUNT(*) > 1 + LIMIT 1` + ); + if (Array.isArray(duplicateCases) && duplicateCases.length > 0) { + throw new Error("Cannot enforce one Avenia KYC case per provider customer while duplicate rows exist"); + } + + await queryInterface.addColumn("kyc_cases", "submission_status", { + allowNull: false, + defaultValue: "not_started", + type: DataTypes.STRING(16) + }); + await queryInterface.addColumn("kyc_cases", "submission_request_hash", { + allowNull: true, + type: DataTypes.STRING(64) + }); + await queryInterface.addColumn("kyc_cases", "submission_started_at", { + allowNull: true, + type: DataTypes.DATE + }); + await queryInterface.sequelize.query( + "UPDATE kyc_cases SET submission_status = 'submitted' WHERE provider_case_id IS NOT NULL" + ); + await queryInterface.addConstraint("kyc_cases", { + fields: ["submission_status"], + name: "kyc_cases_submission_status_check", + type: "check", + where: { submission_status: ["not_started", "submitting", "submitted", "unknown"] } + }); + await queryInterface.addIndex("kyc_cases", ["provider_customer_id"], { + name: "uniq_kyc_cases_avenia_provider_customer", + unique: true, + where: { provider: "avenia" } + }); +} + +export async function down(queryInterface: QueryInterface): Promise { + await queryInterface.removeIndex("kyc_cases", "uniq_kyc_cases_avenia_provider_customer"); + await queryInterface.removeConstraint("kyc_cases", "kyc_cases_submission_status_check"); + await queryInterface.removeColumn("kyc_cases", "submission_started_at"); + await queryInterface.removeColumn("kyc_cases", "submission_request_hash"); + await queryInterface.removeColumn("kyc_cases", "submission_status"); +} diff --git a/apps/api/src/models/kycCase.model.ts b/apps/api/src/models/kycCase.model.ts index 14f944ffe..43f1e0172 100644 --- a/apps/api/src/models/kycCase.model.ts +++ b/apps/api/src/models/kycCase.model.ts @@ -3,6 +3,7 @@ import sequelize from "../config/database"; import type { ProviderName, VerificationStatus } from "./providerCustomer.model"; export type KycCaseType = "kyc" | "kyb"; +export type KycSubmissionStatus = "not_started" | "submitting" | "submitted" | "unknown"; // Unified KYC/KYB verification attempts, independent of the provider account row. // Replaces the dead kyc_level_2 table (no data conversion — it had no readers). @@ -17,6 +18,9 @@ export interface KycCaseAttributes { statusExternal: string | null; providerCaseId: string | null; failureReasons: string[] | null; + submissionStatus: KycSubmissionStatus; + submissionRequestHash: string | null; + submissionStartedAt: Date | null; submittedAt: Date | null; approvedAt: Date | null; rejectedAt: Date | null; @@ -33,6 +37,9 @@ type KycCaseCreationAttributes = Optional< | "statusExternal" | "providerCaseId" | "failureReasons" + | "submissionStatus" + | "submissionRequestHash" + | "submissionStartedAt" | "submittedAt" | "approvedAt" | "rejectedAt" @@ -51,6 +58,9 @@ class KycCase extends Model implem declare statusExternal: string | null; declare providerCaseId: string | null; declare failureReasons: string[] | null; + declare submissionStatus: KycSubmissionStatus; + declare submissionRequestHash: string | null; + declare submissionStartedAt: Date | null; declare submittedAt: Date | null; declare approvedAt: Date | null; declare rejectedAt: Date | null; @@ -131,6 +141,23 @@ KycCase.init( field: "status_external", type: DataTypes.STRING(255) }, + submissionRequestHash: { + allowNull: true, + field: "submission_request_hash", + type: DataTypes.STRING(64) + }, + submissionStartedAt: { + allowNull: true, + field: "submission_started_at", + type: DataTypes.DATE + }, + submissionStatus: { + allowNull: false, + defaultValue: "not_started", + field: "submission_status", + type: DataTypes.STRING(16), + validate: { isIn: [["not_started", "submitting", "submitted", "unknown"]] } + }, submittedAt: { allowNull: true, field: "submitted_at", @@ -157,6 +184,12 @@ KycCase.init( { fields: ["provider_customer_id"], name: "idx_kyc_cases_provider_customer_id" + }, + { + fields: ["provider_customer_id"], + name: "uniq_kyc_cases_avenia_provider_customer", + unique: true, + where: { provider: "avenia" } } ], modelName: "KycCase", diff --git a/apps/api/src/tests/contracts/avenia.contract.test.ts b/apps/api/src/tests/contracts/avenia.contract.test.ts index 6ee19da5d..0adbaf96c 100644 --- a/apps/api/src/tests/contracts/avenia.contract.test.ts +++ b/apps/api/src/tests/contracts/avenia.contract.test.ts @@ -13,6 +13,9 @@ * live would move BRLA balance, and reading one needs the id of a real payout. * `createOnchainSwapQuote`/`createOnchainSwapTicket`/`getMainAccountBalance`/ * `getAveniaSwapTicket` have no production consumers and are deliberately uncovered. + * + * TODO: Add sandbox contract coverage for every consumed Avenia KYC/KYB operation and + * complete flow, including documents, UBOs, API submissions, attempts, and status polling. */ import { describe, expect, test } from "bun:test"; import { diff --git a/docs/security-spec/05-integrations/brla.md b/docs/security-spec/05-integrations/brla.md index c365cdbba..e21e30dbd 100644 --- a/docs/security-spec/05-integrations/brla.md +++ b/docs/security-spec/05-integrations/brla.md @@ -82,11 +82,13 @@ The invariant `transferAmount ≥ payoutAmount` must hold (transfer covers payou 18. **`/v1/brla/createSubaccount` MUST require an authenticated principal and use only canonical identity** — The route uses `requirePartnerOrUserAuth()` and the controller requires an effective user. Bare partner keys and anonymous callers receive `400`; the Avenia API is not called and no `provider_customers` row is created. Existing-tax-ID conflict and reuse decisions inspect only canonical Avenia `provider_customers` ownership. The controller does not query or adopt rows from `tax_ids`. 19. **BRL quote creation MUST remain anonymous-eligible while register/start remain user-gated** — `POST /v1/quotes` and `POST /v1/quotes/best` accept BRL corridors from anonymous callers and partner-key callers (with or without a `userId` binding). The Avenia `createPayInQuote` calls used by the BRL engines do not require a user-bound principal. The actual Avenia subaccount/taxId resolution still happens server-side at register time via `resolveAveniaAccountForRamp(effectiveUserId, additionalData.taxId)`. `POST /v1/ramp/register` requires Supabase or secret-key credentials, and `RampService.registerRamp` rejects provider-backed ramps without an effective user with `400 Invalid quote`. **An anonymous BRL quote may be claimed by an authenticated caller** (the normal web-app funnel: quote before login, register after) — claiming is not an escalation because the anonymous quote carries no owner and the Avenia identity is derived from the claimer's own KYC records, never from the quote or request body. 20. **`brlaPayoutOnBase` MUST verify the ephemeral's BRLA balance before the first broadcast of the presigned transfer** — The presigned payout is single-use (its nonce is consumed even on revert), so the handler calls `ensurePresignedTransferFunded` before `sendRawTransaction`: sender/token/amount are decoded from the signed raw tx and the ephemeral balance is polled (3-minute timeout); a shortfall raises a recoverable error instead of burning the nonce. The Avenia-side balance poll (invariant 4) runs after this on-chain transfer and does not replace it. See `03-ramp-engine/ramp-phase-flows.md` invariant 12. -21. **Avenia company KYB completion MUST be provider-confirmed and ownership-bound** — `POST /v1/brla/kyb/new-level-1/web-sdk` stores the returned Avenia `attemptId` as the owned business `kyc_cases.provider_case_id`. `GET /v1/brla/kyb/attempt-status` accepts only a case owned by the effective user, queries that exact attempt, persists normalized status on both the case and provider customer, and returns only `status`, optional `result`, and optional normalized `failureReason`. Client-side events cannot assert completion: only provider `COMPLETED` plus `APPROVED` may complete onboarding; `REJECTED`, `EXPIRED`, `PENDING`, and `PROCESSING` must not pass the parent verification gate. -22. **A KYB attempt Avenia has not started processing MUST stay canonical `pending`, never `in_review`** — Company subaccount creation and KYB link initiation record `pending` (the attempt is `PENDING` at Avenia until the user completes the hosted steps); `in_review` is set only once Avenia reports `PROCESSING`. While the bound attempt's stored external status is still `PENDING`, re-initiation by the owner is allowed and rebinds the case to the fresh `attemptId` (the hosted URLs are never stored, so this is the only resume path); the `409` conflict applies once the attempt is `PROCESSING` or decided. Because the stored status can lag, re-initiation additionally probes the live attempt and refuses (`409`) when Avenia reports it processing or approved — a rejected decision stays re-initiable, and a failing probe falls back to allowing the resume. This cannot be used to bypass verification: a fresh attempt restarts at `PENDING` and invariant 21's completion gate is unchanged. To support form-less resume, `GET /v1/onboarding/status` exposes `taxReference` (the CNPJ) for **business** rows only — the response is already scoped to the caller's own entities, and individual CPFs remain unexposed. +21. **Avenia company KYB completion MUST be provider-confirmed and ownership-bound** — `POST /v1/brla/kyb/new-level-1/web-sdk` stores the returned Avenia `attemptId` as the owned business `kyc_cases.provider_case_id`. `GET /v1/brla/kyb/attempt-status` accepts only a case owned by the effective user, queries that exact attempt, persists normalized status on both the case and provider customer, and returns only `status`, `retryable`, optional `result`, and optional normalized `failureReason`. Client-side events cannot assert completion: only provider `COMPLETED` plus `APPROVED` may complete onboarding; `REJECTED`, `EXPIRED`, `PENDING`, and `PROCESSING` must not pass the parent verification gate. +22. **A KYB attempt Avenia has not started processing MUST stay canonical `pending`, never `in_review`** — Company subaccount creation and KYB link initiation record `pending` (the attempt is `PENDING` at Avenia until the user completes the hosted steps); `in_review` is set only once Avenia reports `PROCESSING`. While the bound attempt's stored external status is still `PENDING`, re-initiation by the owner is allowed and rebinds the case to the fresh `attemptId` (the hosted URLs are never stored, so this is the only resume path); the `409` conflict applies once the attempt is `PROCESSING` or decided. Because the stored status can lag, re-initiation additionally probes the live attempt and refuses (`409`) when Avenia reports it processing or approved — a rejected decision stays re-initiable, while a failed live probe fails closed rather than risking a duplicate attempt. This cannot be used to bypass verification: a fresh attempt restarts at `PENDING` and invariant 21's completion gate is unchanged. To support form-less resume, `GET /v1/onboarding/status` exposes `taxReference` (the CNPJ) for **business** rows only — the response is already scoped to the caller's own entities, and individual CPFs remain unexposed. 23. **BRL Base destination variants MUST use token-specific static topology** — Base USDC MUST omit Squid entirely. Other configured non-BRLA Base outputs MUST execute exactly one same-chain `squidRouterSwap` phase before `destinationTransfer`; transaction preparation MUST use the Base builder, omit `squidRouterPay` and backup transactions, and allocate `destinationTransfer` at the nonce immediately after the Squid swap. BRLA remains the direct bypass in invariant 14. 24. **Dashboard BRL BUY confirmation MUST not bypass PIX verification** — The dashboard displays the server-generated `depositQrCode`, keeps the ramp unstarted, and calls `/ramp/start` only after the user confirms submitting PIX. That click is not proof of settlement; `brlaOnrampMint` must still verify the Avenia/Base balance before advancing. 25. **Unified BRL limit reads MUST use the authenticated user's provider account** — `POST /v1/limits` MUST derive the Avenia subaccount through `resolveAveniaAccountForUser`; it MUST NOT accept a caller-supplied tax ID or subaccount. BRL `max`, `used`, year, and month are mapped directly from Avenia's BRL fiat-in/fiat-out limit row. Tax IDs and provider subaccount IDs are never returned. +26. **Avenia API KYB mutations MUST be ownership-bound and document-gated** — `/v1/brla/kyb/documents`, `/v1/brla/kyb/ubos`, and `/v1/brla/kyb/new-level-1/api` accept Supabase sessions or profile-bound secret API credentials. Every operation resolves the supplied subaccount to an Avenia business `provider_customers` row owned by one of the effective profile's customer entities before calling Avenia. UBO identification/selfie documents and final-submission corporate documents are fetched from that same subaccount and must be provider-ready with the expected document type. Binary bytes are uploaded directly to Avenia's short-lived pre-signed URL; Vortex does not proxy or persist them. +27. **Avenia API KYB resubmission MUST follow the provider's retry decision and preserve ambiguous outcomes** — A successful API submission binds the returned attempt ID to the existing KYB case, sets both canonical rows to `pending`, records external `PENDING`, and clears prior rejection fields. A bound attempt may be replaced only after Avenia returns `COMPLETED`, `REJECTED`, and `retryable: true`; pending, processing, approved, expired, and non-retryable rejected attempts return `409`. A partial unique index enforces one Avenia case per provider account, and the case's separate submission-operation state serializes API and hosted submissions. A transport, process, or persistence failure whose side effect cannot be disproved leaves that state `submitting` or `unknown`; the next identical request lists attempts created since the claim and binds a unique matching attempt instead of replaying it. Absence from that list does not prove the original side effect cannot arrive later, so automatic replay remains blocked pending manual reconciliation. Ambiguous submission failure MUST NOT be converted into a compliance rejection. Hosted initiation cannot replace an API-originated attempt. Status persistence uses monotonic guards and conditionally updates the still-bound provider attempt so stale or late polls cannot overwrite current state. ## Threat Vectors & Mitigations @@ -107,6 +109,7 @@ The invariant `transferAmount ≥ payoutAmount` must hold (transfer covers payou | **Claiming an anonymous BRL estimate at register time** | Attacker mints an anonymous BRL quote, then presents a Supabase token (or a different user's linked secret API key) at register time to bind the resulting ramp to a different user's Avenia provider customer | An authenticated caller may claim an ownerless quote; `RampService.registerRamp` rejects only when both `quote.userId` and `request.userId` are non-null and differ. Provider identity is derived from the authenticated caller's canonical Avenia account, so the quote cannot select another user's provider customer. | | **Destination-token decimal under-delivery** | A BRL on-ramp targets an 18-decimal token such as BSC USDT, but the quote output is truncated to 6 decimals before `destinationTransfer` raw amount construction. | On-ramp finalization uses destination-token decimals for BRL EVM outputs; Squid metadata preserves destination raw output from `route.estimate.toAmount`. | | **Company KYB status bypass or cross-user attempt lookup** | A browser asserts that hosted verification finished, or probes another user's Avenia attempt ID and receives provider submission metadata. | Initiation binds the attempt to the authenticated user's KYB case; status lookup checks that binding before the provider call, minimizes its response, and the client/parent accept only provider-confirmed `COMPLETED` + `APPROVED`. | +| **Duplicate API KYB attempt after timeout** | A caller retries final KYB submission after Avenia accepted the first call but its response was lost, creating parallel provider attempts. | The case is claimed before the provider call. Ambiguous failures persist `submission_status = 'unknown'` and return an upstream error; an identical retry reconciles Avenia's attempt list before it can replay the submission. Provider-confirmed retryable rejection is the only normal resubmission path. | ## Audit Checklist @@ -129,6 +132,7 @@ The invariant `transferAmount ≥ payoutAmount` must hold (transfer covers payou - [x] PIX deposit details released to user only after presign validation. **PASS** — gated by `ephemeralPresignChecksPass` (see `transaction-validation.md`). - [ ] Avenia interactions logged for reconciliation (amounts, not credentials). **PARTIAL** — info logs include amounts; no formal reconciliation log with structured fields. - [x] **FINDING F-064 (MEDIUM)**: BRLA KYC callback endpoint requires authentication. **PASS (FIXED)** — `/kyc/record-attempt` uses `requireAuth`. +- [x] Avenia API KYB operations enforce effective-profile ownership, company account type, document readiness/type, and provider-confirmed retryability before final submission. - [x] BRL→BRLA-on-Base on-ramps emit only provider mint, funding, and `destinationTransfer` — no Nabla, fee distribution, Squid, final settlement, or Base cleanup transaction. **PASS** — `phases/blocks/flows/brl-onramp-base-direct.ts`. - [x] The BRL→BRLA direct flow omits Squid and final settlement rather than relying on executor short-circuits. **PASS** — `phases/blocks/flows/brl-onramp-base-direct.ts`. - [x] BRL→EVM destination-token precision preserved. **PASS** — block flow simulation preserves Squid destination raw output and destination-token decimals. diff --git a/packages/shared/src/endpoints/brla.endpoints.ts b/packages/shared/src/endpoints/brla.endpoints.ts index fa7b1d0cd..ef41d52d3 100644 --- a/packages/shared/src/endpoints/brla.endpoints.ts +++ b/packages/shared/src/endpoints/brla.endpoints.ts @@ -56,7 +56,7 @@ export interface BrlaGetKycStatusResponse { type: "KYC"; level: string; status: KycAttemptStatus; - result: KycAttemptResult; + result?: KycAttemptResult; failureReason?: KycFailureReason; } diff --git a/packages/shared/src/services/brla/brlaApiService.test.ts b/packages/shared/src/services/brla/brlaApiService.test.ts index 521cf69ce..73e7345cf 100644 --- a/packages/shared/src/services/brla/brlaApiService.test.ts +++ b/packages/shared/src/services/brla/brlaApiService.test.ts @@ -6,7 +6,32 @@ import { AveniaDocumentType, type AveniaKybLevel1Payload, type AveniaUboPayload function serviceWithMockedRequest() { const service = Object.create(BrlaApiService.prototype) as BrlaApiService; - const sendRequest = mock(async () => ({ id: "provider-id" })); + const sendRequest = mock(async (endpoint: Endpoint) => { + if (endpoint === Endpoint.GetDocument) { + return { + document: { + documentType: AveniaDocumentType.PASSPORT, + id: "document/1", + ready: true, + uploadStatusFront: "PROCESSED" + } + }; + } + if (endpoint === Endpoint.GetKybAttempt) { + return { + attempt: { + createdAt: "2026-08-06T12:00:00.000Z", + id: "attempt-1", + levelName: "kyb-level-1", + resultMessage: "", + retryable: false, + status: "PENDING", + updatedAt: "2026-08-06T12:00:00.000Z" + } + }; + } + return { id: "provider-id" }; + }); Object.assign(service, { sendRequest }); return { sendRequest, service }; } @@ -104,4 +129,11 @@ describe("BrlaApiService Avenia KYB Level 1 mappings", () => { expect(AveniaDocumentType.COMPANY_TAX_IDENTIFICATION_DOCUMENT).toBe("COMPANY-TAX-IDENTIFICATION-DOCUMENT"); expect(AveniaDocumentType.RESIDENCE_PERMIT).toBe("RESIDENCE-PERMIT"); }); + + test("rejects malformed successful provider responses", async () => { + const service = Object.create(BrlaApiService.prototype) as BrlaApiService; + Object.assign(service, { sendRequest: mock(async () => ({})) }); + + await expect(service.submitKybLevel1(kyb, "sub-1")).rejects.toThrow(); + }); }); diff --git a/packages/shared/src/services/brla/brlaApiService.ts b/packages/shared/src/services/brla/brlaApiService.ts index dcb35ed00..7453905cb 100644 --- a/packages/shared/src/services/brla/brlaApiService.ts +++ b/packages/shared/src/services/brla/brlaApiService.ts @@ -3,6 +3,15 @@ import { BRLA_API_KEY, BRLA_BASE_URL, BRLA_PRIVATE_KEY, DocumentUploadRequest, D import logger from "../../logger"; import { ProviderHttpError } from "../providerHttpError"; import { Endpoint, EndpointMapping, Endpoints, Methods } from "./mappings"; +import { + aveniaDocumentResponseSchema, + aveniaDocumentUploadResponseSchema, + aveniaKybAttemptStatusSchema, + aveniaKybLevel1ResponseSchema, + aveniaKycAttemptsSchema, + aveniaLevel1ResponseSchema, + aveniaUboResponseSchema +} from "./schemas"; import { AccountLimitsResponse, AveniaAccountBalanceResponse, @@ -227,7 +236,7 @@ export class BrlaApiService { isDoubleSided }; const query = `subAccountId=${encodeURIComponent(subAccountId)}`; - return await this.sendRequest(Endpoint.Documents, "POST", query, payload); + return aveniaDocumentUploadResponseSchema.parse(await this.sendRequest(Endpoint.Documents, "POST", query, payload)); } public async getUploadedDocuments(subAccountId: string): Promise { @@ -237,12 +246,14 @@ export class BrlaApiService { public async getUploadedDocument(documentId: string, subAccountId: string): Promise { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; - return await this.sendRequest(Endpoint.GetDocument, "GET", query, undefined, documentId); + return aveniaDocumentResponseSchema.parse( + await this.sendRequest(Endpoint.GetDocument, "GET", query, undefined, documentId) + ); } public async createUbo(payload: AveniaUboPayload, subAccountId: string): Promise { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; - return await this.sendRequest(Endpoint.Ubos, "POST", query, payload); + return aveniaUboResponseSchema.parse(await this.sendRequest(Endpoint.Ubos, "POST", query, payload)); } public async createPayInQuote( @@ -388,17 +399,17 @@ export class BrlaApiService { public async submitKycLevel1(payload: KycLevel1Payload): Promise { const query = `subAccountId=${encodeURIComponent(payload.subAccountId)}`; - return await this.sendRequest(Endpoint.Level1Api, "POST", query, payload); + return aveniaLevel1ResponseSchema.parse(await this.sendRequest(Endpoint.Level1Api, "POST", query, payload)); } public async submitKybLevel1(payload: AveniaKybLevel1Payload, subAccountId: string): Promise { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; - return await this.sendRequest(Endpoint.Level1Api, "POST", query, payload); + return aveniaLevel1ResponseSchema.parse(await this.sendRequest(Endpoint.Level1Api, "POST", query, payload)); } public async getKycAttempts(subAccountId: string): Promise { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; - return await this.sendRequest(Endpoint.GetKycAttempt, "GET", query, undefined); + return aveniaKycAttemptsSchema.parse(await this.sendRequest(Endpoint.GetKycAttempt, "GET", query, undefined)); } /** @@ -410,7 +421,7 @@ export class BrlaApiService { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; // Avenia requires the field to be present but ignores its value for the Web SDK flow. const payload = { redirectUrl: "" }; - return await this.sendRequest(Endpoint.KybLevel1WebSdk, "POST", query, payload); + return aveniaKybLevel1ResponseSchema.parse(await this.sendRequest(Endpoint.KybLevel1WebSdk, "POST", query, payload)); } /** @@ -420,7 +431,9 @@ export class BrlaApiService { */ public async getKybAttemptStatus(attemptId: string, subAccountId?: string): Promise { const query = subAccountId ? `subAccountId=${encodeURIComponent(subAccountId)}` : undefined; - return await this.sendRequest(Endpoint.GetKybAttempt, "GET", query, undefined, attemptId); + return aveniaKybAttemptStatusSchema.parse( + await this.sendRequest(Endpoint.GetKybAttempt, "GET", query, undefined, attemptId) + ); } public async getAccountBalance(subAccountId: string): Promise { diff --git a/packages/shared/src/services/brla/schemas.ts b/packages/shared/src/services/brla/schemas.ts index cebb07c28..8b0fc5bfc 100644 --- a/packages/shared/src/services/brla/schemas.ts +++ b/packages/shared/src/services/brla/schemas.ts @@ -14,6 +14,10 @@ import { AveniaSubaccountWallet, AveniaTicketStatus, AveniaUboResponse, + DocumentUploadResponse, + GetKycAttemptResponse, + KybLevel1Response, + KycAttempt, KycAttemptResult, KycAttemptStatus, KycLevel1Response, @@ -147,14 +151,28 @@ export const aveniaAccountInfoSchema = z.looseObject({ /** A document after Avenia has processed the bytes uploaded to its pre-signed URL. */ export const aveniaDocumentResponseSchema = z.looseObject({ document: z.looseObject({ + createdAt: z.string().min(1).optional(), documentType: z.enum(AveniaDocumentType), id: z.string().min(1), ready: z.boolean(), - uploadStatusFront: z.string().min(1) + updatedAt: z.string().min(1).optional(), + uploadErrorBack: z.string().optional(), + uploadErrorFront: z.string().optional(), + uploadStatusBack: z.string().optional(), + uploadStatusFront: z.string().min(1), + uploadURLBack: z.string().optional(), + uploadURLFront: z.string().optional() }) -}) satisfies z.ZodType<{ - document: Pick; -}>; +}) satisfies z.ZodType<{ document: AveniaDocument }>; + +/** The upload target returned when an Avenia document record is created. */ +export const aveniaDocumentUploadResponseSchema = z.looseObject({ + id: z.string().min(1), + livenessUrl: z.string().min(1).optional(), + uploadURLBack: z.string().optional(), + uploadURLFront: z.string().min(1), + validateLivenessToken: z.string().min(1).optional() +}) satisfies z.ZodType; /** The identifier returned by UBO creation. */ export const aveniaUboResponseSchema = z.looseObject({ @@ -166,16 +184,31 @@ export const aveniaLevel1ResponseSchema = z.looseObject({ id: z.string().min(1) }) satisfies z.ZodType; +/** The hosted company KYB attempt and continuation URLs. */ +export const aveniaKybLevel1ResponseSchema = z.looseObject({ + attemptId: z.string().min(1), + authorizedRepresentativeUrl: z.string().min(1), + basicCompanyDataUrl: z.string().min(1) +}) satisfies z.ZodType; + +const aveniaAttemptSchema = z.looseObject({ + createdAt: z.string().datetime({ offset: true }), + id: z.string().min(1), + levelName: z.string().min(1), + result: z.enum(KycAttemptResult).optional(), + resultMessage: z.string(), + retryable: z.boolean(), + status: z.enum(KycAttemptStatus), + submissionData: z.record(z.string(), z.unknown()).optional(), + updatedAt: z.string().datetime({ offset: true }) +}) satisfies z.ZodType; + +/** Paginated attempt history used to reconcile an ambiguous submission. */ +export const aveniaKycAttemptsSchema = z.looseObject({ + attempts: z.array(aveniaAttemptSchema) +}) satisfies z.ZodType; + /** A KYB attempt returned by GET /v2/kyc/attempts/{attemptId}. */ export const aveniaKybAttemptStatusSchema = z.looseObject({ - attempt: z.looseObject({ - createdAt: z.string().min(1), - id: z.string().min(1), - levelName: z.string().min(1), - result: z.enum(KycAttemptResult).optional(), - resultMessage: z.string(), - retryable: z.boolean(), - status: z.enum(KycAttemptStatus), - updatedAt: z.string().min(1) - }) + attempt: aveniaAttemptSchema }) satisfies z.ZodType; diff --git a/packages/shared/src/services/brla/types.ts b/packages/shared/src/services/brla/types.ts index 0554be9f9..4309f2643 100644 --- a/packages/shared/src/services/brla/types.ts +++ b/packages/shared/src/services/brla/types.ts @@ -473,6 +473,7 @@ export interface KybLevel1Response { export interface KybAttemptStatusResponse { failureReason?: string; result?: KycAttemptResult; + retryable?: boolean; status: KycAttemptStatus; } @@ -546,10 +547,10 @@ export enum KycAttemptResult { export interface KycAttempt { id: string; - levelName: "level-1"; - submissionData: unknown; + levelName: string; + submissionData?: unknown; status: KycAttemptStatus; - result: KycAttemptResult; + result?: KycAttemptResult; resultMessage: string; retryable: boolean; createdAt: string; From 3de61fc484a8d1d7b278b1d7fb87d92dcdeced49 Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 11 Aug 2026 11:49:33 -0300 Subject: [PATCH 8/9] feat(api): publish onboarding requirements discovery --- .../api/controllers/alfredpay.controller.ts | 4 +- ...onboarding-requirements.controller.test.ts | 70 + .../api/controllers/onboarding.controller.ts | 51 +- .../v1/onboarding-requirements.route.test.ts | 28 + .../api/src/api/routes/v1/onboarding.route.ts | 4 +- .../src/tests/http-surface.invariants.test.ts | 15 + docs/README.md | 2 +- docs/api/openapi/vortex.openapi.d.ts | 5262 ++++++++++++----- docs/api/openapi/vortex.openapi.json | 1689 +++++- docs/api/pages/10-sandbox.md | 5 +- docs/api/scripts/check-openapi.ts | 63 + docs/api/wire-contract.snapshot.md | 184 +- docs/proposal-api-driven-kyc-kyb.md | 291 + docs/proposal-unified-kyc-kyb-api.md | 160 - .../05-integrations/alfredpay.md | 2 +- .../07-operations/api-surface.md | 2 + .../src/endpoints/alfredpay.endpoints.ts | 2 +- .../shared/src/endpoints/brla.endpoints.ts | 2 +- packages/shared/src/endpoints/index.ts | 1 + .../onboarding-requirements.endpoints.test.ts | 53 + .../onboarding-requirements.endpoints.ts | 674 +++ 21 files changed, 6797 insertions(+), 1767 deletions(-) create mode 100644 apps/api/src/api/controllers/onboarding-requirements.controller.test.ts create mode 100644 apps/api/src/api/routes/v1/onboarding-requirements.route.test.ts create mode 100644 docs/proposal-api-driven-kyc-kyb.md delete mode 100644 docs/proposal-unified-kyc-kyb-api.md create mode 100644 packages/shared/src/endpoints/onboarding-requirements.endpoints.test.ts create mode 100644 packages/shared/src/endpoints/onboarding-requirements.endpoints.ts diff --git a/apps/api/src/api/controllers/alfredpay.controller.ts b/apps/api/src/api/controllers/alfredpay.controller.ts index 6d00c06a0..cd9c3f575 100644 --- a/apps/api/src/api/controllers/alfredpay.controller.ts +++ b/apps/api/src/api/controllers/alfredpay.controller.ts @@ -232,10 +232,10 @@ export class AlfredpayController { static async alfredpayStatus(req: Request, res: Response) { try { - const { country } = req.query as unknown as AlfredpayStatusRequest; + const { country, type } = req.query as unknown as AlfredpayStatusRequest; const userId = AlfredpayController.getRequiredUserId(req); - const alfredPayCustomer = await findAlfredpayCustomer(userId, country as AlfredPayCountry); + const alfredPayCustomer = await findAlfredpayCustomer(userId, country as AlfredPayCountry, type); if (!alfredPayCustomer) { return res.status(404).json({ error: "Alfredpay customer not found" }); diff --git a/apps/api/src/api/controllers/onboarding-requirements.controller.test.ts b/apps/api/src/api/controllers/onboarding-requirements.controller.test.ts new file mode 100644 index 000000000..51dd5fdc5 --- /dev/null +++ b/apps/api/src/api/controllers/onboarding-requirements.controller.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, mock } from "bun:test"; +import type { Request, Response } from "express"; +import { getOnboardingRequirements } from "./onboarding.controller"; + +function createResponse() { + const response = { + body: undefined as unknown, + statusCode: 200, + json: mock((body: unknown) => { + response.body = body; + return response; + }), + status: mock((statusCode: number) => { + response.statusCode = statusCode; + return response; + }) + }; + return response; +} + +describe("getOnboardingRequirements", () => { + it("returns public requirements case-insensitively", () => { + const response = createResponse(); + + getOnboardingRequirements( + { query: { country: "br", customerType: "BUSINESS" } } as unknown as Request, + response as unknown as Response + ); + + expect(response.statusCode).toBe(200); + expect(response.body).toMatchObject({ + country: "BR", + customerType: "business", + flow: "avenia-br-business-level-1-api-kyb", + provider: "avenia" + }); + }); + + it("rejects incomplete queries", () => { + const response = createResponse(); + + getOnboardingRequirements({ query: { country: "BR" } } as unknown as Request, response as unknown as Response); + + expect(response.statusCode).toBe(400); + expect(response.body).toMatchObject({ error: { code: "INVALID_ONBOARDING_REQUIREMENTS_QUERY" } }); + }); + + it("does not advertise unsupported provider flows", () => { + const response = createResponse(); + + getOnboardingRequirements( + { query: { country: "AR", customerType: "business" } } as unknown as Request, + response as unknown as Response + ); + + expect(response.statusCode).toBe(404); + expect(response.body).toMatchObject({ error: { code: "ONBOARDING_REQUIREMENTS_NOT_FOUND" } }); + }); + + it("leaves Monerium outside this discovery proposal", () => { + const response = createResponse(); + + getOnboardingRequirements( + { query: { country: "EU", customerType: "individual" } } as unknown as Request, + response as unknown as Response + ); + + expect(response.statusCode).toBe(404); + }); +}); diff --git a/apps/api/src/api/controllers/onboarding.controller.ts b/apps/api/src/api/controllers/onboarding.controller.ts index 6a7a793d2..95efda035 100644 --- a/apps/api/src/api/controllers/onboarding.controller.ts +++ b/apps/api/src/api/controllers/onboarding.controller.ts @@ -1,4 +1,11 @@ -import { BrlaApiService, KycAttemptResult, KycAttemptStatus } from "@vortexfi/shared"; +import { + BrlaApiService, + getOnboardingRequirements as findOnboardingRequirements, + KycAttemptResult, + KycAttemptStatus, + ONBOARDING_REQUIREMENTS, + OnboardingRequirementsCountry +} from "@vortexfi/shared"; import { Request, Response } from "express"; import httpStatus from "http-status"; import logger from "../../config/logger"; @@ -20,6 +27,48 @@ import { getMoneriumStatus, MONERIUM_REAUTHENTICATION_REQUIRED } from "../servic const PROVIDER_REFRESH_TTL_MS = 60_000; const lastProviderRefreshAt = new Map(); +/** GET /v1/onboarding/requirements - public metadata for an existing provider-specific flow. */ +export function getOnboardingRequirements(req: Request, res: Response): void { + const country = typeof req.query.country === "string" ? req.query.country.toUpperCase() : ""; + const customerType = typeof req.query.customerType === "string" ? req.query.customerType.toLowerCase() : ""; + + if (!country || (customerType !== "individual" && customerType !== "business")) { + res.status(httpStatus.BAD_REQUEST).json({ + error: { + code: "INVALID_ONBOARDING_REQUIREMENTS_QUERY", + message: "country and customerType (individual or business) are required", + status: httpStatus.BAD_REQUEST + } + }); + return; + } + + if (!(country in ONBOARDING_REQUIREMENTS)) { + res.status(httpStatus.NOT_FOUND).json({ + error: { + code: "ONBOARDING_REQUIREMENTS_NOT_FOUND", + message: `No API-driven onboarding requirements are published for ${country} ${customerType}`, + status: httpStatus.NOT_FOUND + } + }); + return; + } + + const requirements = findOnboardingRequirements(country as OnboardingRequirementsCountry, customerType); + if (!requirements) { + res.status(httpStatus.NOT_FOUND).json({ + error: { + code: "ONBOARDING_REQUIREMENTS_NOT_FOUND", + message: `No API-driven onboarding requirements are published for ${country} ${customerType}`, + status: httpStatus.NOT_FOUND + } + }); + return; + } + + res.status(httpStatus.OK).json(requirements); +} + function shouldRefreshProviderStatus(customerId: string): boolean { const now = Date.now(); const last = lastProviderRefreshAt.get(customerId); diff --git a/apps/api/src/api/routes/v1/onboarding-requirements.route.test.ts b/apps/api/src/api/routes/v1/onboarding-requirements.route.test.ts new file mode 100644 index 000000000..923da6162 --- /dev/null +++ b/apps/api/src/api/routes/v1/onboarding-requirements.route.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "bun:test"; +import express from "express"; +import type { AddressInfo } from "node:net"; +import onboardingRoutes from "./onboarding.route"; + +describe("GET /v1/onboarding/requirements", () => { + it("serves public discovery metadata without authentication", async () => { + const app = express(); + app.use("/v1/onboarding", onboardingRoutes); + const server = app.listen(0); + await new Promise(resolve => server.once("listening", resolve)); + + try { + const { port } = server.address() as AddressInfo; + const response = await fetch(`http://127.0.0.1:${port}/v1/onboarding/requirements?country=MX&customerType=business`); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + country: "MX", + customerType: "business", + flow: "alfredpay-mx-business-api-kyb", + provider: "alfredpay" + }); + } finally { + server.close(); + } + }); +}); diff --git a/apps/api/src/api/routes/v1/onboarding.route.ts b/apps/api/src/api/routes/v1/onboarding.route.ts index 2fc2ecf46..a38117896 100644 --- a/apps/api/src/api/routes/v1/onboarding.route.ts +++ b/apps/api/src/api/routes/v1/onboarding.route.ts @@ -1,11 +1,13 @@ import { Request, Response, Router } from "express"; -import { getOnboardingStatus, putActiveEntity } from "../../controllers/onboarding.controller"; +import { getOnboardingRequirements, getOnboardingStatus, putActiveEntity } from "../../controllers/onboarding.controller"; import { requirePartnerOrUserAuth } from "../../middlewares/dualAuth"; import { authorizeManagedProfile } from "../../middlewares/managedProfileAuth"; import { requireAuth } from "../../middlewares/supabaseAuth"; const router: Router = Router({ mergeParams: true }); +router.get("/requirements", getOnboardingRequirements); + /** * GET /v1/onboarding/status * Aggregated per-entity provider/KYC onboarding status for the authenticated profile. diff --git a/apps/api/src/tests/http-surface.invariants.test.ts b/apps/api/src/tests/http-surface.invariants.test.ts index 812c8dba0..6ff644e1b 100644 --- a/apps/api/src/tests/http-surface.invariants.test.ts +++ b/apps/api/src/tests/http-surface.invariants.test.ts @@ -370,6 +370,21 @@ describe("HTTP surface: auth flow, webhooks, history, public routes", () => { const buyIds = (buyMethods.body.paymentMethods as Array<{ id: string }>).map(method => method.id); expect(buyIds.sort()).toEqual(["ach", "pix", "spei"]); + const requirements = await requestJson("/v1/onboarding/requirements?country=BR&customerType=business"); + expect(requirements.status).toBe(200); + expect(requirements.body).toMatchObject({ + country: "BR", + customerType: "business", + flow: "avenia-br-business-level-1-api-kyb", + provider: "avenia" + }); + expect((requirements.body.steps as Array<{ operationId?: string }>).map(step => step.operationId).filter(Boolean)).toContain( + "submitAveniaKybLevel1Api" + ); + + const unsupportedRequirements = await requestJson("/v1/onboarding/requirements?country=AR&customerType=business"); + expect(unsupportedRequirements.status).toBe(404); + const mxnMethods = await requestJson("/v1/supported-payment-methods?fiat=MXN"); expect(mxnMethods.status).toBe(200); expect((mxnMethods.body.paymentMethods as Array<{ id: string }>).map(method => method.id)).toEqual(["spei"]); diff --git a/docs/README.md b/docs/README.md index 5c057c2e2..06316677d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ The smaller set of general project documents stays directly in `docs/`: | [`product-dashboard.md`](product-dashboard.md) | Current dashboard product scope and acknowledged gaps | | [`proposal-headless-profiles-and-pricing-plans.md`](proposal-headless-profiles-and-pricing-plans.md) | Active proposal for delegated management of headless customer profiles | | [`proposal-mcp-server.md`](proposal-mcp-server.md) | Active, non-authoritative discussion draft | -| [`proposal-unified-kyc-kyb-api.md`](proposal-unified-kyc-kyb-api.md) | Early proposal for API-driven, provider-neutral customer verification | +| [`proposal-api-driven-kyc-kyb.md`](proposal-api-driven-kyc-kyb.md) | Proposal for API-driven verification using preserved provider-specific workflows | The root [`README.md`](../README.md) is human onboarding, [`MAP.md`](../MAP.md) is repository wayfinding, and `CLAUDE.md` files contain instructions for coding agents. diff --git a/docs/api/openapi/vortex.openapi.d.ts b/docs/api/openapi/vortex.openapi.d.ts index b9859441e..09b328fdc 100644 --- a/docs/api/openapi/vortex.openapi.d.ts +++ b/docs/api/openapi/vortex.openapi.d.ts @@ -109,11 +109,9 @@ export interface paths { put?: never; /** * Create user or retry KYC - * @description `companyName`, `startDate` and `cnpj` are only required when taxIdType is `CNPJ` + * @description Creates the Avenia individual or company subaccount owned by the effective profile. `quoteId` and `sessionId` are optional provenance fields. A managed-profile manager supplies `X-Managed-Profile-Id` and must have the BR corridor. * - * `quoteId` is optional: pass it in the normal ramp flow, or omit it for the quote-less KYB deep link where business verification starts before any quote exists. - * - * **Auth:** uses `optionalAuth` — accepts a Supabase Bearer token if present but does not require one. + * **Auth:** Supabase Bearer or profile-bound secret API key. */ post: operations["createSubaccount"]; delete?: never; @@ -131,7 +129,9 @@ export interface paths { }; /** * Get user's KYC status - * @description **Auth:** requires `Authorization: Bearer `. + * @description Returns the latest Avenia KYC attempt status owned by the effective profile. + * + * **Auth:** Supabase Bearer or profile-bound secret API key. */ get: operations["fetchSubaccountKycStatus"]; put?: never; @@ -151,9 +151,9 @@ export interface paths { }; /** * Get selfie liveness URL - * @description Returns the Avenia selfie/liveness-check URL for the subaccount associated with this tax ID. + * @description Returns the Avenia selfie/liveness-check URL for the owned subaccount. * - * **Auth:** requires `Authorization: Bearer `. + * **Auth:** Supabase Bearer or profile-bound secret API key. */ get: operations["brlaGetSelfieLivenessUrl"]; put?: never; @@ -175,9 +175,9 @@ export interface paths { put?: never; /** * Get KYC document upload URLs - * @description Returns presigned upload URLs for the user's ID document and selfie. Only `ID` and `DRIVERS-LICENSE` are accepted for `documentType` (passport not supported here). + * @description Returns presigned upload URLs for the owned user's ID document and selfie. Only `ID` and `DRIVERS-LICENSE` are accepted. * - * **Auth:** uses `optionalAuth` — accepts a Supabase Bearer token if present but does not require one. + * **Auth:** Supabase Bearer or profile-bound secret API key. */ post: operations["brlaGetUploadUrls"]; delete?: never; @@ -195,9 +195,9 @@ export interface paths { }; /** * Get user information - * @description Fetches a user's subaccount information. The response contains only the EVM wallet address and KYC level. + * @description Fetches the effective profile's Avenia subaccount information. `taxId` is an optional ownership cross-check. * - * **Auth:** requires `Authorization: Bearer `. + * **Auth:** Supabase Bearer or profile-bound secret API key. */ get: operations["getBrlaUser"]; put?: never; @@ -217,7 +217,9 @@ export interface paths { }; /** * Get user's remaining transaction limits - * @description **Auth:** requires `Authorization: Bearer `. + * @description Returns the remaining Avenia limit for one ramp direction. `taxId` is an optional ownership cross-check. + * + * **Auth:** Supabase Bearer or profile-bound secret API key. */ get: operations["getBrlaUserRemainingLimit"]; put?: never; @@ -239,9 +241,9 @@ export interface paths { put?: never; /** * Submit KYC level 1 data - * @description Submits the user's KYC level 1 payload to Avenia after documents have been uploaded via `/v1/brla/getUploadUrls`. Includes a built-in 5-second delay to allow upstream document propagation. + * @description Submits the owned user's KYC Level 1 payload after documents have been uploaded via `/v1/brla/getUploadUrls`. * - * **Auth:** uses `optionalAuth`. + * **Auth:** Supabase Bearer or profile-bound secret API key. */ post: operations["brlaNewKyc"]; delete?: never; @@ -258,7 +260,7 @@ export interface paths { cookie?: never; }; /** - * Validate Pix key + * Validate PIX key * @description Checks whether a Pix key exists and is valid. The key value itself is intentionally not echoed back in the response for security. * * **Auth:** requires `Authorization: Bearer `. @@ -272,7 +274,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/limits": { + "/v1/brla/kyc/record-attempt": { parameters: { query?: never; header?: never; @@ -282,66 +284,37 @@ export interface paths { get?: never; put?: never; /** - * Get user ramp limits - * @description Returns onramp and offramp limits for the authenticated user's requested fiat corridors. Alfredpay usage is calculated from completed Vortex ramps in the current UTC calendar month and may be delayed by the 60-second in-memory cache. Avenia BRL maximums, usage, and period are read from Avenia. - * - * **Auth:** requires either `X-API-Key: sk_*` linked to a user or `Authorization: Bearer `. Unlinked partner keys are rejected. + * Record initial Avenia KYC attempt + * @description Records the initial Avenia KYC consultation for the effective profile. */ - post: operations["getUserLimits"]; + post: operations["recordInitialAveniaKycAttempt"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/public-key": { + "/v1/brla/kyb/new-level-1/web-sdk": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Public Key - * @description Returns the RSA-PSS 2048 / SHA-256 public key used to verify Vortex webhook signatures. This is NOT a partner `pk_*` API key. + * Start hosted Avenia KYB + * @description Starts the existing provider-hosted Avenia Level 1 KYB flow for an owned company subaccount. */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description RSA-PSS public key in PEM format. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...replace-with-actual-key...\n-----END PUBLIC KEY-----\n" - * } - */ - "application/json": { - /** @description RSA-PSS 2048-bit public key in PEM format. Use this key to verify webhook signatures with RSA-PSS / SHA-256. */ - publicKey: string; - }; - }; - }; - }; - }; - put?: never; - post?: never; + post: operations["startAveniaKybLevel1Hosted"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/quotes": { + "/v1/brla/kyb/documents": { parameters: { query?: never; header?: never; @@ -351,17 +324,17 @@ export interface paths { get?: never; put?: never; /** - * Create a new quote - * @description Generates a quote for a specified ramp transaction, detailing input and output amounts, fees, and expiration. + * Create Avenia KYB document + * @description Creates an Avenia document and returns presigned upload targets. Upload bytes directly to the returned URLs. */ - post: operations["createQuote"]; + post: operations["createAveniaKybDocument"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/quotes/{id}": { + "/v1/brla/kyb/documents/{documentId}": { parameters: { query?: never; header?: never; @@ -369,33 +342,10 @@ export interface paths { cookie?: never; }; /** - * Get existing quote - * @description Get a quote by ID. - * - * **Auth:** none. This endpoint is fully public; anyone with the quote ID can read it. + * Get Avenia KYB document + * @description Reads readiness and upload status for an owned Avenia KYB document. */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Quote Id. */ - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["QuoteResponse"]; - }; - }; - }; - }; + get: operations["getAveniaKybDocument"]; put?: never; post?: never; delete?: never; @@ -404,7 +354,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/quotes/best": { + "/v1/brla/kyb/ubos": { parameters: { query?: never; header?: never; @@ -414,39 +364,37 @@ export interface paths { get?: never; put?: never; /** - * Create a quote for the best network - * @description Generates a new quote for the network that yields the highest output amount for the given parameters. This endpoint compares the output for a given input amount over all supported networks and returns the 'best' quote, defined as the one with the highest output. + * Create Avenia KYB UBO + * @description Registers a UBO after verifying that referenced identity documents are ready and owned by the company subaccount. */ - post: operations["createBestQuote"]; + post: operations["createAveniaKybUbo"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp-info": { + "/v1/brla/kyb/new-level-1/api": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get sanitized ramp eligibility - * @description Returns only sanitized per-corridor KYC state and buy/sell eligibility for the profile derived from the validated API credential. The endpoint accepts no user/profile selector and never returns PII, provider/customer IDs, KYC failure reasons, bank/wallet data, ramp history, or exact financial limits. When both public and secret headers are supplied they must belong to the same credential. Supabase Bearer sessions do not authorize this endpoint. - * - * **Auth:** `X-Public-Key` or `X-API-Key`. + * Submit API-driven Avenia KYB + * @description Submits the API-driven Avenia Level 1 KYB attempt after validating the owned corporate documents and UBO references. */ - get: operations["getRampInfo"]; - put?: never; - post?: never; + post: operations["submitAveniaKybLevel1Api"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/{id}": { + "/v1/brla/kyb/attempt-status": { parameters: { query?: never; header?: never; @@ -454,89 +402,10 @@ export interface paths { cookie?: never; }; /** - * Get ramp status - * @description Fetches an updated ramp process. + * Get Avenia KYB attempt status + * @description Reads an Avenia KYB attempt after binding it to the effective profile's company account. */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Ramp ID. */ - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - countryCode?: components["schemas"]["CountryCode"]; - /** - * Format: date-time - * @description Timestamp of when the ramp process was created. - */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; - inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; - }; - }; - }; - }; - }; + get: operations["getAveniaKybAttemptStatus"]; put?: never; post?: never; delete?: never; @@ -545,7 +414,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/ramp/{id}/errors": { + "/v1/alfredpay/alfredpayStatus": { parameters: { query?: never; header?: never; @@ -553,12 +422,10 @@ export interface paths { cookie?: never; }; /** - * Get ramp error logs - * @description Returns the chronological error log for a ramp. - * - * **Auth:** requires either `X-API-Key: sk_*` (partner) OR `Authorization: Bearer ` (user). Ownership is enforced. + * Get Alfredpay onboarding status + * @description Reads and, when applicable, refreshes the effective profile's Alfredpay onboarding status. */ - get: operations["getRampErrorLogs"]; + get: operations["getAlfredpayStatus"]; put?: never; post?: never; delete?: never; @@ -567,139 +434,87 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/ramp/history": { + "/v1/alfredpay/createIndividualCustomer": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get authenticated user ramp history - * @description Fetches all non-initial ramps owned by the authenticated user across wallet addresses. Requires a Supabase session or user-scoped secret API key. Partner-only credentials are not sufficient. + * Create Alfredpay individual customer + * @description Creates the effective profile's Alfredpay individual customer. */ - get: { - parameters: { - query?: { - /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ - limit?: number; - /** @description The offset for querying older transactions. */ - offset?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authenticated user's ramp history. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetRampHistoryResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; + post: operations["createAlfredpayIndividualCustomer"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/history/{walletAddress}": { + "/v1/alfredpay/createBusinessCustomer": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get ramp history for wallet address - * @description Fetches the transaction history for a given wallet address. The response returns the last 20 items by default. This can be adjusted by using the `limit` and `offset` query parameters. + * Create Alfredpay business customer + * @description Creates the effective profile's Alfredpay business customer. */ - get: { - parameters: { - query?: { - /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ - limit?: number; - /** @description The offset for querying the transactions. Necessary if the number of transaction items of the address is larger than the maximum limit. A larger value will return older transaction items. */ - offset?: number; - }; - header?: never; - path: { - /** @description The wallet address for which the ramp history is queried for. */ - walletAddress: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetRampHistoryResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; + post: operations["createAlfredpayBusinessCustomer"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/register": { + "/v1/alfredpay/getKycRedirectLink": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Register new ramp process - * @description Initiates a new on-ramp or off-ramp process by providing quote details, signing accounts, and additional data. + * Get Alfredpay KYC redirect link + * @description Creates or reads the provider-hosted individual KYC session. */ - post: operations["registerRamp"]; + get: operations["getAlfredpayKycRedirectLink"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/start": { + "/v1/alfredpay/getKybRedirectLink": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Start ramp process - * @description Starts a ramp process. - * - * It is assumed all required information from the client has already been sent using the `update` endpoint. This endpoint is only used to tell the backend any external operation (like a bank transfer) has been completed, and the ramp can start. + * Get Alfredpay KYB redirect link + * @description Creates or reads the provider-hosted business KYB session. */ - post: operations["startRamp"]; + get: operations["getAlfredpayKybRedirectLink"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/update": { + "/v1/alfredpay/kycRedirectOpened": { parameters: { query?: never; header?: never; @@ -709,28 +524,17 @@ export interface paths { get?: never; put?: never; /** - * Update ramp process - * @description Submits presigned transactions and additional data to an existing ramp process before starting it. - * This endpoint can be called many times, and data can be incrementally added to the ramp. - * - * Note: For both pre-signed transactions and the generic `additionalData` object, existing properties will be overriden by new values. - * - * ### Required data for ramps. - * The signed counterpart of the initial unsignedTxs object must be provided for all ramps, as required by the object. - * For offramps, the `additionalData` field must contain the confirmation hash corresponding to the inital transaction in which the user sends the funds. - * If the originating chain is `Assethub`, then `assetHubToPendulumHash` must be provided. - * If the originating chain is any `EVM` chain, then `squidRouterSwapHash` must be provided. `squidRouterApproveHash` is only required when an approval transaction was actually submitted; if the wallet already holds a sufficient allowance for the router, it can be omitted. - * - * For onramps, no additional data is required after registering the ramp. + * Record Alfredpay hosted form opened + * @description Records that the customer opened the provider-hosted form. This is not a compliance decision. */ - post: operations["updateRamp"]; + post: operations["notifyAlfredpayKycRedirectOpened"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/session/create": { + "/v1/alfredpay/kycRedirectFinished": { parameters: { query?: never; header?: never; @@ -740,140 +544,28 @@ export interface paths { get?: never; put?: never; /** - * Create widget session - * @description Creates a hosted Vortex Widget session and returns the URL to open for the user. - * - * This single endpoint supports two mutually exclusive request shapes: - * - * - **Fixed quote** (`GetWidgetUrlLocked`) — pass a `quoteId` you created via `POST /v1/quotes`. The widget uses that exact quote and does not refresh it. If the quote expires before the user finishes, they must close the window and start over. - * - * - **Auto-refresh** (`GetWidgetUrlRefresh`) — pass the route parameters (`network`, `rampType`, `inputAmount`, plus `fiat` / `cryptoLocked` / `paymentMethod` as relevant for the direction). The widget creates and refreshes quotes on demand for the user. - * - * Use the example switcher below to see the request shape for each mode. `externalSessionId` is required in both modes and is echoed back in webhook payloads. + * Record Alfredpay hosted form completion + * @description Records customer-reported completion of the provider-hosted form. Provider status remains authoritative. */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["GetWidgetUrlLocked"] | components["schemas"]["GetWidgetUrlRefresh"]; - }; - }; - responses: { - /** @description Returned when a fixed-quote session was created. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id"eId=quote_01HXY..." - * } - */ - "application/json": { - /** @description The widget URL to open for the user. */ - url: string; - }; - }; - }; - /** @description Returned when an auto-refresh session was created. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id&rampType=BUY&network=polygon&inputAmount=150&fiat=BRL&cryptoLocked=USDC&paymentMethod=pix" - * } - */ - "application/json": { - /** @description The widget URL to open for the user. */ - url: string; - }; - }; - }; - /** @description Missing required fields, or `quoteId` not provided for fixed-quote mode and route fields not provided for auto-refresh mode. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Quote not found or expired (fixed-quote mode only). */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; + post: operations["notifyAlfredpayKycRedirectFinished"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/supported-countries": { + "/v1/alfredpay/getKycStatus": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Supported Countries */ - get: { - parameters: { - query?: { - /** - * @description ISO code: "BR", "AR", etc. - * @example - */ - countryCode?: string; - /** @description e.g. "Brazil", "Germany" */ - name?: string; - /** @description e.g. "BRL". All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ - fiatCurrency?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - countries: { - /** @description e.g. `DE` */ - countryCode: string; - }[]; - /** @description e.g. 🇩🇪 */ - emoji: string; - /** @description e.g. `Germany` */ - name: string; - support: { - /** @description e.g. `true` */ - buy: boolean; - /** @description e.g. `true` */ - sell: boolean; - }; - /** @description All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ - supportedCurrencies: string[]; - }; - }; - }; - }; - }; + /** + * Get Alfredpay KYC or KYB status + * @description Reads individual KYC or business KYB status. `type` defaults to `INDIVIDUAL`. + */ + get: operations["getAlfredpayKycStatus"]; put?: never; post?: never; delete?: never; @@ -882,160 +574,138 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/supported-cryptocurrencies": { + "/v1/alfredpay/retryKyc": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Supported Cryptocurrencies - * @description Retrieve all supported cryptocurrencies, filtered by network. + * Retry Alfredpay verification + * @description Restarts the existing API-based or hosted Alfredpay verification flow. */ - get: { - parameters: { - query?: { - /** - * @description Filter supported cryptocurrencies by network. Allowed values: `assethub`, `avalanche`, `base`, `bsc`, `ethereum`, `polygon` - * @example - */ - network?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - cryptocurrencies: { - /** @description Defined if network is EVM. */ - assetContractAddress?: string | null; - assetDecimals: number; - /** @description Defined if network is Assethub. */ - assetForeignAssetId?: string | null; - assetNetwork: components["schemas"]["Networks"]; - assetSymbol: string; - }[]; - }; - }; - }; - }; + post: operations["retryAlfredpayKyc"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/alfredpay/submitKycInformation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + get?: never; put?: never; - post?: never; + /** + * Submit Alfredpay KYC information + * @description Creates an API-driven Alfredpay individual KYC submission for MX, CO, or AR. + */ + post: operations["submitAlfredpayKycInformation"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/supported-fiat-currencies": { + "/v1/alfredpay/submitKycFile": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Supported Fiat Currencies */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - currencies: { - /** @description e.g. `2` */ - decimals: number; - /** @description e.g. `Brazilian Real` */ - name: string; - /** @description e.g. `BRL` */ - symbol: string; - }[]; - }; - }; - }; - }; + get?: never; + put?: never; + /** + * Upload Alfredpay KYC file + * @description Uploads one individual KYC file. Files are limited to 5 MiB. + */ + post: operations["submitAlfredpayKycFile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/alfredpay/sendKycSubmission": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + get?: never; put?: never; - post?: never; + /** + * Finalize Alfredpay KYC submission + * @description Finalizes an API-driven Alfredpay individual KYC submission. + */ + post: operations["sendAlfredpayKycSubmission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/supported-payment-methods": { + "/v1/alfredpay/submitKybInformation": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Supported Payment Methods - * @description Retrieve all available payment methods, filtered by type or fiat. + * Submit Alfredpay KYB information + * @description Creates or updates an API-driven Alfredpay business KYB submission for MX or CO. */ - get: { - parameters: { - query?: { - /** - * @description Filter supported payment methods by the ramp type. Allowed values: `sell` or `buy`. - * @example - */ - type?: string; - /** - * @description Filter supported payment methods by fiat currency. Allowed values: `EUR`, `ARS`, `BRL`, `USD`, `MXN`, `COP`. - * @example - */ - fiat?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @description Array of supported payment methods matching the params. */ - "paymentMethods:": { - /** @description Unique identifier of the payment method: `sepa`, `pix`, `cbu` */ - id: string; - /** @description Payment method limits in USD */ - limits: { - max: number; - min: number; - }; - /** @description Unique name of the payment method: `SEPA`, `PIX`, `CBU` */ - name: string; - /** @description Array of supported fiat currencies by payment method. */ - supportedFiats: string[]; - }[]; - }; - }; - }; - }; + post: operations["submitAlfredpayKybInformation"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/alfredpay/submitKybFile": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Upload Alfredpay KYB file + * @description Uploads one company KYB file. Files are limited to 5 MiB. + */ + post: operations["submitAlfredpayKybFile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/alfredpay/findKybCustomerAndBusiness": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + /** + * Find Alfredpay KYB submission identifiers + * @description Returns the submission and related-person identifiers needed for representative document uploads. + */ + get: operations["findAlfredpayKybCustomerAndBusiness"]; put?: never; post?: never; delete?: never; @@ -1044,7 +714,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/webhook": { + "/v1/alfredpay/submitKybRelatedPersonFile": { parameters: { query?: never; header?: never; @@ -1054,34 +724,139 @@ export interface paths { get?: never; put?: never; /** - * Register Webhook - * @description Register a new webhook to receive event notifications. - * - * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. + * Upload Alfredpay related-person file + * @description Uploads one identity-document side for an Alfredpay KYB related person. Files are limited to 5 MiB. + */ + post: operations["submitAlfredpayKybRelatedPersonFile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/alfredpay/sendKybSubmission": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Finalize Alfredpay KYB submission + * @description Finalizes an API-driven Alfredpay business KYB submission. + */ + post: operations["sendAlfredpayKybSubmission"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/onboarding/active-entity": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Select active customer entity + * @description Selects the authenticated profile's immutable active customer-entity type. Managed-child delegation is not supported. + */ + put: operations["selectActiveCustomerEntity"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/onboarding/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get aggregate onboarding status + * @description Returns the effective profile's aggregate customer entities, provider accounts, and KYC/KYB cases. A manager may select an authorized child with `X-Managed-Profile-Id`. + */ + get: operations["getOnboardingStatus"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/onboarding/requirements": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Discover KYC or KYB requirements + * @description Returns versioned field, document, and ordered-operation metadata for an existing Avenia or Alfredpay onboarding flow. This endpoint does not return profile state or customer PII and does not replace the provider-specific operations it references. Monerium is outside this discovery proposal. + */ + get: operations["getOnboardingRequirements"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/limits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get user ramp limits + * @description Returns onramp and offramp limits for the authenticated user's requested fiat corridors. Alfredpay usage is calculated from completed Vortex ramps in the current UTC calendar month and may be delayed by the 60-second in-memory cache. Avenia BRL maximums, usage, and period are read from Avenia. * - * Webhooks are bound to the account behind your secret key: a `quoteId` must belong to a quote created with your key (any other quote returns `404`). The callback URL must use HTTPS, must not embed credentials, and must resolve to a publicly routable address; private or reserved IP ranges are rejected. + * **Auth:** requires either `X-API-Key: sk_*` linked to a user or `Authorization: Bearer `. Unlinked partner keys are rejected. */ - post: { + post: operations["getUserLimits"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/public-key": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Public Key + * @description Returns the RSA-PSS 2048 / SHA-256 public key used to verify Vortex webhook signatures. This is NOT a partner `pk_*` API key. + */ + get: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: { - content: { - "application/json": { - events?: string[]; - /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific quote. The quote must have been created with your API key. */ - quoteId?: string; - /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific session */ - sessionId?: string; - /** @description Your HTTPS webhook endpoint URL. No embedded credentials; must resolve to a publicly routable address. */ - url: string; - }; - }; - }; + requestBody?: never; responses: { + /** @description RSA-PSS public key in PEM format. */ 200: { headers: { [name: string]: unknown; @@ -1089,45 +864,26 @@ export interface paths { content: { /** * @example { - * "createdAt": "2025-10-01T16:21:04.648Z", - * "events": [ - * "TRANSACTION_CREATED", - * "STATUS_CHANGE" - * ], - * "id": "340ba946-f3f3-4007-893c-3374bfcd096b", - * "isActive": true, - * "quoteId": "3258910e-93ee-443e-b793-28cc1d4ccdf3", - * "sessionId": null, - * "url": "https://your-website.com" + * "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...replace-with-actual-key...\n-----END PUBLIC KEY-----\n" * } */ "application/json": { - /** @description The creation date of the webhook */ - createdAt: string; - /** @description The events the webhook is subscribed for */ - events: string[]; - /** @description Webhook UUID */ - id: string; - /** @description Is the webhook active */ - isActive: boolean; - /** @description (optional): The specific transactionId that the events are subscribed for */ - quoteId?: string; - /** @description (optional): The specific sessionId that the events are subscribed for */ - sessionId?: string; - /** @description Your HTTPS webhook endpoint URL */ - url: string; + /** @description RSA-PSS 2048-bit public key in PEM format. Use this key to verify webhook signatures with RSA-PSS / SHA-256. */ + publicKey: string; }; }; }; }; }; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/webhook/{id}": { + "/v1/quotes": { parameters: { query?: never; header?: never; @@ -1136,20 +892,36 @@ export interface paths { }; get?: never; put?: never; - post?: never; /** - * Delete Webhook - * @description Remove a webhook subscription. - * - * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. + * Create a new quote + * @description Generates a quote for a specified ramp transaction, detailing input and output amounts, fees, and expiration. + */ + post: operations["createQuote"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/quotes/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get existing quote + * @description Get a quote by ID. * - * Deletion is scoped to your account: a webhook registered by another account returns `404`. + * **Auth:** none. This endpoint is fully public; anyone with the quote ID can read it. */ - delete: { + get: { parameters: { query?: never; header?: never; path: { + /** @description Quote Id. */ id: string; }; cookie?: never; @@ -1161,852 +933,3414 @@ export interface paths { [name: string]: unknown; }; content: { - "application/json": { - message: string; - success: boolean; - }; + "application/json": components["schemas"]["QuoteResponse"]; }; }; }; }; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; -} -export type webhooks = Record; -export interface components { - schemas: { - AccountMeta: { - /** @description The account address. */ - address: string; - /** - * @description The type of the account. - * @enum {string} - */ - type: "EVM" | "Substrate"; - }; - ApiCredential: { - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - environment: "live" | "test"; - /** Format: date-time */ - expiresAt: string; - /** Format: uuid */ - id: string; - name: string; - /** Format: uuid */ - partnerId: string | null; - /** Format: uuid */ - profileId: string; - /** @description Retrievable public half of the credential. */ - publicKey: string; - /** Format: date-time */ - publicLastUsedAt: string | null; - /** Format: date-time */ - revokedAt: string | null; - /** @description Non-secret 16-character lookup/display prefix. The secret value is not retrievable. */ - secretKeyPrefix: string; - /** Format: date-time */ - secretLastUsedAt: string | null; - /** Format: date-time */ - updatedAt: string; - }; - ApiCredentialErrorResponse: { - error: { - /** @description Machine-readable error code such as `AUTHENTICATION_REQUIRED`, `INVALID_PUBLIC_KEY`, `INVALID_SECRET_KEY`, `CREDENTIAL_MISMATCH`, `CREDENTIAL_LIMIT_REACHED`, `CREDENTIAL_NOT_FOUND`, `CREDENTIAL_SUBJECT_REQUIRED`, `INVALID_CREDENTIAL_EXPIRY`, or `INVALID_CREDENTIAL_NAME`. */ - code: string; - message: string; - status: number; - }; - }; - /** @enum {string} */ - AveniaDocumentType: "ID" | "DRIVERS-LICENSE" | "PASSPORT" | "SELFIE" | "SELFIE-FROM-LIVENESS"; - AveniaKYCDataUploadRequest: { - documentType: components["schemas"]["AveniaDocumentType"]; - /** @description CPF or CNPJ. */ - taxId: string; - }; - AveniaKYCDataUploadResponse: { - idUpload: components["schemas"]["DocumentUploadEntry"]; - selfieUpload: components["schemas"]["DocumentUploadEntry"]; + "/v1/quotes/best": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - BrlaAddress: { - cep: string; - city: string; - complement?: string | null; - district: string; - number: string; - state: string; - street: string; + get?: never; + put?: never; + /** + * Create a quote for the best network + * @description Generates a new quote for the network that yields the highest output amount for the given parameters. This endpoint compares the output for a given input amount over all supported networks and returns the 'best' quote, defined as the one with the highest output. + */ + post: operations["createBestQuote"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp-info": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - BrlaErrorResponse: { - /** @description Detailed error message or object from BRLA API or server. */ - details?: null & (string | { - [key: string]: unknown; - }); - /** @description A summary of the error. */ - error?: string; + /** + * Get sanitized ramp eligibility + * @description Returns only sanitized per-corridor KYC state and buy/sell eligibility for the profile derived from the validated API credential. The endpoint accepts no user/profile selector and never returns PII, provider/customer IDs, KYC failure reasons, bank/wallet data, ramp history, or exact financial limits. When both public and secret headers are supplied they must belong to the same credential. Supabase Bearer sessions do not authorize this endpoint. + * + * **Auth:** `X-Public-Key` or `X-API-Key`. + */ + get: operations["getRampInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - BrlaGetSelfieLivenessUrlResponse: { - id: string; - livenessUrl: string; - uploadURLFront: string; - validateLivenessToken: string; + /** + * Get ramp status + * @description Fetches an updated ramp process. + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Ramp ID. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; + }; + }; + }; + }; }; - BrlaValidatePixKeyResponse: { - valid: boolean; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/{id}/errors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - CleanupPhase: { - /** @enum {string} */ - string?: "moonbeamCleanup" | "pendulumCleanup" | "stellarCleanup"; + /** + * Get ramp error logs + * @description Returns the chronological error log for a ramp. + * + * **Auth:** requires either `X-API-Key: sk_*` (partner) OR `Authorization: Bearer ` (user). Ownership is enforced. + */ + get: operations["getRampErrorLogs"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - /** @description Allowed values: `AR`, `BR`, `EU` */ - CountryCode: string; - CreateApiCredentialRequest: { - /** - * Format: date-time - * @description Optional future ISO-8601 expiry, at most two years from creation. Defaults to one year. - */ - expiresAt?: string; - /** @default API Credential */ - name: string; - }; - CreateApiCredentialResponse: components["schemas"]["ApiCredential"] & { - /** @description Returned only at creation. Store it immediately in a server-side secret manager. */ - secretKey: string; - }; - CreateBestQuoteRequest: { - /** @description Your api key, if available. */ - apiKey?: string; - countryCode?: components["schemas"]["CountryCode"]; - /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "BUY". */ - from?: components["schemas"]["PaymentMethod"]; - /** - * @description The amount of currency to be input. - * @example 100.00 - */ - inputAmount: string; - /** @description The currency type for the input amount. */ - inputCurrency: components["schemas"]["RampCurrency"]; - /** @description Optional whitelist of networks to evaluate when searching for the best quote. If omitted or empty, all eligible networks for the corridor are considered. */ - networks?: components["schemas"]["Networks"][]; - /** @description The desired currency type for the output amount. */ - outputCurrency: components["schemas"]["RampCurrency"]; - /** @description Your partner ID, if available. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - /** @description The type of ramp process (on-ramp or off-ramp). */ - rampType: components["schemas"]["RampDirection"]; - /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "SELL". */ - to?: components["schemas"]["PaymentMethod"]; + /** + * Get authenticated user ramp history + * @description Fetches all non-initial ramps owned by the authenticated user across wallet addresses. Requires a Supabase session or user-scoped secret API key. Partner-only credentials are not sufficient. + */ + get: { + parameters: { + query?: { + /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ + limit?: number; + /** @description The offset for querying older transactions. */ + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authenticated user's ramp history. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetRampHistoryResponse"]; + }; + }; + }; }; - CreateQuoteRequest: { - /** @description Your api key, if available. */ - apiKey?: string; - countryCode?: components["schemas"]["CountryCode"]; - /** @description From destination */ - from: components["schemas"]["DestinationType"]; - /** - * @description The amount of currency to be input. - * @example 100.00 - */ - inputAmount: string; - /** @description The currency type for the input amount. */ - inputCurrency: components["schemas"]["RampCurrency"]; - network?: components["schemas"]["Networks"]; - /** @description The desired currency type for the output amount. */ - outputCurrency: components["schemas"]["RampCurrency"]; - /** @description Your partner ID, if available. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - /** @description The type of ramp process (on-ramp or off-ramp). */ - rampType: components["schemas"]["RampDirection"]; - /** @description To destination */ - to: components["schemas"]["DestinationType"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/history/{walletAddress}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - CreateSubaccountRequest: { - address: components["schemas"]["BrlaAddress"]; - /** - * Format: date - * @description Date must be in format YYYY-MMM-DD. - */ - birthdate: string; - cnpj?: string | null; - companyName?: string | null; - cpf: string; - fullName: string; - phone: string; - /** @description Optional. The quote that triggered onboarding. Omit it for the quote-less KYB deep link (`?kyb` / `?kybLocked` widget entry), where business verification starts before any quote exists. Stored only as onboarding provenance; it is not an authorization input. */ - quoteId?: string | null; - /** - * Format: date - * @description Date must be in format YYYY-MMM-DD. - */ - startDate?: string | null; - taxIdType: components["schemas"]["TaxIdType"]; + /** + * Get ramp history for wallet address + * @description Fetches the transaction history for a given wallet address. The response returns the last 20 items by default. This can be adjusted by using the `limit` and `offset` query parameters. + */ + get: { + parameters: { + query?: { + /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ + limit?: number; + /** @description The offset for querying the transactions. Necessary if the number of transaction items of the address is larger than the maximum limit. A larger value will return older transaction items. */ + offset?: number; + }; + header?: never; + path: { + /** @description The wallet address for which the ramp history is queried for. */ + walletAddress: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetRampHistoryResponse"]; + }; + }; + }; }; - CreateSubaccountResponse: { - /** @description The ID of the created or processed subaccount. */ - subaccountId?: string; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + get?: never; + put?: never; /** - * @description Represents either a blockchain network or a traditional payment method. - * @enum {string} + * Register new ramp process + * @description Initiates a new on-ramp or off-ramp process by providing quote details, signing accounts, and additional data. */ - DestinationType: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam" | "pendulum" | "stellar" | "pix" | "sepa" | "cbu" | "ach" | "spei"; - DocumentUploadEntry: { - id: string; - livenessUrl?: string; - uploadURLBack?: string; - uploadURLFront: string; - validateLivenessToken?: string; + post: operations["registerRamp"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/start": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - ErrorResponse: { - /** @description HTTP status code returned by the API error handler. */ - code?: number; - /** @description Validation error details, when the request fails schema or input validation. */ - errors?: Record[]; - /** @description A human-readable error message. */ - message?: string; - /** @description HTTP status code included by selected provider-style error responses. */ - statusCode?: number; - /** @description Provider-style error category, when available. */ - type?: string; + get?: never; + put?: never; + /** + * Start ramp process + * @description Starts a ramp process. + * + * It is assumed all required information from the client has already been sent using the `update` endpoint. This endpoint is only used to tell the backend any external operation (like a bank transfer) has been completed, and the ramp can start. + */ + post: operations["startRamp"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/ramp/update": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - /** @enum {string} */ - FiatToken: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP"; - GetKycStatusResponse: { - /** @description The KYC level achieved. */ - level?: number; - /** - * @description The KYC status. - * @enum {string} - */ - status?: "PENDING" | "APPROVED" | "REJECTED"; - /** - * @description Event type, typically "KYC". - * @enum {string} - */ - type?: "KYC"; + get?: never; + put?: never; + /** + * Update ramp process + * @description Submits presigned transactions and additional data to an existing ramp process before starting it. + * This endpoint can be called many times, and data can be incrementally added to the ramp. + * + * Note: For both pre-signed transactions and the generic `additionalData` object, existing properties will be overriden by new values. + * + * ### Required data for ramps. + * The signed counterpart of the initial unsignedTxs object must be provided for all ramps, as required by the object. + * For offramps, the `additionalData` field must contain the confirmation hash corresponding to the inital transaction in which the user sends the funds. + * If the originating chain is `Assethub`, then `assetHubToPendulumHash` must be provided. + * If the originating chain is any `EVM` chain, then `squidRouterSwapHash` must be provided. `squidRouterApproveHash` is only required when an approval transaction was actually submitted; if the wallet already holds a sufficient allowance for the router, it can be omitted. + * + * For onramps, no additional data is required after registering the ramp. + */ + post: operations["updateRamp"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/session/create": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - GetRampErrorLogsResponse: components["schemas"]["RampErrorLog"][]; + get?: never; + put?: never; + /** + * Create widget session + * @description Creates a hosted Vortex Widget session and returns the URL to open for the user. + * + * This single endpoint supports two mutually exclusive request shapes: + * + * - **Fixed quote** (`GetWidgetUrlLocked`) — pass a `quoteId` you created via `POST /v1/quotes`. The widget uses that exact quote and does not refresh it. If the quote expires before the user finishes, they must close the window and start over. + * + * - **Auto-refresh** (`GetWidgetUrlRefresh`) — pass the route parameters (`network`, `rampType`, `inputAmount`, plus `fiat` / `cryptoLocked` / `paymentMethod` as relevant for the direction). The widget creates and refreshes quotes on demand for the user. + * + * Use the example switcher below to see the request shape for each mode. `externalSessionId` is required in both modes and is echoed back in webhook payloads. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GetWidgetUrlLocked"] | components["schemas"]["GetWidgetUrlRefresh"]; + }; + }; + responses: { + /** @description Returned when a fixed-quote session was created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id"eId=quote_01HXY..." + * } + */ + "application/json": { + /** @description The widget URL to open for the user. */ + url: string; + }; + }; + }; + /** @description Returned when an auto-refresh session was created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id&rampType=BUY&network=polygon&inputAmount=150&fiat=BRL&cryptoLocked=USDC&paymentMethod=pix" + * } + */ + "application/json": { + /** @description The widget URL to open for the user. */ + url: string; + }; + }; + }; + /** @description Missing required fields, or `quoteId` not provided for fixed-quote mode and route fields not provided for auto-refresh mode. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Quote not found or expired (fixed-quote mode only). */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/supported-countries": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Supported Countries */ + get: { + parameters: { + query?: { + /** + * @description ISO code: "BR", "AR", etc. + * @example + */ + countryCode?: string; + /** @description e.g. "Brazil", "Germany" */ + name?: string; + /** @description e.g. "BRL". All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ + fiatCurrency?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + countries: { + /** @description e.g. `DE` */ + countryCode: string; + }[]; + /** @description e.g. 🇩🇪 */ + emoji: string; + /** @description e.g. `Germany` */ + name: string; + support: { + /** @description e.g. `true` */ + buy: boolean; + /** @description e.g. `true` */ + sell: boolean; + }; + /** @description All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ + supportedCurrencies: string[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/supported-cryptocurrencies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Supported Cryptocurrencies + * @description Retrieve all supported cryptocurrencies, filtered by network. + */ + get: { + parameters: { + query?: { + /** + * @description Filter supported cryptocurrencies by network. Allowed values: `assethub`, `avalanche`, `base`, `bsc`, `ethereum`, `polygon` + * @example + */ + network?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + cryptocurrencies: { + /** @description Defined if network is EVM. */ + assetContractAddress?: string | null; + assetDecimals: number; + /** @description Defined if network is Assethub. */ + assetForeignAssetId?: string | null; + assetNetwork: components["schemas"]["Networks"]; + assetSymbol: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/supported-fiat-currencies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Supported Fiat Currencies */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + currencies: { + /** @description e.g. `2` */ + decimals: number; + /** @description e.g. `Brazilian Real` */ + name: string; + /** @description e.g. `BRL` */ + symbol: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/supported-payment-methods": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Supported Payment Methods + * @description Retrieve all available payment methods, filtered by type or fiat. + */ + get: { + parameters: { + query?: { + /** + * @description Filter supported payment methods by the ramp type. Allowed values: `sell` or `buy`. + * @example + */ + type?: string; + /** + * @description Filter supported payment methods by fiat currency. Allowed values: `EUR`, `ARS`, `BRL`, `USD`, `MXN`, `COP`. + * @example + */ + fiat?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Array of supported payment methods matching the params. */ + "paymentMethods:": { + /** @description Unique identifier of the payment method: `sepa`, `pix`, `cbu` */ + id: string; + /** @description Payment method limits in USD */ + limits: { + max: number; + min: number; + }; + /** @description Unique name of the payment method: `SEPA`, `PIX`, `CBU` */ + name: string; + /** @description Array of supported fiat currencies by payment method. */ + supportedFiats: string[]; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhook": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register Webhook + * @description Register a new webhook to receive event notifications. + * + * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. + * + * Webhooks are bound to the account behind your secret key: a `quoteId` must belong to a quote created with your key (any other quote returns `404`). The callback URL must use HTTPS, must not embed credentials, and must resolve to a publicly routable address; private or reserved IP ranges are rejected. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + events?: string[]; + /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific quote. The quote must have been created with your API key. */ + quoteId?: string; + /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific session */ + sessionId?: string; + /** @description Your HTTPS webhook endpoint URL. No embedded credentials; must resolve to a publicly routable address. */ + url: string; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "createdAt": "2025-10-01T16:21:04.648Z", + * "events": [ + * "TRANSACTION_CREATED", + * "STATUS_CHANGE" + * ], + * "id": "340ba946-f3f3-4007-893c-3374bfcd096b", + * "isActive": true, + * "quoteId": "3258910e-93ee-443e-b793-28cc1d4ccdf3", + * "sessionId": null, + * "url": "https://your-website.com" + * } + */ + "application/json": { + /** @description The creation date of the webhook */ + createdAt: string; + /** @description The events the webhook is subscribed for */ + events: string[]; + /** @description Webhook UUID */ + id: string; + /** @description Is the webhook active */ + isActive: boolean; + /** @description (optional): The specific transactionId that the events are subscribed for */ + quoteId?: string; + /** @description (optional): The specific sessionId that the events are subscribed for */ + sessionId?: string; + /** @description Your HTTPS webhook endpoint URL */ + url: string; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhook/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete Webhook + * @description Remove a webhook subscription. + * + * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. + * + * Deletion is scoped to your account: a webhook registered by another account returns `404`. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message: string; + success: boolean; + }; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + AccountMeta: { + /** @description The account address. */ + address: string; + /** + * @description The type of the account. + * @enum {string} + */ + type: "EVM" | "Substrate"; + }; + ApiCredential: { + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + environment: "live" | "test"; + /** Format: date-time */ + expiresAt: string; + /** Format: uuid */ + id: string; + name: string; + /** Format: uuid */ + partnerId: string | null; + /** Format: uuid */ + profileId: string; + /** @description Retrievable public half of the credential. */ + publicKey: string; + /** Format: date-time */ + publicLastUsedAt: string | null; + /** Format: date-time */ + revokedAt: string | null; + /** @description Non-secret 16-character lookup/display prefix. The secret value is not retrievable. */ + secretKeyPrefix: string; + /** Format: date-time */ + secretLastUsedAt: string | null; + /** Format: date-time */ + updatedAt: string; + }; + ApiCredentialErrorResponse: { + error: { + /** @description Machine-readable error code such as `AUTHENTICATION_REQUIRED`, `INVALID_PUBLIC_KEY`, `INVALID_SECRET_KEY`, `CREDENTIAL_MISMATCH`, `CREDENTIAL_LIMIT_REACHED`, `CREDENTIAL_NOT_FOUND`, `CREDENTIAL_SUBJECT_REQUIRED`, `INVALID_CREDENTIAL_EXPIRY`, or `INVALID_CREDENTIAL_NAME`. */ + code: string; + message: string; + status: number; + }; + }; + /** @enum {string} */ + AveniaDocumentType: "ID" | "DRIVERS-LICENSE" | "PASSPORT" | "RESIDENCE-PERMIT" | "SELFIE" | "SELFIE-FROM-LIVENESS" | "CERTIFICATE-OF-INCORPORATION" | "COMPANY-TAX-IDENTIFICATION-DOCUMENT"; + AveniaKYCDataUploadRequest: { + /** @enum {string} */ + documentType: "ID" | "DRIVERS-LICENSE"; + isDoubleSided?: boolean; + /** @description CPF or CNPJ. */ + taxId: string; + }; + AveniaKYCDataUploadResponse: { + idUpload: components["schemas"]["DocumentUploadEntry"]; + selfieUpload: components["schemas"]["DocumentUploadEntry"]; + }; + BrlaAddress: { + cep: string; + city: string; + complement?: string | null; + district: string; + number: string; + state: string; + street: string; + }; + BrlaErrorResponse: { + /** @description Detailed error message or object from BRLA API or server. */ + details?: null & (string | { + [key: string]: unknown; + }); + /** @description A summary of the error. */ + error?: string; + }; + BrlaGetSelfieLivenessUrlResponse: { + id: string; + livenessUrl: string; + uploadURLFront: string; + validateLivenessToken: string; + }; + BrlaValidatePixKeyResponse: { + valid: boolean; + }; + CleanupPhase: { + /** @enum {string} */ + string?: "moonbeamCleanup" | "pendulumCleanup" | "stellarCleanup"; + }; + /** @description Allowed values: `AR`, `BR`, `EU` */ + CountryCode: string; + CreateApiCredentialRequest: { + /** + * Format: date-time + * @description Optional future ISO-8601 expiry, at most two years from creation. Defaults to one year. + */ + expiresAt?: string; + /** @default API Credential */ + name: string; + }; + CreateApiCredentialResponse: components["schemas"]["ApiCredential"] & { + /** @description Returned only at creation. Store it immediately in a server-side secret manager. */ + secretKey: string; + }; + CreateBestQuoteRequest: { + /** @description Your api key, if available. */ + apiKey?: string; + countryCode?: components["schemas"]["CountryCode"]; + /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "BUY". */ + from?: components["schemas"]["PaymentMethod"]; + /** + * @description The amount of currency to be input. + * @example 100.00 + */ + inputAmount: string; + /** @description The currency type for the input amount. */ + inputCurrency: components["schemas"]["RampCurrency"]; + /** @description Optional whitelist of networks to evaluate when searching for the best quote. If omitted or empty, all eligible networks for the corridor are considered. */ + networks?: components["schemas"]["Networks"][]; + /** @description The desired currency type for the output amount. */ + outputCurrency: components["schemas"]["RampCurrency"]; + /** @description Your partner ID, if available. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + /** @description The type of ramp process (on-ramp or off-ramp). */ + rampType: components["schemas"]["RampDirection"]; + /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "SELL". */ + to?: components["schemas"]["PaymentMethod"]; + }; + CreateQuoteRequest: { + /** @description Your api key, if available. */ + apiKey?: string; + countryCode?: components["schemas"]["CountryCode"]; + /** @description From destination */ + from: components["schemas"]["DestinationType"]; + /** + * @description The amount of currency to be input. + * @example 100.00 + */ + inputAmount: string; + /** @description The currency type for the input amount. */ + inputCurrency: components["schemas"]["RampCurrency"]; + network?: components["schemas"]["Networks"]; + /** @description The desired currency type for the output amount. */ + outputCurrency: components["schemas"]["RampCurrency"]; + /** @description Your partner ID, if available. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + /** @description The type of ramp process (on-ramp or off-ramp). */ + rampType: components["schemas"]["RampDirection"]; + /** @description To destination */ + to: components["schemas"]["DestinationType"]; + }; + CreateSubaccountRequest: { + /** @enum {string} */ + accountType: "INDIVIDUAL" | "COMPANY"; + /** @description Individual full name or company legal name. */ + name: string; + quoteId?: string; + sessionId?: string; + /** @description CPF for an individual or CNPJ for a company. */ + taxId: string; + }; + CreateSubaccountResponse: { + /** @description The ID of the created or processed subaccount. */ + subAccountId: string; + }; + /** + * @description Represents either a blockchain network or a traditional payment method. + * @enum {string} + */ + DestinationType: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam" | "pendulum" | "stellar" | "pix" | "sepa" | "cbu" | "ach" | "spei"; + DocumentUploadEntry: { + id: string; + livenessUrl?: string; + uploadURLBack?: string; + uploadURLFront: string; + validateLivenessToken?: string; + }; + ErrorResponse: { + /** @description HTTP status code returned by the API error handler. */ + code?: number; + /** @description Validation error details, when the request fails schema or input validation. */ + errors?: Record[]; + /** @description A human-readable error message. */ + message?: string; + /** @description HTTP status code included by selected provider-style error responses. */ + statusCode?: number; + /** @description Provider-style error category, when available. */ + type?: string; + }; + /** @enum {string} */ + FiatToken: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP"; + GetKycStatusResponse: { + /** @enum {string} */ + failureReason?: "face" | "name" | "birthdate" | "unknown" | "tax_id"; + /** @description The KYC level achieved. */ + level: string; + /** @enum {string} */ + result?: "APPROVED" | "REJECTED"; + /** + * @description The KYC status. + * @enum {string} + */ + status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; + /** + * @description Event type, typically "KYC". + * @enum {string} + */ + type: "KYC"; + }; + GetRampErrorLogsResponse: components["schemas"]["RampErrorLog"][]; GetRampHistoryResponse: { totalCount: string; transactions: components["schemas"]["GetRampHistoryTransaction"]; }; - GetRampHistoryTransaction: { - currentPhase: components["schemas"]["RampPhase"]; - date: string; - /** @description The deadline for starting an initial ramp. */ - expiresAt: string; - /** @description A link to the transaction explorer of the blockchain showing the details of the transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ - externalTxExplorerLink?: string; - /** @description The hash of the blockchain transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ - externalTxHash?: string; - from: components["schemas"]["DestinationType"]; - fromAmount: string; - fromCurrency: components["schemas"]["RampCurrency"]; - id: string; - status: components["schemas"]["SimpleStatus"]; - to: components["schemas"]["DestinationType"]; - toAmount: string; - toCurrency: components["schemas"]["RampCurrency"]; - type: components["schemas"]["RampDirection"]; - /** @description Destination address for a BUY ramp when available. */ - walletAddress?: string; + GetRampHistoryTransaction: { + currentPhase: components["schemas"]["RampPhase"]; + date: string; + /** @description The deadline for starting an initial ramp. */ + expiresAt: string; + /** @description A link to the transaction explorer of the blockchain showing the details of the transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ + externalTxExplorerLink?: string; + /** @description The hash of the blockchain transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ + externalTxHash?: string; + from: components["schemas"]["DestinationType"]; + fromAmount: string; + fromCurrency: components["schemas"]["RampCurrency"]; + id: string; + status: components["schemas"]["SimpleStatus"]; + to: components["schemas"]["DestinationType"]; + toAmount: string; + toCurrency: components["schemas"]["RampCurrency"]; + type: components["schemas"]["RampDirection"]; + /** @description Destination address for a BUY ramp when available. */ + walletAddress?: string; + }; + GetUserLimitsRequest: { + corridors: ("AR" | "BR" | "CO" | "MX" | "US")[]; + }; + GetUserLimitsResponse: { + limits: components["schemas"]["UserLimit"][]; + }; + GetUserRemainingLimitResponse: { + /** + * Format: double + * @description The remaining limit for the requested direction. + */ + remainingLimit: number; + }; + GetUserResponse: { + /** @description The user's EVM wallet address. */ + evmAddress: string; + /** @description The user's KYC level. */ + kycLevel: number; + /** @enum {string} */ + identityStatus: "NOT-IDENTIFIED" | "CONFIRMED"; + subAccountId: string; + }; + GetWidgetUrlLocked: { + /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ + callbackUrl?: string; + /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ + externalSessionId?: string; + /** @description Pass the ID of an existing quote to make the widget lock in that particular quote without allowing to change it. */ + quoteId: string; + /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ + walletAddressLocked?: string; + }; + GetWidgetUrlRefresh: { + /** @description Your api key, if available. This is passed to all the quotes generated in this widget session. */ + apiKey?: string; + /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ + callbackUrl?: string; + countryCode?: components["schemas"]["CountryCode"]; + cryptoLocked?: components["schemas"]["OnChainToken"]; + /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ + externalSessionId: string; + fiat?: components["schemas"]["FiatToken"]; + inputAmount: string; + network: components["schemas"]["Networks"]; + /** @description The identifier of a partner. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + rampType: components["schemas"]["RampDirection"]; + /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ + walletAddressLocked?: string; + }; + KYCDataUploadFileFiles: { + /** Format: url */ + CNHUploadUrl?: string; + /** Format: url */ + RGBackUploadUrl?: string; + /** Format: url */ + RGFrontUploadUrl?: string; + /** Format: url */ + selfieUploadUrl?: string; + }; + /** @enum {string} */ + KYCDocType: "RG" | "CNH"; + KycLevel1Payload: { + city: string; + country: string; + countryOfTaxId: string; + /** @description ISO date (YYYY-MM-DD). */ + dateOfBirth: string; + /** Format: email */ + email: string; + fullName: string; + state: string; + streetAddress: string; + subAccountId: string; + taxIdNumber: string; + uploadedDocumentId: string; + uploadedSelfieId: string; + zipCode: string; + }; + KycLevel1Response: { + id: string; + }; + ListApiCredentialsResponse: { + credentials: components["schemas"]["ApiCredential"][]; + }; + /** + * @description Supported blockchain networks. + * @enum {string} + */ + Networks: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam"; + /** @enum {string} */ + OnChainToken: "USDC" | "USDT" | "ETH" | "USDC.E"; + /** @description Data related to the payment for the ramp transaction. */ + PaymentData: { + /** + * @description The amount for the payment. + * @example 0.05 + */ + amount?: string; + /** + * @description The target account for an anchor operation. + * @example GDSDQLBVDD5RZYKNDM2LAX5JDNNQOTSZOKECUYEXYMUZMAPXTMDUJCVF + */ + anchorTargetAccount?: string; + /** + * @description The memo content. + * @example 1204asjfnaksf10982e4 + */ + memo?: string; + /** + * @description Type of memo (e.g., text, id). + * @example text + */ + memoType?: string; + }; + /** @description `PIX`, `SEPA`, `CBU` */ + PaymentMethod: string; + /** @description Represents a transaction that has been presigned. Based on UnsignedTx structure. */ + PresignedTx: { + /** @description Any additional metadata associated with the transaction. Can be an empty object. */ + meta?: { + [key: string]: unknown; + }; + /** + * Format: int64 + * @description Nonce for the transaction, if applicable. + */ + nonce?: number; + /** + * @description The phase this transaction belongs to within the ramp logic. + * @enum {string} + */ + phase?: "RampPhase" | "CleanupPhase"; + /** @description Address of the account that signed/will sign this transaction. */ + signer?: string; + /** + * @description The presigned transaction payload or relevant data. + * @example AAAAAKg... + */ + txData?: string; + } & { + [key: string]: unknown; + }; + QuoteResponse: { + anchorFeeFiat: string; + anchorFeeUSD: string; + /** + * Format: date-time + * @description The timestamp when this quote expires. + */ + expiresAt?: string; + feeCurrency: components["schemas"]["RampCurrency"]; + from?: components["schemas"]["DestinationType"]; + /** + * Format: uuid + * @description Unique identifier for the quote. + */ + id?: string; + /** @description The input amount specified in the request. */ + inputAmount?: string; + inputCurrency?: components["schemas"]["RampCurrency"]; + networkFeeFiat: string; + networkFeeUSD: string; + /** @description The calculated output amount after fees and conversions. */ + outputAmount?: string; + outputCurrency?: components["schemas"]["RampCurrency"]; + partnerFeeFiat: string; + partnerFeeUSD: string; + processingFeeFiat: string; + processingFeeUSD: string; + /** @description The type of ramp process. */ + rampType?: components["schemas"]["RampDirection"]; + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + }; + /** + * @description Represents supported currencies for ramp operations, including fiat and on-chain tokens. + * @example USDC + * @enum {string} + */ + RampCurrency: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP" | "USDC" | "USDT" | "USDC.E"; + /** @enum {string} */ + RampDirection: "BUY" | "SELL"; + RampErrorLog: { + details?: string; + error: string; + phase: components["schemas"]["RampPhase"]; + recoverable?: boolean; + /** Format: date-time */ + timestamp: string; + }; + RampInfoResponse: { + /** @description Sanitized eligibility keyed by corridor country code. No exact limits, PII, provider IDs, or failure reasons are returned. */ + corridors: { + [key: string]: { + canBuy: boolean; + canSell: boolean; + /** @enum {string} */ + kycStatus: "not_started" | "pending" | "approved" | "rejected"; + }; + }; + }; + /** + * @description The current phase of the ramp process. + * @enum {string} + */ + RampPhase: "initial" | "timedOut" | "stellarCreateAccount" | "squidrouterApprove" | "squidrouterSwap" | "fundEphemeral" | "nablaApprove" | "nablaSwap" | "moonbeamToPendulum" | "moonbeamToPendulumXcm" | "pendulumToMoonbeam" | "assethubToPendulum" | "pendulumToAssethub" | "spacewalkRedeem" | "stellarPayment" | "subsidizePreSwap" | "subsidizePostSwap" | "brlaTeleport" | "onHoldForComplianceCheck" | "brlaPayoutOnMoonbeam" | "failed"; + RampProcess: { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; + }; + RegisterRampRequest: { + /** + * @description Optional additional data for the ramp process. + * + * For Brazil onramps, destinationAddress is required. + * + * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. + */ + additionalData?: { + /** @description Destination address, used for onramp. */ + destinationAddress?: string; + /** @description Auth token obtained from Monerium's API, for the current user. Only required for Monerium-related ramps. */ + moneriumAuthToken: string; + paymentData?: components["schemas"]["PaymentData"]; + /** @description PIX key for the destination account in an onramp. */ + pixDestination?: string; + /** @description Tax ID of the receiver for onramp. */ + receiverTaxId?: string; + /** @description Tax ID of the user. */ + taxId?: string; + /** @description Wallet address initiating the offramp. */ + walletAddress: string; + } & { + [key: string]: unknown; + }; + /** + * Format: uuid + * @description The unique identifier for the quote. + */ + quoteId: string; + /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ + signingAccounts: { + /** @description The account address. */ + address: string; + /** + * @description The type of the account. + * @enum {string} + */ + type: "EVM" | "Substrate"; + }[]; + }; + /** @description `PENDING`, `FAILED`, `COMPLETED` */ + SimpleStatus: string; + StartKYC2Request: { + documentType: components["schemas"]["KYCDocType"]; + taxId: string; + }; + StartKYC2Response: { + uploadUrls?: components["schemas"]["KYCDataUploadFileFiles"]; + }; + StartRampRequest: { + rampId: string; + }; + /** @enum {string} */ + TaxIdType: "CPF" | "CNPJ"; + TriggerOfframpRequest: { + /** + * @description The amount to offramp. + * @example 100.50 + */ + amount: string; + /** @description The recipient's PIX key. */ + pixKey: string; + /** @description The recipient's Tax ID for validation. */ + receiverTaxId: string; + /** @description The sender's Tax ID. */ + taxId: string; + }; + TriggerOfframpResponse: { + /** @description The ID of the triggered offramp transaction. */ + offrampId?: string; + }; + /** @description Represents an unsigned transaction that requires user signature. Actual properties will depend on the transaction type and network. */ + UnsignedTx: { + meta?: Record; + nonce?: number; + /** @enum {string} */ + phase?: "RampPhase" | "CleanupPhase"; + signer?: string; + /** + * @description The unsigned transaction payload or relevant data. + * @example AAAAAKu... + */ + txData?: string; + } & { + [key: string]: unknown; + }; + UpdateRampRequest: { + /** @description Optional additional data, like transaction hashes from external services. */ + additionalData?: ({ + /** @description Transaction hash for AssetHub to Pendulum transfer, if applicable. */ + assetHubToPendulumHash?: string | null; + /** @description Signed message to trigger a Monerium offramp. */ + moneriumOfframpSignature: string; + /** @description Transaction hash for Squid Router approval. Optional: omit when the wallet already holds a sufficient allowance and no approval transaction was submitted. */ + squidRouterApproveHash?: string | null; + /** @description Transaction hash for Squid Router swap, if applicable. */ + squidRouterSwapHash?: string | null; + } & { + [key: string]: unknown; + }) | null; + /** @description An array of transactions that have been pre-signed by the user. */ + presignedTxs: components["schemas"]["PresignedTx"][]; + /** + * @description The unique identifier of the ramp process to start. + * @example proc_12345 + */ + rampId: string; + }; + UserLimit: { + /** @enum {string} */ + corridor: "AR" | "BR" | "CO" | "MX" | "US"; + currency: components["schemas"]["RampCurrency"]; + direction: components["schemas"]["RampDirection"]; + /** @description Maximum amount in the returned currency's human units. */ + max: string; + period: components["schemas"]["UserLimitPeriod"]; + /** @description Amount consumed during the period in the returned currency's human units. */ + used: string; + }; + UserLimitPeriod: { + /** + * Format: date-time + * @description Exclusive end of the reported period. + */ + endsAt: string; + /** Format: date-time */ + startsAt: string; + /** @constant */ + type: "calendar_month"; + }; + /** @enum {string} */ + AlfredpayCountry: "AR" | "CO" | "MX" | "US"; + /** @enum {string} */ + AlfredpayCustomerType: "INDIVIDUAL" | "BUSINESS"; + /** @enum {string} */ + AlfredpayStatus: "CONSULTED" | "LINK_OPENED" | "USER_COMPLETED" | "VERIFYING" | "FAILED" | "SUCCESS" | "UPDATE_REQUIRED"; + AlfredpayCreateCustomerRequest: { + country: components["schemas"]["AlfredpayCountry"]; + }; + AlfredpayCreateCustomerResponse: { + /** Format: date-time */ + createdAt: string; + }; + AlfredpayRedirectLinkResponse: { + submissionId: string; + /** Format: uri */ + verification_url: string; + }; + AlfredpayRedirectNotificationRequest: { + country: components["schemas"]["AlfredpayCountry"]; + type?: components["schemas"]["AlfredpayCustomerType"]; + }; + AlfredpayStatusResponse: { + country: components["schemas"]["AlfredpayCountry"]; + /** Format: date-time */ + creationTime: string; + status: components["schemas"]["AlfredpayStatus"]; + }; + AlfredpayKycStatusResponse: { + alfred_pay_id: string; + country: components["schemas"]["AlfredpayCountry"]; + lastFailure?: string; + status: components["schemas"]["AlfredpayStatus"]; + /** Format: date-time */ + updated_at: string; + }; + AlfredpayRetryRequest: components["schemas"]["AlfredpayRedirectNotificationRequest"]; + AlfredpayRetryResponse: components["schemas"]["AlfredpayRedirectLinkResponse"] | components["schemas"]["SuccessResponse"]; + SubmitKycInformationRequest: { + address: string; + city: string; + /** @enum {string} */ + country: "AR" | "CO" | "MX"; + countryCode?: string; + cuit?: string; + /** Format: date */ + dateOfBirth: string; + dni: string; + /** Format: email */ + email?: string; + firstName: string; + lastName: string; + nationalities?: string[]; + pep?: boolean; + phoneNumber?: string; + state: string; + typeDocument?: string; + /** @enum {string} */ + typeDocumentAr?: "DNI"; + /** @enum {string} */ + typeDocumentCol?: "CC" | "CE"; + zipCode: string; + } & ({ + /** @constant */ + country?: "MX"; + } | { + /** @constant */ + country?: "CO"; + } | { + /** @constant */ + country?: "AR"; + }); + SubmitInformationResponse: { + submissionId: string; + }; + AlfredpayKycFileUploadRequest: { + /** @enum {string} */ + country: "AR" | "CO" | "MX"; + /** Format: binary */ + file: string; + /** @enum {string} */ + fileType: "National ID Front" | "National ID Back" | "Selfie"; + submissionId: string; + }; + AlfredpaySendSubmissionRequest: { + /** @enum {string} */ + country: "AR" | "CO" | "MX"; + submissionId: string; + }; + AlfredpayKybRelatedPerson: { + cpf?: string; + /** Format: date */ + dateOfBirth: string; + dni?: string; + /** Format: email */ + email: string; + firstName: string; + lastName: string; + nationalities: string[]; + pep?: boolean; + }; + SubmitKybInformationRequest: { + accountPurpose: string; + address: string; + businessActivities: string; + businessName: string; + city: string; + complianceScreeningDescription?: string; + conductsComplianceScreening?: boolean; + /** @enum {string} */ + country: "CO" | "MX"; + expectedMonthlyTransactions: number; + expectedMonthlyVolumeUsd: number; + isRegulatedBusiness: boolean; + operatesInSanctionedCountries: boolean; + relatedPersons: components["schemas"]["AlfredpayKybRelatedPerson"][]; + sourceOfFunds: string; + state: string; + taxId: string; + transmitsCustomerFunds: boolean; + walletAddresses: string; + /** Format: uri */ + website: string; + zipCode: string; + } & (unknown & unknown); + AlfredpayKybFileUploadRequest: { + /** @enum {string} */ + country: "CO" | "MX"; + /** Format: binary */ + file: string; + /** @enum {string} */ + fileType: "taxIdDocument" | "articlesIncorporation" | "proofAddress" | "shareholderRegistry" | "businessLicense" | "uploadAmlPolicy"; + submissionId: string; + }; + AlfredpayKybRelatedPersonFileUploadRequest: { + /** @enum {string} */ + country: "CO" | "MX"; + /** Format: binary */ + file: string; + /** @enum {string} */ + fileType: "docFront" | "docBack"; + relatedPersonId: string; + }; + AlfredpayKybDetailsResponse: { + relatedPersons: { + idRelatedPerson: string; + }[]; + submissionId: string; + }[]; + SuccessResponse: { + /** @constant */ + success: true; + }; + OnboardingApiErrorResponse: { + error: string | { + code: string; + message: string; + status: number; + }; + }; + AveniaKybDocumentRequest: { + documentType: components["schemas"]["AveniaDocumentType"]; + isDoubleSided?: boolean; + }; + AveniaKybDocumentUploadResponse: { + id: string; + /** Format: uri */ + livenessUrl?: string; + /** Format: uri */ + uploadURLBack?: string; + /** Format: uri */ + uploadURLFront: string; + validateLivenessToken?: string; + }; + AveniaKybDocumentResponse: { + document: { + documentType: components["schemas"]["AveniaDocumentType"]; + id: string; + ready: boolean; + uploadErrorBack?: string; + uploadErrorFront?: string; + uploadStatusBack?: string; + uploadStatusFront: string; + }; + }; + /** @enum {string} */ + AveniaUboControlRole: "CEO" | "CFO" | "COO" | "CTO" | "President" | "Vice President" | "Director" | "Managing Director" | "Managing Partner" | "General Partner" | "Partner" | "Secretary" | "Treasurer" | "Chairman" | "Board Member" | "Authorized Signatory" | "General Counsel" | "Owner" | "Founder" | "Manager" | "Member" | "Comptroller" | "Chief Compliance Officer"; + AveniaUboPayload: { + city: string; + country: string; + countryOfTaxId: string; + /** Format: date */ + dateOfBirth: string; + documentCountry: string; + /** Format: email */ + email?: string; + fullName: string; + hasControl?: components["schemas"]["AveniaUboControlRole"]; + percentageOfOwnership: string; + phone?: string; + state: string; + streetLine1: string; + streetLine2?: string; + streetLine3?: string; + taxIdNumber: string; + uploadedIdentificationId: string; + uploadedSelfieId?: string; + zipCode: string; + }; + AveniaUboResponse: { + id: string; + }; + AveniaKybLevel1Payload: { + businessActivityDescription: string; + certificateOfIncorporationDocumentId: string; + companyCity: string; + companyCountry: string; + companyLegalName: string; + companyRegistrationNumber: string; + companyState: string; + companyStreetLine1: string; + companyStreetLine2?: string; + companyStreetLine3?: string; + companyZipCode: string; + countrySubdivisionTaxResidence?: string; + countryTaxResidence: string; + /** Format: email */ + emailPixKey?: string; + /** @enum {string} */ + estimatedAnnualRevenueUsd: "less_than_100k" | "100k_to_1m" | "1m_to_10m" | "10m_to_50m" | "50m_to_100m" | "more_than_100m"; + estimatedMonthlyVolumeUsd: string; + /** @enum {string} */ + numberOfEmployees: "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001+"; + /** @enum {string} */ + reasonForAccountOpening: "charitable_donations" | "ecommerce_retail_payments" | "investment_purposes" | "other" | "payments_to_friends_or_family_abroad" | "payroll" | "personal_or_living_expenses" | "protect_wealth" | "purchase_goods_and_services" | "receive_payments_for_goods_and_services" | "tax_optimization" | "third_party_money_transmission" | "treasury_management"; + sandboxReject?: boolean; + /** Format: uri */ + socialMedia?: string; + /** @enum {string} */ + sourceOfFundsAndIncome: "business_loans" | "grants" | "inter_company_funds" | "investment_proceeds" | "legal_settlement" | "owners_capital" | "pension_retirement" | "sale_of_assets" | "sales_of_goods_and_services" | "third_party_funds" | "treasury_reserves"; + taxIdentificationDocumentId: string; + taxIdentificationNumberTin: string; + uboIds: string[]; + /** Format: uri */ + website?: string; + }; + AveniaKybHostedResponse: { + attemptId: string; + /** Format: uri */ + authorizedRepresentativeUrl: string; + /** Format: uri */ + basicCompanyDataUrl: string; + }; + AveniaKybAttemptStatusResponse: { + failureReason?: string; + /** @enum {string} */ + result?: "APPROVED" | "REJECTED"; + retryable?: boolean; + /** @enum {string} */ + status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; + }; + RecordInitialKycAttemptRequest: { + quoteId?: string; + sessionId?: string; + taxId: string; + }; + OnboardingRequirementField: { + allowedValues?: string[]; + description?: string; + format?: string; + path: string; + required: boolean; + requiredWhen?: string; + /** @enum {string} */ + type: "array" | "boolean" | "number" | "string"; + }; + OnboardingDocumentRequirement: { + acceptedMediaTypes?: string[]; + /** @enum {string} */ + collection?: "direct-upload" | "hosted"; + description?: string; + required: boolean; + requiredWhen?: string; + type: string; + }; + OnboardingRequirementStep: { + condition?: string; + description: string; + derivedValues?: { + [key: string]: string; + }; + fixedBody?: { + [key: string]: string; + }; + fixedQuery?: { + [key: string]: string; + }; + /** @enum {string} */ + kind: "api" | "direct-upload" | "hosted"; + /** @enum {string} */ + method?: "GET" | "POST" | "PUT"; + operationId?: string; + order: number; + path?: string; + repeatFor?: string; + requestSchema?: string; + }; + OnboardingRequirementsResponse: { + /** @enum {string} */ + country: "AR" | "BR" | "CO" | "MX" | "US"; + /** @enum {string} */ + customerType: "individual" | "business"; + /** Format: uri */ + documentationUrl: string; + documents: components["schemas"]["OnboardingDocumentRequirement"][]; + fields: components["schemas"]["OnboardingRequirementField"][]; + flow: string; + /** @enum {string} */ + mode: "api" | "hosted" | "hybrid"; + /** Format: uri */ + openapiUrl: string; + /** @enum {string} */ + provider: "alfredpay" | "avenia"; + requirementsVersion: string; + steps: components["schemas"]["OnboardingRequirementStep"][]; + }; + OnboardingRequirementsErrorResponse: { + error: { + /** @enum {string} */ + code: "INVALID_ONBOARDING_REQUIREMENTS_QUERY" | "ONBOARDING_REQUIREMENTS_NOT_FOUND"; + message: string; + /** @enum {integer} */ + status: 400 | 404; + }; + }; + OnboardingStatusResponse: { + activeEntityId: string | null; + entities: { + accounts: { + companyName: string | null; + country: string | null; + /** @enum {string} */ + customerType: "individual" | "business"; + error: null | { + code: string; + message: string; + }; + id: string; + kycCase: null | { + /** Format: date-time */ + approvedAt: string | null; + failureReasons: string[] | null; + level: string | null; + /** Format: date-time */ + rejectedAt: string | null; + /** @enum {string} */ + status: "pending" | "started" | "in_review" | "approved" | "rejected"; + statusExternal: string | null; + /** Format: date-time */ + submittedAt: string | null; + /** @enum {string} */ + type: "kyc" | "kyb"; + }; + /** @enum {string} */ + provider: "alfredpay" | "avenia" | "monerium" | "mykobo"; + rail: string | null; + /** @enum {string} */ + state: "pending" | "started" | "in_review" | "approved" | "rejected"; + /** @enum {string} */ + status: "pending" | "started" | "in_review" | "approved" | "rejected"; + statusExternal: string | null; + /** @description Business tax ID only; individual tax IDs remain private. */ + taxReference: string | null; + }[]; + id: string; + /** @enum {string} */ + status: "active" | "archived" | "blocked"; + /** @enum {string} */ + type: "individual" | "business"; + }[]; + roles: string[]; + selectionRequired: boolean; + }; + SelectActiveCustomerEntityRequest: { + /** @enum {string} */ + type: "individual" | "business"; + }; + SelectActiveCustomerEntityResponse: { + activeEntityId: string; + /** @enum {string} */ + type: "individual" | "business"; + }; + ValidatePixKeyResponse: { + /** @description Indicates if the PIX key is valid. */ + valid?: boolean; + }; + }; + responses: { + InvalidInput: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + code: number; + message: string; + }; + }; + }; + RecordNotFound: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + code: number; + message: string; + }; + }; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + listApiCredentials: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Credentials, newest first, including revoked and expired lifecycle records. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListApiCredentialsResponse"]; + }; + }; + /** @description Missing or invalid Bearer token. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + }; + }; + createApiCredential: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["CreateApiCredentialRequest"]; + }; + }; + responses: { + /** @description Credential created. Persist `secretKey` immediately; it cannot be retrieved again. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateApiCredentialResponse"]; + }; + }; + /** @description `INVALID_CREDENTIAL_EXPIRY` or `INVALID_CREDENTIAL_NAME`. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description Missing or invalid Bearer token. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description `CREDENTIAL_LIMIT_REACHED`: the profile already holds five active non-expired credentials. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + }; + }; + revokeApiCredential: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Immutable credential ID to revoke. */ + credentialId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Credential revoked; both values are immediately unusable. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Missing or invalid Bearer token. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description `CREDENTIAL_NOT_FOUND`: credential is missing, already revoked, partner-managed, or not owned by the profile. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + }; + }; + requestOTP: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "email": "user@example.com" + * } + */ + "application/json": { + /** Format: email */ + email: string; + /** @description Optional locale for the email, e.g. `pt-BR`. */ + locale?: string; + }; + }; + }; + responses: { + /** @description OTP sent. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message: string; + success: boolean; + }; + }; + }; + /** @description Email missing or locale not a string. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + /** @description Failed to send the OTP email. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; + verifyOTP: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "email": "user@example.com", + * "token": "123456" + * } + */ + "application/json": { + /** Format: email */ + email: string; + /** @description The 6-digit code from the email. */ + token: string; + }; + }; + }; + responses: { + /** @description Session created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + access_token: string; + refresh_token: string; + success: boolean; + /** Format: uuid */ + user_id: string; + }; + }; + }; + /** @description Missing fields, or the OTP is invalid or expired. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; + createSubaccount: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["CreateSubaccountRequest"]; + }; + }; + responses: { + /** @description Subaccount created or KYC retry initiated successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateSubaccountResponse"]; + }; + }; + /** + * @description Bad Request. Possible reasons: + * - Missing required fields (cpf, cnpj, companyName, startDate) + * - Subaccount already created and KYC level > 0 + * - Other invalid request details + */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal Server Error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + fetchSubaccountKycStatus: { + parameters: { + query: { + /** @description The user's Tax ID. */ + taxId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved KYC status. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetKycStatusResponse"]; + }; + }; + /** @description Missing taxId or subaccount not found (returned as 400 from code). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description No KYC process started. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal Server Error (e.g., no KYC events found when expected). */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + brlaGetSelfieLivenessUrl: { + parameters: { + query: { + /** @description CPF or CNPJ. */ + taxId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Liveness URL returned. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaGetSelfieLivenessUrlResponse"]; + }; + }; + /** @description Missing taxId or ramp disabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Supabase Bearer required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + brlaGetUploadUrls: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AveniaKYCDataUploadRequest"]; + }; + }; + responses: { + /** @description Upload URLs returned. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AveniaKYCDataUploadResponse"]; + }; + }; + /** @description Missing/invalid documentType or taxId; or ramp disabled for this tax ID. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + getBrlaUser: { + parameters: { + query?: { + /** @description The user's Tax ID. */ + taxId?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved user information. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetUserResponse"]; + }; + }; + /** + * @description Bad Request. Possible reasons: + * - Missing taxId query parameter + * - KYC invalid + */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Subaccount not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal Server Error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + getBrlaUserRemainingLimit: { + parameters: { + query: { + /** @description The user's Tax ID. */ + taxId?: string; + /** @description Ramp direction whose remaining limit should be returned. */ + direction: "BUY" | "SELL"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successfully retrieved user's remaining limits. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetUserRemainingLimitResponse"]; + }; + }; + /** @description Missing taxId query parameter or other invalid request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Subaccount not found or limits not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal Server Error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + brlaNewKyc: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["KycLevel1Payload"]; + }; + }; + responses: { + /** @description KYC submission accepted. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KycLevel1Response"]; + }; + }; + /** @description Validation failure. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + }; + }; + brlaValidatePixKey: { + parameters: { + query: { + /** @description Pix key to validate (CPF, CNPJ, email, phone, or random key). */ + pixKey: string; + }; + header?: never; + path?: never; + cookie?: never; }; - GetUserLimitsRequest: { - corridors: ("AR" | "BR" | "CO" | "MX" | "US")[]; + requestBody?: never; + responses: { + /** @description Validation result. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaValidatePixKeyResponse"]; + }; + }; + /** @description Missing or invalid pix key. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; }; - GetUserLimitsResponse: { - limits: components["schemas"]["UserLimit"][]; + }; + recordInitialAveniaKycAttempt: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - GetUserRemainingLimitResponse: { - /** - * Format: double - * @description The remaining limit for offramp operations. - */ - remainingLimitOfframp?: number; - /** - * Format: double - * @description The remaining limit for onramp operations. - */ - remainingLimitOnramp?: number; + requestBody: { + content: { + "application/json": components["schemas"]["RecordInitialKycAttemptRequest"]; + }; }; - GetUserResponse: { - /** @description The user's EVM wallet address. */ - evmAddress?: string; - /** - * @description The user's KYC level. - * @enum {number} - */ - kycLevel?: 1 | 2; + responses: { + /** @description Attempt recorded. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + /** @description Invalid request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - GetWidgetUrlLocked: { - /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ - callbackUrl?: string; - /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ - externalSessionId?: string; - /** @description Pass the ID of an existing quote to make the widget lock in that particular quote without allowing to change it. */ - quoteId: string; - /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ - walletAddressLocked?: string; + }; + startAveniaKybLevel1Hosted: { + parameters: { + query: { + subAccountId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Hosted KYB URLs created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AveniaKybHostedResponse"]; + }; + }; + /** @description Invalid company account or KYB state. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Subaccount not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description KYB is already active, approved, or conflicts with an API submission. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + createAveniaKybDocument: { + parameters: { + query: { + subAccountId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AveniaKybDocumentRequest"]; + }; + }; + responses: { + /** @description Document upload targets created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AveniaKybDocumentUploadResponse"]; + }; + }; + /** @description Invalid document request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Subaccount not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getAveniaKybDocument: { + parameters: { + query: { + subAccountId: string; + }; + header?: never; + path: { + documentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Document status. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AveniaKybDocumentResponse"]; + }; + }; + /** @description Invalid document identifier. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Document does not belong to the effective profile. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Document not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - GetWidgetUrlRefresh: { - /** @description Your api key, if available. This is passed to all the quotes generated in this widget session. */ - apiKey?: string; - /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ - callbackUrl?: string; - countryCode?: components["schemas"]["CountryCode"]; - cryptoLocked?: components["schemas"]["OnChainToken"]; - /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ - externalSessionId: string; - fiat?: components["schemas"]["FiatToken"]; - inputAmount: string; - network: components["schemas"]["Networks"]; - /** @description The identifier of a partner. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - rampType: components["schemas"]["RampDirection"]; - /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ - walletAddressLocked?: string; + }; + createAveniaKybUbo: { + parameters: { + query: { + subAccountId: string; + }; + header?: never; + path?: never; + cookie?: never; }; - KYCDataUploadFileFiles: { - /** Format: url */ - CNHUploadUrl?: string; - /** Format: url */ - RGBackUploadUrl?: string; - /** Format: url */ - RGFrontUploadUrl?: string; - /** Format: url */ - selfieUploadUrl?: string; + requestBody: { + content: { + "application/json": components["schemas"]["AveniaUboPayload"]; + }; }; - /** @enum {string} */ - KYCDocType: "RG" | "CNH"; - KycLevel1Payload: { - city: string; - country: string; - countryOfTaxId: string; - /** @description ISO date (YYYY-MM-DD). */ - dateOfBirth: string; - /** Format: email */ - email: string; - fullName: string; - state: string; - streetAddress: string; - subAccountId: string; - taxIdNumber: string; - uploadedDocumentId: string; - uploadedSelfieId: string; - zipCode: string; + responses: { + /** @description UBO registered. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AveniaUboResponse"]; + }; + }; + /** @description Invalid UBO or document state. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Subaccount or referenced document not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description A referenced document is not ready. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - KycLevel1Response: { - id: string; + }; + submitAveniaKybLevel1Api: { + parameters: { + query: { + subAccountId: string; + }; + header?: never; + path?: never; + cookie?: never; }; - ListApiCredentialsResponse: { - credentials: components["schemas"]["ApiCredential"][]; + requestBody: { + content: { + "application/json": components["schemas"]["AveniaKybLevel1Payload"]; + }; }; - /** - * @description Supported blockchain networks. - * @enum {string} - */ - Networks: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam"; - /** @enum {string} */ - OnChainToken: "USDC" | "USDT" | "ETH" | "USDC.E"; - /** @description Data related to the payment for the ramp transaction. */ - PaymentData: { - /** - * @description The amount for the payment. - * @example 0.05 - */ - amount?: string; - /** - * @description The target account for an anchor operation. - * @example GDSDQLBVDD5RZYKNDM2LAX5JDNNQOTSZOKECUYEXYMUZMAPXTMDUJCVF - */ - anchorTargetAccount?: string; - /** - * @description The memo content. - * @example 1204asjfnaksf10982e4 - */ - memo?: string; - /** - * @description Type of memo (e.g., text, id). - * @example text - */ - memoType?: string; + responses: { + /** @description KYB attempt submitted. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KycLevel1Response"]; + }; + }; + /** @description Invalid submission or document state. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Subaccount or referenced document not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description A different KYB submission is already in progress. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @description `PIX`, `SEPA`, `CBU` */ - PaymentMethod: string; - /** @description Represents a transaction that has been presigned. Based on UnsignedTx structure. */ - PresignedTx: { - /** @description Any additional metadata associated with the transaction. Can be an empty object. */ - meta?: { - [key: string]: unknown; + }; + getAveniaKybAttemptStatus: { + parameters: { + query: { + attemptId: string; }; - /** - * Format: int64 - * @description Nonce for the transaction, if applicable. - */ - nonce?: number; - /** - * @description The phase this transaction belongs to within the ramp logic. - * @enum {string} - */ - phase?: "RampPhase" | "CleanupPhase"; - /** @description Address of the account that signed/will sign this transaction. */ - signer?: string; - /** - * @description The presigned transaction payload or relevant data. - * @example AAAAAKg... - */ - txData?: string; - } & { - [key: string]: unknown; + header?: never; + path?: never; + cookie?: never; }; - QuoteResponse: { - anchorFeeFiat: string; - anchorFeeUSD: string; - /** - * Format: date-time - * @description The timestamp when this quote expires. - */ - expiresAt?: string; - feeCurrency: components["schemas"]["RampCurrency"]; - from?: components["schemas"]["DestinationType"]; - /** - * Format: uuid - * @description Unique identifier for the quote. - */ - id?: string; - /** @description The input amount specified in the request. */ - inputAmount?: string; - inputCurrency?: components["schemas"]["RampCurrency"]; - networkFeeFiat: string; - networkFeeUSD: string; - /** @description The calculated output amount after fees and conversions. */ - outputAmount?: string; - outputCurrency?: components["schemas"]["RampCurrency"]; - partnerFeeFiat: string; - partnerFeeUSD: string; - processingFeeFiat: string; - processingFeeUSD: string; - /** @description The type of ramp process. */ - rampType?: components["schemas"]["RampDirection"]; - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - vortexFeeFiat: string; - vortexFeeUSD: string; + requestBody?: never; + responses: { + /** @description KYB attempt status. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AveniaKybAttemptStatusResponse"]; + }; + }; + /** @description Invalid attempt identifier. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Attempt does not belong to the effective profile. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Attempt not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description The attempt is no longer the current bound KYB attempt. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** - * @description Represents supported currencies for ramp operations, including fiat and on-chain tokens. - * @example USDC - * @enum {string} - */ - RampCurrency: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP" | "USDC" | "USDT" | "USDC.E"; - /** @enum {string} */ - RampDirection: "BUY" | "SELL"; - RampErrorLog: { - details?: string; - error: string; - phase: components["schemas"]["RampPhase"]; - recoverable?: boolean; - /** Format: date-time */ - timestamp: string; + }; + getAlfredpayStatus: { + parameters: { + query: { + country: components["schemas"]["AlfredpayCountry"]; + /** @description Selects the individual or business customer. When omitted, the active customer entity is used for backward compatibility. */ + type?: components["schemas"]["AlfredpayCustomerType"]; + }; + header?: never; + path?: never; + cookie?: never; }; - RampInfoResponse: { - /** @description Sanitized eligibility keyed by corridor country code. No exact limits, PII, provider IDs, or failure reasons are returned. */ - corridors: { - [key: string]: { - canBuy: boolean; - canSell: boolean; - /** @enum {string} */ - kycStatus: "not_started" | "pending" | "approved" | "rejected"; + requestBody?: never; + responses: { + /** @description Current onboarding status. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayStatusResponse"]; + }; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No Alfredpay customer exists. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; - /** - * @description The current phase of the ramp process. - * @enum {string} - */ - RampPhase: "initial" | "timedOut" | "stellarCreateAccount" | "squidrouterApprove" | "squidrouterSwap" | "fundEphemeral" | "nablaApprove" | "nablaSwap" | "moonbeamToPendulum" | "moonbeamToPendulumXcm" | "pendulumToMoonbeam" | "assethubToPendulum" | "pendulumToAssethub" | "spacewalkRedeem" | "stellarPayment" | "subsidizePreSwap" | "subsidizePostSwap" | "brlaTeleport" | "onHoldForComplianceCheck" | "brlaPayoutOnMoonbeam" | "failed"; - RampProcess: { - anchorFeeFiat: string; - anchorFeeUSD: string; - countryCode?: components["schemas"]["CountryCode"]; - /** - * Format: date-time - * @description Timestamp of when the ramp process was created. - */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; - inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; + }; + createAlfredpayIndividualCustomer: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - RegisterRampRequest: { - /** - * @description Optional additional data for the ramp process. - * - * For Brazil onramps, destinationAddress is required. - * - * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. - */ - additionalData?: { - /** @description Destination address, used for onramp. */ - destinationAddress?: string; - /** @description Auth token obtained from Monerium's API, for the current user. Only required for Monerium-related ramps. */ - moneriumAuthToken: string; - paymentData?: components["schemas"]["PaymentData"]; - /** @description PIX key for the destination account in an onramp. */ - pixDestination?: string; - /** @description Tax ID of the receiver for onramp. */ - receiverTaxId?: string; - /** @description Tax ID of the user. */ - taxId?: string; - /** @description Wallet address initiating the offramp. */ - walletAddress: string; - } & { - [key: string]: unknown; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpayCreateCustomerRequest"]; + }; + }; + responses: { + /** @description Customer created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayCreateCustomerResponse"]; + }; + }; + /** @description Invalid country or customer state. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; }; - /** - * Format: uuid - * @description The unique identifier for the quote. - */ - quoteId: string; - /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ - signingAccounts: { - /** @description The account address. */ - address: string; - /** - * @description The type of the account. - * @enum {string} - */ - type: "EVM" | "Substrate"; - }[]; }; - /** @description `PENDING`, `FAILED`, `COMPLETED` */ - SimpleStatus: string; - StartKYC2Request: { - documentType: components["schemas"]["KYCDocType"]; - taxId: string; + }; + createAlfredpayBusinessCustomer: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - StartKYC2Response: { - uploadUrls?: components["schemas"]["KYCDataUploadFileFiles"]; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpayCreateCustomerRequest"]; + }; }; - StartRampRequest: { - rampId: string; + responses: { + /** @description Customer created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayCreateCustomerResponse"]; + }; + }; + /** @description Invalid country or customer state. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @enum {string} */ - TaxIdType: "CPF" | "CNPJ"; - TriggerOfframpRequest: { - /** - * @description The amount to offramp. - * @example 100.50 - */ - amount: string; - /** @description The recipient's PIX key. */ - pixKey: string; - /** @description The recipient's Tax ID for validation. */ - receiverTaxId: string; - /** @description The sender's Tax ID. */ - taxId: string; + }; + getAlfredpayKycRedirectLink: { + parameters: { + query: { + country: components["schemas"]["AlfredpayCountry"]; + }; + header?: never; + path?: never; + cookie?: never; }; - TriggerOfframpResponse: { - /** @description The ID of the triggered offramp transaction. */ - offrampId?: string; + requestBody?: never; + responses: { + /** @description Hosted verification URL. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayRedirectLinkResponse"]; + }; + }; + /** @description Verification is already in review or completed. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - /** @description Represents an unsigned transaction that requires user signature. Actual properties will depend on the transaction type and network. */ - UnsignedTx: { - meta?: Record; - nonce?: number; - /** @enum {string} */ - phase?: "RampPhase" | "CleanupPhase"; - signer?: string; - /** - * @description The unsigned transaction payload or relevant data. - * @example AAAAAKu... - */ - txData?: string; - } & { - [key: string]: unknown; + }; + getAlfredpayKybRedirectLink: { + parameters: { + query: { + country: components["schemas"]["AlfredpayCountry"]; + }; + header?: never; + path?: never; + cookie?: never; }; - UpdateRampRequest: { - /** @description Optional additional data, like transaction hashes from external services. */ - additionalData?: ({ - /** @description Transaction hash for AssetHub to Pendulum transfer, if applicable. */ - assetHubToPendulumHash?: string | null; - /** @description Signed message to trigger a Monerium offramp. */ - moneriumOfframpSignature: string; - /** @description Transaction hash for Squid Router approval. Optional: omit when the wallet already holds a sufficient allowance and no approval transaction was submitted. */ - squidRouterApproveHash?: string | null; - /** @description Transaction hash for Squid Router swap, if applicable. */ - squidRouterSwapHash?: string | null; - } & { - [key: string]: unknown; - }) | null; - /** @description An array of transactions that have been pre-signed by the user. */ - presignedTxs: components["schemas"]["PresignedTx"][]; - /** - * @description The unique identifier of the ramp process to start. - * @example proc_12345 - */ - rampId: string; + requestBody?: never; + responses: { + /** @description Hosted verification URL. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayRedirectLinkResponse"]; + }; + }; + /** @description Verification is already in review or completed. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - UserLimit: { - /** @enum {string} */ - corridor: "AR" | "BR" | "CO" | "MX" | "US"; - currency: components["schemas"]["RampCurrency"]; - direction: components["schemas"]["RampDirection"]; - /** @description Maximum amount in the returned currency's human units. */ - max: string; - period: components["schemas"]["UserLimitPeriod"]; - /** @description Amount consumed during the period in the returned currency's human units. */ - used: string; + }; + notifyAlfredpayKycRedirectOpened: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - UserLimitPeriod: { - /** - * Format: date-time - * @description Exclusive end of the reported period. - */ - endsAt: string; - /** Format: date-time */ - startsAt: string; - /** @constant */ - type: "calendar_month"; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpayRedirectNotificationRequest"]; + }; }; - ValidatePixKeyResponse: { - /** @description Indicates if the PIX key is valid. */ - valid?: boolean; + responses: { + /** @description Event recorded. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuccessResponse"]; + }; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; - responses: { - InvalidInput: { - headers: { - [name: string]: unknown; - }; + notifyAlfredpayKycRedirectFinished: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { content: { - "application/json": { - code: number; - message: string; - }; + "application/json": components["schemas"]["AlfredpayRedirectNotificationRequest"]; }; }; - RecordNotFound: { - headers: { - [name: string]: unknown; + responses: { + /** @description Event recorded. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuccessResponse"]; + }; }; - content: { - "application/json": { - code: number; - message: string; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - listApiCredentials: { + getAlfredpayKycStatus: { parameters: { - query?: never; + query: { + country: components["schemas"]["AlfredpayCountry"]; + type?: components["schemas"]["AlfredpayCustomerType"]; + }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Credentials, newest first, including revoked and expired lifecycle records. */ + /** @description Current KYC or KYB status. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ListApiCredentialsResponse"]; + "application/json": components["schemas"]["AlfredpayKycStatusResponse"]; }; }; - /** @description Missing or invalid Bearer token. */ + /** @description Authentication required. */ 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + content?: never; + }; + /** @description Customer or submission not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal server error. */ + /** @description Alfredpay request failed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; }; }; - createApiCredential: { + retryAlfredpayKyc: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - "application/json": components["schemas"]["CreateApiCredentialRequest"]; + "application/json": components["schemas"]["AlfredpayRetryRequest"]; }; }; responses: { - /** @description Credential created. Persist `secretKey` immediately; it cannot be retrieved again. */ - 201: { + /** @description Retry started. Hosted flows return a link; API flows return success. */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CreateApiCredentialResponse"]; + "application/json": components["schemas"]["AlfredpayRetryResponse"]; }; }; - /** @description `INVALID_CREDENTIAL_EXPIRY` or `INVALID_CREDENTIAL_NAME`. */ + /** @description No failed submission exists or the current status cannot be retried. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; - /** @description Missing or invalid Bearer token. */ + /** @description Authentication required. */ 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; - /** @description `CREDENTIAL_LIMIT_REACHED`: the profile already holds five active non-expired credentials. */ - 409: { + /** @description Customer not found. */ + 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; - /** @description Internal server error. */ + /** @description Alfredpay request failed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; }; }; - revokeApiCredential: { + submitAlfredpayKycInformation: { parameters: { query?: never; header?: never; - path: { - /** @description Immutable credential ID to revoke. */ - credentialId: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SubmitKycInformationRequest"]; + }; + }; responses: { - /** @description Credential revoked; both values are immediately unusable. */ - 204: { + /** @description Submission created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SubmitInformationResponse"]; + }; + }; + /** @description Invalid identity data. */ + 400: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Missing or invalid Bearer token. */ + /** @description Authentication required. */ 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description `CREDENTIAL_NOT_FOUND`: credential is missing, already revoked, partner-managed, or not owned by the profile. */ + /** @description Individual customer not found. */ 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; - /** @description Internal server error. */ + /** @description Alfredpay request failed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; - }; + content?: never; }; }; }; - requestOTP: { + submitAlfredpayKycFile: { parameters: { query?: never; header?: never; @@ -2015,57 +4349,57 @@ export interface operations { }; requestBody: { content: { - /** - * @example { - * "email": "user@example.com" - * } - */ - "application/json": { - /** Format: email */ - email: string; - /** @description Optional locale for the email, e.g. `pt-BR`. */ - locale?: string; - }; + "multipart/form-data": components["schemas"]["AlfredpayKycFileUploadRequest"]; }; }; responses: { - /** @description OTP sent. */ + /** @description File uploaded. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - message: string; - success: boolean; - }; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Email missing or locale not a string. */ + /** @description Invalid multipart request. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - error: string; - }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Failed to send the OTP email. */ - 500: { + /** @description Managed profile or corridor is not authorized. */ + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - error: string; - }; + content?: never; + }; + /** @description Individual customer not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; }; - verifyOTP: { + sendAlfredpayKycSubmission: { parameters: { query?: never; header?: never; @@ -2074,151 +4408,185 @@ export interface operations { }; requestBody: { content: { - /** - * @example { - * "email": "user@example.com", - * "token": "123456" - * } - */ - "application/json": { - /** Format: email */ - email: string; - /** @description The 6-digit code from the email. */ - token: string; - }; + "application/json": components["schemas"]["AlfredpaySendSubmissionRequest"]; }; }; responses: { - /** @description Session created. */ + /** @description Submission finalized. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - access_token: string; - refresh_token: string; - success: boolean; - /** Format: uuid */ - user_id: string; - }; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Missing fields, or the OTP is invalid or expired. */ + /** @description Invalid or incomplete submission. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - error: string; - }; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Individual customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - createSubaccount: { + submitAlfredpayKybInformation: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - "application/json": components["schemas"]["CreateSubaccountRequest"]; + "application/json": components["schemas"]["SubmitKybInformationRequest"]; }; }; responses: { - /** @description Subaccount created or KYC retry initiated successfully. */ + /** @description Submission created or updated. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CreateSubaccountResponse"]; + "application/json": components["schemas"]["SubmitInformationResponse"]; }; }; - /** - * @description Bad Request. Possible reasons: - * - Missing required fields (cpf, cnpj, companyName, startDate) - * - Subaccount already created and KYC level > 0 - * - Other invalid request details - */ + /** @description Invalid company, representative, or questionnaire data. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal Server Error. */ - 500: { + /** @description Managed profile or corridor is not authorized. */ + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Business customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description KYB is already in review or completed. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - fetchSubaccountKycStatus: { + submitAlfredpayKybFile: { parameters: { - query: { - /** @description The user's Tax ID. */ - taxId: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["AlfredpayKybFileUploadRequest"]; + }; + }; responses: { - /** @description Successfully retrieved KYC status. */ + /** @description File uploaded. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetKycStatusResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Missing taxId or subaccount not found (returned as 400 from code). */ + /** @description Invalid multipart request. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description No KYC process started. */ - 404: { + /** @description Managed profile or corridor is not authorized. */ + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Business customer not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal Server Error (e.g., no KYC events found when expected). */ + /** @description Alfredpay request failed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; }; }; - brlaGetSelfieLivenessUrl: { + findAlfredpayKybCustomerAndBusiness: { parameters: { query: { - /** @description CPF or CNPJ. */ - taxId: string; + country: "CO" | "MX"; }; header?: never; path?: never; @@ -2226,43 +4594,39 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Liveness URL returned. */ + /** @description Matching KYB submissions. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaGetSelfieLivenessUrlResponse"]; + "application/json": components["schemas"]["AlfredpayKybDetailsResponse"]; }; }; - /** @description Missing taxId or ramp disabled. */ - 400: { + /** @description Authentication required. */ + 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; - /** @description Supabase Bearer required. */ - 401: { + /** @description Customer or submission not found. */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Internal server error. */ + /** @description Alfredpay request failed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; }; }; - brlaGetUploadUrls: { + submitAlfredpayKybRelatedPersonFile: { parameters: { query?: never; header?: never; @@ -2271,190 +4635,213 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["AveniaKYCDataUploadRequest"]; + "multipart/form-data": components["schemas"]["AlfredpayKybRelatedPersonFileUploadRequest"]; }; }; responses: { - /** @description Upload URLs returned. */ + /** @description File uploaded. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaKYCDataUploadResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Missing/invalid documentType or taxId; or ramp disabled for this tax ID. */ + /** @description Invalid multipart request. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal server error. */ - 500: { + /** @description Managed profile or corridor is not authorized. */ + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Business customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Alfredpay request failed. */ + 500: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - getBrlaUser: { + sendAlfredpayKybSubmission: { parameters: { - query: { - /** @description The user's Tax ID. */ - taxId: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpaySendSubmissionRequest"]; + }; + }; responses: { - /** @description Successfully retrieved user information. */ + /** @description Submission finalized. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetUserResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** - * @description Bad Request. Possible reasons: - * - Missing taxId query parameter - * - KYC invalid - */ + /** @description Invalid or incomplete submission. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Authentication required. */ + 401: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Subaccount not found. */ - 404: { + /** @description Managed profile or corridor is not authorized. */ + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Business customer not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal Server Error. */ + /** @description Alfredpay request failed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; }; }; - getBrlaUserRemainingLimit: { + selectActiveCustomerEntity: { parameters: { - query: { - /** @description The user's Tax ID. */ - taxId: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SelectActiveCustomerEntityRequest"]; + }; + }; responses: { - /** @description Successfully retrieved user's remaining limits. */ + /** @description Active customer entity selected. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetUserRemainingLimitResponse"]; + "application/json": components["schemas"]["SelectActiveCustomerEntityResponse"]; }; }; - /** @description Missing taxId query parameter or other invalid request. */ + /** @description Invalid customer-entity type. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description Supabase Bearer authentication required. */ + 401: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Subaccount not found or limits not found. */ + /** @description No active owned entity of the requested type exists. */ 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + content?: never; + }; + /** @description The selection conflicts with an existing selection or is ambiguous. */ + 409: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Internal Server Error. */ + /** @description Selection could not be completed. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; }; }; - brlaNewKyc: { + getOnboardingStatus: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["KycLevel1Payload"]; - }; - }; + requestBody?: never; responses: { - /** @description KYC submission accepted. */ + /** @description Aggregate onboarding status. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["KycLevel1Response"]; + "application/json": components["schemas"]["OnboardingStatusResponse"]; }; }; - /** @description Validation failure. */ - 400: { + /** @description Authentication required. */ + 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; - /** @description Internal server error. */ + /** @description Onboarding state could not be read. */ 500: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; + content?: never; }; }; }; - brlaValidatePixKey: { + getOnboardingRequirements: { parameters: { query: { - /** @description Pix key to validate (CPF, CNPJ, email, phone, or random key). */ - pixKey: string; + country: "AR" | "BR" | "CO" | "MX" | "US"; + customerType: "individual" | "business"; }; header?: never; path?: never; @@ -2462,38 +4849,31 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Validation result. */ + /** @description Requirements and existing operation sequence. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaValidatePixKeyResponse"]; + "application/json": components["schemas"]["OnboardingRequirementsResponse"]; }; }; - /** @description Missing or invalid pix key. */ + /** @description Missing or invalid query. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; - }; - }; - /** @description Supabase Bearer required. */ - 401: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["OnboardingRequirementsErrorResponse"]; }; - content?: never; }; - /** @description Internal server error. */ - 500: { + /** @description No published flow exists for the country and customer type. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["OnboardingRequirementsErrorResponse"]; }; }; }; diff --git a/docs/api/openapi/vortex.openapi.json b/docs/api/openapi/vortex.openapi.json index 05201359e..5ecb957ef 100644 --- a/docs/api/openapi/vortex.openapi.json +++ b/docs/api/openapi/vortex.openapi.json @@ -56,6 +56,157 @@ "required": ["address", "type"], "type": "object" }, + "AlfredpayCountry": { + "enum": ["AR", "CO", "MX", "US"], + "type": "string" + }, + "AlfredpayCreateCustomerRequest": { + "properties": { + "country": { "$ref": "#/components/schemas/AlfredpayCountry" } + }, + "required": ["country"], + "type": "object" + }, + "AlfredpayCreateCustomerResponse": { + "properties": { + "createdAt": { "format": "date-time", "type": "string" } + }, + "required": ["createdAt"], + "type": "object" + }, + "AlfredpayCustomerType": { + "enum": ["INDIVIDUAL", "BUSINESS"], + "type": "string" + }, + "AlfredpayKybDetailsResponse": { + "items": { + "properties": { + "relatedPersons": { + "items": { + "properties": { "idRelatedPerson": { "type": "string" } }, + "required": ["idRelatedPerson"], + "type": "object" + }, + "type": "array" + }, + "submissionId": { "type": "string" } + }, + "required": ["submissionId", "relatedPersons"], + "type": "object" + }, + "type": "array" + }, + "AlfredpayKybFileUploadRequest": { + "properties": { + "country": { "enum": ["CO", "MX"], "type": "string" }, + "file": { "format": "binary", "type": "string" }, + "fileType": { + "enum": [ + "taxIdDocument", + "articlesIncorporation", + "proofAddress", + "shareholderRegistry", + "businessLicense", + "uploadAmlPolicy" + ], + "type": "string" + }, + "submissionId": { "type": "string" } + }, + "required": ["country", "submissionId", "fileType", "file"], + "type": "object" + }, + "AlfredpayKybRelatedPerson": { + "properties": { + "cpf": { "type": "string" }, + "dateOfBirth": { "format": "date", "type": "string" }, + "dni": { "type": "string" }, + "email": { "format": "email", "type": "string" }, + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "nationalities": { "items": { "type": "string" }, "minItems": 1, "type": "array" }, + "pep": { "type": "boolean" } + }, + "required": ["firstName", "lastName", "email", "dateOfBirth", "nationalities"], + "type": "object" + }, + "AlfredpayKybRelatedPersonFileUploadRequest": { + "properties": { + "country": { "enum": ["CO", "MX"], "type": "string" }, + "file": { "format": "binary", "type": "string" }, + "fileType": { "enum": ["docFront", "docBack"], "type": "string" }, + "relatedPersonId": { "type": "string" } + }, + "required": ["country", "relatedPersonId", "fileType", "file"], + "type": "object" + }, + "AlfredpayKycFileUploadRequest": { + "properties": { + "country": { "enum": ["AR", "CO", "MX"], "type": "string" }, + "file": { "format": "binary", "type": "string" }, + "fileType": { "enum": ["National ID Front", "National ID Back", "Selfie"], "type": "string" }, + "submissionId": { "type": "string" } + }, + "required": ["country", "submissionId", "fileType", "file"], + "type": "object" + }, + "AlfredpayKycStatusResponse": { + "properties": { + "alfred_pay_id": { "type": "string" }, + "country": { "$ref": "#/components/schemas/AlfredpayCountry" }, + "lastFailure": { "type": "string" }, + "status": { "$ref": "#/components/schemas/AlfredpayStatus" }, + "updated_at": { "format": "date-time", "type": "string" } + }, + "required": ["status", "updated_at", "alfred_pay_id", "country"], + "type": "object" + }, + "AlfredpayRedirectLinkResponse": { + "properties": { + "submissionId": { "type": "string" }, + "verification_url": { "format": "uri", "type": "string" } + }, + "required": ["verification_url", "submissionId"], + "type": "object" + }, + "AlfredpayRedirectNotificationRequest": { + "properties": { + "country": { "$ref": "#/components/schemas/AlfredpayCountry" }, + "type": { "$ref": "#/components/schemas/AlfredpayCustomerType" } + }, + "required": ["country"], + "type": "object" + }, + "AlfredpayRetryRequest": { + "$ref": "#/components/schemas/AlfredpayRedirectNotificationRequest" + }, + "AlfredpayRetryResponse": { + "oneOf": [ + { "$ref": "#/components/schemas/AlfredpayRedirectLinkResponse" }, + { "$ref": "#/components/schemas/SuccessResponse" } + ] + }, + "AlfredpaySendSubmissionRequest": { + "properties": { + "country": { "enum": ["AR", "CO", "MX"], "type": "string" }, + "submissionId": { "type": "string" } + }, + "required": ["country", "submissionId"], + "type": "object" + }, + "AlfredpayStatus": { + "enum": ["CONSULTED", "LINK_OPENED", "USER_COMPLETED", "VERIFYING", "FAILED", "SUCCESS", "UPDATE_REQUIRED"], + "type": "string" + }, + "AlfredpayStatusResponse": { + "properties": { + "country": { "$ref": "#/components/schemas/AlfredpayCountry" }, + "creationTime": { "format": "date-time", "type": "string" }, + "status": { "$ref": "#/components/schemas/AlfredpayStatus" } + }, + "required": ["status", "country", "creationTime"], + "type": "object" + }, "ApiCredential": { "properties": { "createdAt": { "format": "date-time", "type": "string" }, @@ -121,13 +272,26 @@ "type": "object" }, "AveniaDocumentType": { - "enum": ["ID", "DRIVERS-LICENSE", "PASSPORT", "SELFIE", "SELFIE-FROM-LIVENESS"], + "enum": [ + "ID", + "DRIVERS-LICENSE", + "PASSPORT", + "RESIDENCE-PERMIT", + "SELFIE", + "SELFIE-FROM-LIVENESS", + "CERTIFICATE-OF-INCORPORATION", + "COMPANY-TAX-IDENTIFICATION-DOCUMENT" + ], "type": "string" }, "AveniaKYCDataUploadRequest": { "properties": { "documentType": { - "$ref": "#/components/schemas/AveniaDocumentType" + "enum": ["ID", "DRIVERS-LICENSE"], + "type": "string" + }, + "isDoubleSided": { + "type": "boolean" }, "taxId": { "description": "CPF or CNPJ.", @@ -149,6 +313,220 @@ "required": ["idUpload", "selfieUpload"], "type": "object" }, + "AveniaKybAttemptStatusResponse": { + "properties": { + "failureReason": { "type": "string" }, + "result": { "enum": ["APPROVED", "REJECTED"], "type": "string" }, + "retryable": { "type": "boolean" }, + "status": { "enum": ["PENDING", "PROCESSING", "COMPLETED", "EXPIRED"], "type": "string" } + }, + "required": ["status"], + "type": "object" + }, + "AveniaKybDocumentRequest": { + "properties": { + "documentType": { "$ref": "#/components/schemas/AveniaDocumentType" }, + "isDoubleSided": { "type": "boolean" } + }, + "required": ["documentType"], + "type": "object" + }, + "AveniaKybDocumentResponse": { + "properties": { + "document": { + "properties": { + "documentType": { "$ref": "#/components/schemas/AveniaDocumentType" }, + "id": { "type": "string" }, + "ready": { "type": "boolean" }, + "uploadErrorBack": { "type": "string" }, + "uploadErrorFront": { "type": "string" }, + "uploadStatusBack": { "type": "string" }, + "uploadStatusFront": { "type": "string" } + }, + "required": ["id", "documentType", "uploadStatusFront", "ready"], + "type": "object" + } + }, + "required": ["document"], + "type": "object" + }, + "AveniaKybDocumentUploadResponse": { + "properties": { + "id": { "type": "string" }, + "livenessUrl": { "format": "uri", "type": "string" }, + "uploadURLBack": { "format": "uri", "type": "string" }, + "uploadURLFront": { "format": "uri", "type": "string" }, + "validateLivenessToken": { "type": "string" } + }, + "required": ["id", "uploadURLFront"], + "type": "object" + }, + "AveniaKybHostedResponse": { + "properties": { + "attemptId": { "type": "string" }, + "authorizedRepresentativeUrl": { "format": "uri", "type": "string" }, + "basicCompanyDataUrl": { "format": "uri", "type": "string" } + }, + "required": ["attemptId", "authorizedRepresentativeUrl", "basicCompanyDataUrl"], + "type": "object" + }, + "AveniaKybLevel1Payload": { + "additionalProperties": false, + "properties": { + "businessActivityDescription": { "type": "string" }, + "certificateOfIncorporationDocumentId": { "type": "string" }, + "companyCity": { "type": "string" }, + "companyCountry": { "type": "string" }, + "companyLegalName": { "type": "string" }, + "companyRegistrationNumber": { "type": "string" }, + "companyState": { "type": "string" }, + "companyStreetLine1": { "type": "string" }, + "companyStreetLine2": { "type": "string" }, + "companyStreetLine3": { "type": "string" }, + "companyZipCode": { "type": "string" }, + "countrySubdivisionTaxResidence": { "type": "string" }, + "countryTaxResidence": { "type": "string" }, + "emailPixKey": { "format": "email", "type": "string" }, + "estimatedAnnualRevenueUsd": { + "enum": ["less_than_100k", "100k_to_1m", "1m_to_10m", "10m_to_50m", "50m_to_100m", "more_than_100m"], + "type": "string" + }, + "estimatedMonthlyVolumeUsd": { "pattern": "^[1-9][0-9]*$", "type": "string" }, + "numberOfEmployees": { "enum": ["1-10", "11-50", "51-200", "201-500", "501-1000", "1001+"], "type": "string" }, + "reasonForAccountOpening": { + "enum": [ + "charitable_donations", + "ecommerce_retail_payments", + "investment_purposes", + "other", + "payments_to_friends_or_family_abroad", + "payroll", + "personal_or_living_expenses", + "protect_wealth", + "purchase_goods_and_services", + "receive_payments_for_goods_and_services", + "tax_optimization", + "third_party_money_transmission", + "treasury_management" + ], + "type": "string" + }, + "sandboxReject": { "type": "boolean" }, + "socialMedia": { "format": "uri", "type": "string" }, + "sourceOfFundsAndIncome": { + "enum": [ + "business_loans", + "grants", + "inter_company_funds", + "investment_proceeds", + "legal_settlement", + "owners_capital", + "pension_retirement", + "sale_of_assets", + "sales_of_goods_and_services", + "third_party_funds", + "treasury_reserves" + ], + "type": "string" + }, + "taxIdentificationDocumentId": { "type": "string" }, + "taxIdentificationNumberTin": { "type": "string" }, + "uboIds": { "items": { "type": "string" }, "maxItems": 50, "minItems": 1, "type": "array" }, + "website": { "format": "uri", "type": "string" } + }, + "required": [ + "uboIds", + "companyLegalName", + "companyRegistrationNumber", + "taxIdentificationNumberTin", + "businessActivityDescription", + "reasonForAccountOpening", + "sourceOfFundsAndIncome", + "numberOfEmployees", + "estimatedAnnualRevenueUsd", + "estimatedMonthlyVolumeUsd", + "countryTaxResidence", + "companyStreetLine1", + "companyCity", + "companyState", + "companyZipCode", + "companyCountry", + "certificateOfIncorporationDocumentId", + "taxIdentificationDocumentId" + ], + "type": "object" + }, + "AveniaUboControlRole": { + "enum": [ + "CEO", + "CFO", + "COO", + "CTO", + "President", + "Vice President", + "Director", + "Managing Director", + "Managing Partner", + "General Partner", + "Partner", + "Secretary", + "Treasurer", + "Chairman", + "Board Member", + "Authorized Signatory", + "General Counsel", + "Owner", + "Founder", + "Manager", + "Member", + "Comptroller", + "Chief Compliance Officer" + ], + "type": "string" + }, + "AveniaUboPayload": { + "additionalProperties": false, + "properties": { + "city": { "type": "string" }, + "country": { "type": "string" }, + "countryOfTaxId": { "type": "string" }, + "dateOfBirth": { "format": "date", "type": "string" }, + "documentCountry": { "type": "string" }, + "email": { "format": "email", "type": "string" }, + "fullName": { "type": "string" }, + "hasControl": { "$ref": "#/components/schemas/AveniaUboControlRole" }, + "percentageOfOwnership": { "type": "string" }, + "phone": { "type": "string" }, + "state": { "type": "string" }, + "streetLine1": { "type": "string" }, + "streetLine2": { "type": "string" }, + "streetLine3": { "type": "string" }, + "taxIdNumber": { "type": "string" }, + "uploadedIdentificationId": { "type": "string" }, + "uploadedSelfieId": { "type": "string" }, + "zipCode": { "type": "string" } + }, + "required": [ + "fullName", + "dateOfBirth", + "countryOfTaxId", + "taxIdNumber", + "percentageOfOwnership", + "uploadedIdentificationId", + "documentCountry", + "streetLine1", + "city", + "state", + "zipCode", + "country" + ], + "type": "object" + }, + "AveniaUboResponse": { + "properties": { "id": { "type": "string" } }, + "required": ["id"], + "type": "object" + }, "BrlaAddress": { "properties": { "cep": { @@ -367,52 +745,36 @@ }, "CreateSubaccountRequest": { "properties": { - "address": { - "$ref": "#/components/schemas/BrlaAddress" - }, - "birthdate": { - "description": "Date must be in format YYYY-MMM-DD.", - "format": "date", + "accountType": { + "enum": ["INDIVIDUAL", "COMPANY"], "type": "string" }, - "cnpj": { - "type": ["string", "null"] - }, - "companyName": { - "type": ["string", "null"] - }, - "cpf": { + "name": { + "description": "Individual full name or company legal name.", "type": "string" }, - "fullName": { + "quoteId": { "type": "string" }, - "phone": { + "sessionId": { "type": "string" }, - "quoteId": { - "description": "Optional. The quote that triggered onboarding. Omit it for the quote-less KYB deep link (`?kyb` / `?kybLocked` widget entry), where business verification starts before any quote exists. Stored only as onboarding provenance; it is not an authorization input.", - "type": ["string", "null"] - }, - "startDate": { - "description": "Date must be in format YYYY-MMM-DD.", - "format": "date", - "type": ["string", "null"] - }, - "taxIdType": { - "$ref": "#/components/schemas/TaxIdType" + "taxId": { + "description": "CPF for an individual or CNPJ for a company.", + "type": "string" } }, - "required": ["phone", "taxIdType", "address", "fullName", "cpf", "birthdate"], + "required": ["accountType", "name", "taxId"], "type": "object" }, "CreateSubaccountResponse": { "properties": { - "subaccountId": { + "subAccountId": { "description": "The ID of the created or processed subaccount.", "type": "string" } }, + "required": ["subAccountId"], "type": "object" }, "DestinationType": { @@ -491,13 +853,21 @@ }, "GetKycStatusResponse": { "properties": { + "failureReason": { + "enum": ["face", "name", "birthdate", "unknown", "tax_id"], + "type": "string" + }, "level": { "description": "The KYC level achieved.", - "type": "number" + "type": "string" + }, + "result": { + "enum": ["APPROVED", "REJECTED"], + "type": "string" }, "status": { "description": "The KYC status.", - "enum": ["PENDING", "APPROVED", "REJECTED"], + "enum": ["PENDING", "PROCESSING", "COMPLETED", "EXPIRED"], "type": "string" }, "type": { @@ -506,6 +876,7 @@ "type": "string" } }, + "required": ["type", "level", "status"], "type": "object" }, "GetRampErrorLogsResponse": { @@ -624,17 +995,13 @@ }, "GetUserRemainingLimitResponse": { "properties": { - "remainingLimitOfframp": { - "description": "The remaining limit for offramp operations.", - "format": "double", - "type": "number" - }, - "remainingLimitOnramp": { - "description": "The remaining limit for onramp operations.", + "remainingLimit": { + "description": "The remaining limit for the requested direction.", "format": "double", "type": "number" } }, + "required": ["remainingLimit"], "type": "object" }, "GetUserResponse": { @@ -643,12 +1010,19 @@ "description": "The user's EVM wallet address.", "type": "string" }, + "identityStatus": { + "enum": ["NOT-IDENTIFIED", "CONFIRMED"], + "type": "string" + }, "kycLevel": { "description": "The user's KYC level.", - "enum": [1, 2], "type": "number" + }, + "subAccountId": { + "type": "string" } }, + "required": ["evmAddress", "kycLevel", "identityStatus", "subAccountId"], "type": "object" }, "GetWidgetUrlLocked": { @@ -830,6 +1204,216 @@ "enum": ["assethub", "arbitrum", "avalanche", "base", "bsc", "ethereum", "polygon", "moonbeam"], "type": "string" }, + "OnboardingApiErrorResponse": { + "properties": { + "error": { + "oneOf": [ + { "type": "string" }, + { + "properties": { + "code": { "type": "string" }, + "message": { "type": "string" }, + "status": { "type": "integer" } + }, + "required": ["code", "message", "status"], + "type": "object" + } + ] + } + }, + "required": ["error"], + "type": "object" + }, + "OnboardingDocumentRequirement": { + "properties": { + "acceptedMediaTypes": { "items": { "type": "string" }, "type": "array" }, + "collection": { "enum": ["direct-upload", "hosted"], "type": "string" }, + "description": { "type": "string" }, + "required": { "type": "boolean" }, + "requiredWhen": { "type": "string" }, + "type": { "type": "string" } + }, + "required": ["type", "required"], + "type": "object" + }, + "OnboardingRequirementField": { + "properties": { + "allowedValues": { "items": { "type": "string" }, "type": "array" }, + "description": { "type": "string" }, + "format": { "type": "string" }, + "path": { "type": "string" }, + "required": { "type": "boolean" }, + "requiredWhen": { "type": "string" }, + "type": { "enum": ["array", "boolean", "number", "string"], "type": "string" } + }, + "required": ["path", "required", "type"], + "type": "object" + }, + "OnboardingRequirementStep": { + "properties": { + "condition": { "type": "string" }, + "derivedValues": { "additionalProperties": { "type": "string" }, "type": "object" }, + "description": { "type": "string" }, + "fixedBody": { "additionalProperties": { "type": "string" }, "type": "object" }, + "fixedQuery": { "additionalProperties": { "type": "string" }, "type": "object" }, + "kind": { "enum": ["api", "direct-upload", "hosted"], "type": "string" }, + "method": { "enum": ["GET", "POST", "PUT"], "type": "string" }, + "operationId": { "type": "string" }, + "order": { "minimum": 1, "type": "integer" }, + "path": { "type": "string" }, + "repeatFor": { "type": "string" }, + "requestSchema": { "type": "string" } + }, + "required": ["order", "kind", "description"], + "type": "object" + }, + "OnboardingRequirementsErrorResponse": { + "properties": { + "error": { + "properties": { + "code": { + "enum": ["INVALID_ONBOARDING_REQUIREMENTS_QUERY", "ONBOARDING_REQUIREMENTS_NOT_FOUND"], + "type": "string" + }, + "message": { "type": "string" }, + "status": { "enum": [400, 404], "type": "integer" } + }, + "required": ["code", "message", "status"], + "type": "object" + } + }, + "required": ["error"], + "type": "object" + }, + "OnboardingRequirementsResponse": { + "properties": { + "country": { "enum": ["AR", "BR", "CO", "MX", "US"], "type": "string" }, + "customerType": { "enum": ["individual", "business"], "type": "string" }, + "documentationUrl": { "format": "uri", "type": "string" }, + "documents": { "items": { "$ref": "#/components/schemas/OnboardingDocumentRequirement" }, "type": "array" }, + "fields": { "items": { "$ref": "#/components/schemas/OnboardingRequirementField" }, "type": "array" }, + "flow": { "type": "string" }, + "mode": { "enum": ["api", "hosted", "hybrid"], "type": "string" }, + "openapiUrl": { "format": "uri", "type": "string" }, + "provider": { "enum": ["alfredpay", "avenia"], "type": "string" }, + "requirementsVersion": { "type": "string" }, + "steps": { "items": { "$ref": "#/components/schemas/OnboardingRequirementStep" }, "type": "array" } + }, + "required": [ + "country", + "customerType", + "documentationUrl", + "documents", + "fields", + "flow", + "mode", + "openapiUrl", + "provider", + "requirementsVersion", + "steps" + ], + "type": "object" + }, + "OnboardingStatusResponse": { + "properties": { + "activeEntityId": { "type": ["string", "null"] }, + "entities": { + "items": { + "properties": { + "accounts": { + "items": { + "properties": { + "companyName": { "type": ["string", "null"] }, + "country": { "type": ["string", "null"] }, + "customerType": { "enum": ["individual", "business"], "type": "string" }, + "error": { + "oneOf": [ + { "type": "null" }, + { + "properties": { + "code": { "type": "string" }, + "message": { "type": "string" } + }, + "required": ["code", "message"], + "type": "object" + } + ] + }, + "id": { "type": "string" }, + "kycCase": { + "oneOf": [ + { "type": "null" }, + { + "properties": { + "approvedAt": { "format": "date-time", "type": ["string", "null"] }, + "failureReasons": { "items": { "type": "string" }, "type": ["array", "null"] }, + "level": { "type": ["string", "null"] }, + "rejectedAt": { "format": "date-time", "type": ["string", "null"] }, + "status": { + "enum": ["pending", "started", "in_review", "approved", "rejected"], + "type": "string" + }, + "statusExternal": { "type": ["string", "null"] }, + "submittedAt": { "format": "date-time", "type": ["string", "null"] }, + "type": { "enum": ["kyc", "kyb"], "type": "string" } + }, + "required": [ + "approvedAt", + "failureReasons", + "level", + "rejectedAt", + "status", + "statusExternal", + "submittedAt", + "type" + ], + "type": "object" + } + ] + }, + "provider": { "enum": ["alfredpay", "avenia", "monerium", "mykobo"], "type": "string" }, + "rail": { "type": ["string", "null"] }, + "state": { "enum": ["pending", "started", "in_review", "approved", "rejected"], "type": "string" }, + "status": { "enum": ["pending", "started", "in_review", "approved", "rejected"], "type": "string" }, + "statusExternal": { "type": ["string", "null"] }, + "taxReference": { + "description": "Business tax ID only; individual tax IDs remain private.", + "type": ["string", "null"] + } + }, + "required": [ + "companyName", + "country", + "customerType", + "error", + "id", + "kycCase", + "provider", + "rail", + "state", + "status", + "statusExternal", + "taxReference" + ], + "type": "object" + }, + "type": "array" + }, + "id": { "type": "string" }, + "status": { "enum": ["active", "archived", "blocked"], "type": "string" }, + "type": { "enum": ["individual", "business"], "type": "string" } + }, + "required": ["accounts", "id", "status", "type"], + "type": "object" + }, + "type": "array" + }, + "roles": { "items": { "type": "string" }, "type": "array" }, + "selectionRequired": { "type": "boolean" } + }, + "required": ["activeEntityId", "entities", "roles", "selectionRequired"], + "type": "object" + }, "OnChainToken": { "enum": ["USDC", "USDT", "ETH", "USDC.E"], "type": "string" @@ -1219,6 +1803,15 @@ ], "type": "object" }, + "RecordInitialKycAttemptRequest": { + "properties": { + "quoteId": { "type": "string" }, + "sessionId": { "type": "string" }, + "taxId": { "type": "string" } + }, + "required": ["taxId"], + "type": "object" + }, "RegisterRampRequest": { "properties": { "additionalData": { @@ -1285,6 +1878,22 @@ "required": ["quoteId", "signingAccounts"], "type": "object" }, + "SelectActiveCustomerEntityRequest": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["individual", "business"], "type": "string" } + }, + "required": ["type"], + "type": "object" + }, + "SelectActiveCustomerEntityResponse": { + "properties": { + "activeEntityId": { "type": "string" }, + "type": { "enum": ["individual", "business"], "type": "string" } + }, + "required": ["activeEntityId", "type"], + "type": "object" + }, "SimpleStatus": { "description": "`PENDING`, `FAILED`, `COMPLETED`", "type": "string" @@ -1318,17 +1927,129 @@ "required": ["rampId"], "type": "object" }, - "TaxIdType": { - "enum": ["CPF", "CNPJ"], - "type": "string" - }, - "TriggerOfframpRequest": { + "SubmitInformationResponse": { "properties": { - "amount": { - "description": "The amount to offramp.", - "examples": ["100.50"], - "type": "string" - }, + "submissionId": { "type": "string" } + }, + "required": ["submissionId"], + "type": "object" + }, + "SubmitKybInformationRequest": { + "allOf": [ + { + "if": { "properties": { "transmitsCustomerFunds": { "const": true } } }, + "then": { "required": ["conductsComplianceScreening"] } + }, + { + "if": { + "properties": { "conductsComplianceScreening": { "const": true } }, + "required": ["conductsComplianceScreening"] + }, + "then": { "required": ["complianceScreeningDescription"] } + } + ], + "properties": { + "accountPurpose": { "type": "string" }, + "address": { "type": "string" }, + "businessActivities": { "type": "string" }, + "businessName": { "type": "string" }, + "city": { "type": "string" }, + "complianceScreeningDescription": { "type": "string" }, + "conductsComplianceScreening": { "type": "boolean" }, + "country": { "enum": ["CO", "MX"], "type": "string" }, + "expectedMonthlyTransactions": { "minimum": 0, "type": "integer" }, + "expectedMonthlyVolumeUsd": { "minimum": 0, "type": "number" }, + "isRegulatedBusiness": { "type": "boolean" }, + "operatesInSanctionedCountries": { "type": "boolean" }, + "relatedPersons": { + "items": { "$ref": "#/components/schemas/AlfredpayKybRelatedPerson" }, + "minItems": 1, + "type": "array" + }, + "sourceOfFunds": { "type": "string" }, + "state": { "type": "string" }, + "taxId": { "type": "string" }, + "transmitsCustomerFunds": { "type": "boolean" }, + "walletAddresses": { "type": "string" }, + "website": { "format": "uri", "type": "string" }, + "zipCode": { "type": "string" } + }, + "required": [ + "businessName", + "taxId", + "country", + "address", + "state", + "city", + "zipCode", + "website", + "relatedPersons", + "walletAddresses", + "sourceOfFunds", + "transmitsCustomerFunds", + "operatesInSanctionedCountries", + "isRegulatedBusiness", + "businessActivities", + "accountPurpose", + "expectedMonthlyVolumeUsd", + "expectedMonthlyTransactions" + ], + "type": "object" + }, + "SubmitKycInformationRequest": { + "oneOf": [ + { + "properties": { "country": { "const": "MX" } }, + "required": ["email"] + }, + { + "properties": { "country": { "const": "CO" } }, + "required": ["typeDocumentCol", "phoneNumber"] + }, + { + "properties": { "country": { "const": "AR" } }, + "required": ["email", "phoneNumber", "countryCode", "nationalities", "typeDocumentAr", "pep"] + } + ], + "properties": { + "address": { "type": "string" }, + "city": { "type": "string" }, + "country": { "enum": ["AR", "CO", "MX"], "type": "string" }, + "countryCode": { "type": "string" }, + "cuit": { "pattern": "^[0-9]{11}$", "type": "string" }, + "dateOfBirth": { "format": "date", "type": "string" }, + "dni": { "type": "string" }, + "email": { "format": "email", "type": "string" }, + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "nationalities": { "items": { "type": "string" }, "type": "array" }, + "pep": { "type": "boolean" }, + "phoneNumber": { "type": "string" }, + "state": { "type": "string" }, + "typeDocument": { "type": "string" }, + "typeDocumentAr": { "enum": ["DNI"], "type": "string" }, + "typeDocumentCol": { "enum": ["CC", "CE"], "type": "string" }, + "zipCode": { "type": "string" } + }, + "required": ["firstName", "lastName", "dateOfBirth", "country", "city", "state", "zipCode", "address", "dni"], + "type": "object" + }, + "SuccessResponse": { + "properties": { "success": { "const": true, "type": "boolean" } }, + "required": ["success"], + "type": "object" + }, + "TaxIdType": { + "enum": ["CPF", "CNPJ"], + "type": "string" + }, + "TriggerOfframpRequest": { + "properties": { + "amount": { + "description": "The amount to offramp.", + "examples": ["100.50"], + "type": "string" + }, "pixKey": { "description": "The recipient's PIX key.", "type": "string" @@ -1419,92 +2140,500 @@ "type": "string" } }, - "required": ["rampId", "presignedTxs"], - "type": "object" - }, - "UserLimit": { - "properties": { - "corridor": { - "enum": ["AR", "BR", "CO", "MX", "US"], - "type": "string" - }, - "currency": { - "$ref": "#/components/schemas/RampCurrency" - }, - "direction": { - "$ref": "#/components/schemas/RampDirection" - }, - "max": { - "description": "Maximum amount in the returned currency's human units.", - "type": "string" - }, - "period": { - "$ref": "#/components/schemas/UserLimitPeriod" - }, - "used": { - "description": "Amount consumed during the period in the returned currency's human units.", - "type": "string" - } + "required": ["rampId", "presignedTxs"], + "type": "object" + }, + "UserLimit": { + "properties": { + "corridor": { + "enum": ["AR", "BR", "CO", "MX", "US"], + "type": "string" + }, + "currency": { + "$ref": "#/components/schemas/RampCurrency" + }, + "direction": { + "$ref": "#/components/schemas/RampDirection" + }, + "max": { + "description": "Maximum amount in the returned currency's human units.", + "type": "string" + }, + "period": { + "$ref": "#/components/schemas/UserLimitPeriod" + }, + "used": { + "description": "Amount consumed during the period in the returned currency's human units.", + "type": "string" + } + }, + "required": ["corridor", "currency", "direction", "max", "period", "used"], + "type": "object" + }, + "UserLimitPeriod": { + "properties": { + "endsAt": { + "description": "Exclusive end of the reported period.", + "format": "date-time", + "type": "string" + }, + "startsAt": { + "format": "date-time", + "type": "string" + }, + "type": { + "const": "calendar_month", + "type": "string" + } + }, + "required": ["type", "startsAt", "endsAt"], + "type": "object" + }, + "ValidatePixKeyResponse": { + "properties": { + "valid": { + "description": "Indicates if the PIX key is valid.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "BearerAuth": { + "bearerFormat": "Supabase JWT", + "scheme": "bearer", + "type": "http" + }, + "PublicApiKey": { + "description": "Public credential value (`pk_live_*` or `pk_test_*`) for attribution and approved low-sensitivity reads.", + "in": "header", + "name": "X-Public-Key", + "type": "apiKey" + }, + "SecretApiKey": { + "description": "Server-side secret credential value (`sk_live_*` or `sk_test_*`).", + "in": "header", + "name": "X-API-Key", + "type": "apiKey" + } + } + }, + "info": { + "description": "Cross-border payments gateway built on the Pendulum blockchain.\n\n**API credentials:** one credential contains a public (`pk_*`) and secret (`sk_*`) value for one profile subject. Send public values through `X-Public-Key` and server-side secret values through `X-API-Key`. If both are sent, they must belong to the same credential or the request returns `403 CREDENTIAL_MISMATCH`. Public capability is limited to attribution and explicitly sanitized reads; secret capability is required for sensitive and state-changing partner operations.\n\n`Authorization: Bearer ` represents a first-party user session and is required for profile-managed credential lifecycle endpoints.\n\n**Webhook signing:** RSA-PSS 2048 / SHA-256. Fetch the signing key from `GET /v1/public-key`.\n", + "title": "Vortex API", + "version": "1.1.0" + }, + "openapi": "3.1.0", + "paths": { + "/v1/alfredpay/alfredpayStatus": { + "get": { + "description": "Reads and, when applicable, refreshes the effective profile's Alfredpay onboarding status.", + "operationId": "getAlfredpayStatus", + "parameters": [ + { "in": "query", "name": "country", "required": true, "schema": { "$ref": "#/components/schemas/AlfredpayCountry" } }, + { + "description": "Selects the individual or business customer. When omitted, the active customer entity is used for backward compatibility.", + "in": "query", + "name": "type", + "required": false, + "schema": { "$ref": "#/components/schemas/AlfredpayCustomerType" } + } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayStatusResponse" } } }, + "description": "Current onboarding status." + }, + "401": { "description": "Authentication required." }, + "404": { "description": "No Alfredpay customer exists." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get Alfredpay onboarding status", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/createBusinessCustomer": { + "post": { + "description": "Creates the effective profile's Alfredpay business customer.", + "operationId": "createAlfredpayBusinessCustomer", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayCreateCustomerRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayCreateCustomerResponse" } } }, + "description": "Customer created." + }, + "400": { "description": "Invalid country or customer state." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Create Alfredpay business customer", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/createIndividualCustomer": { + "post": { + "description": "Creates the effective profile's Alfredpay individual customer.", + "operationId": "createAlfredpayIndividualCustomer", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayCreateCustomerRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayCreateCustomerResponse" } } }, + "description": "Customer created." + }, + "400": { "description": "Invalid country or customer state." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Create Alfredpay individual customer", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/findKybCustomerAndBusiness": { + "get": { + "description": "Returns the submission and related-person identifiers needed for representative document uploads.", + "operationId": "findAlfredpayKybCustomerAndBusiness", + "parameters": [ + { "in": "query", "name": "country", "required": true, "schema": { "enum": ["CO", "MX"], "type": "string" } } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayKybDetailsResponse" } } }, + "description": "Matching KYB submissions." + }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer or submission not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Find Alfredpay KYB submission identifiers", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/getKybRedirectLink": { + "get": { + "description": "Creates or reads the provider-hosted business KYB session.", + "operationId": "getAlfredpayKybRedirectLink", + "parameters": [ + { "in": "query", "name": "country", "required": true, "schema": { "$ref": "#/components/schemas/AlfredpayCountry" } } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayRedirectLinkResponse" } } }, + "description": "Hosted verification URL." + }, + "400": { "description": "Verification is already in review or completed." }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get Alfredpay KYB redirect link", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/getKycRedirectLink": { + "get": { + "description": "Creates or reads the provider-hosted individual KYC session.", + "operationId": "getAlfredpayKycRedirectLink", + "parameters": [ + { "in": "query", "name": "country", "required": true, "schema": { "$ref": "#/components/schemas/AlfredpayCountry" } } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayRedirectLinkResponse" } } }, + "description": "Hosted verification URL." + }, + "400": { "description": "Verification is already in review or completed." }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get Alfredpay KYC redirect link", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/getKycStatus": { + "get": { + "description": "Reads individual KYC or business KYB status. `type` defaults to `INDIVIDUAL`.", + "operationId": "getAlfredpayKycStatus", + "parameters": [ + { "in": "query", "name": "country", "required": true, "schema": { "$ref": "#/components/schemas/AlfredpayCountry" } }, + { + "in": "query", + "name": "type", + "required": false, + "schema": { "$ref": "#/components/schemas/AlfredpayCustomerType" } + } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayKycStatusResponse" } } }, + "description": "Current KYC or KYB status." + }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer or submission not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get Alfredpay KYC or KYB status", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/kycRedirectFinished": { + "post": { + "description": "Records customer-reported completion of the provider-hosted form. Provider status remains authoritative.", + "operationId": "notifyAlfredpayKycRedirectFinished", + "requestBody": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayRedirectNotificationRequest" } } + }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "Event recorded." + }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Record Alfredpay hosted form completion", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/kycRedirectOpened": { + "post": { + "description": "Records that the customer opened the provider-hosted form. This is not a compliance decision.", + "operationId": "notifyAlfredpayKycRedirectOpened", + "requestBody": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayRedirectNotificationRequest" } } + }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "Event recorded." + }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Record Alfredpay hosted form opened", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/retryKyc": { + "post": { + "description": "Restarts the existing API-based or hosted Alfredpay verification flow.", + "operationId": "retryAlfredpayKyc", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayRetryRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpayRetryResponse" } } }, + "description": "Retry started. Hosted flows return a link; API flows return success." + }, + "400": { "description": "No failed submission exists or the current status cannot be retried." }, + "401": { "description": "Authentication required." }, + "404": { "description": "Customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Retry Alfredpay verification", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/sendKybSubmission": { + "post": { + "description": "Finalizes an API-driven Alfredpay business KYB submission.", + "operationId": "sendAlfredpayKybSubmission", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpaySendSubmissionRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "Submission finalized." + }, + "400": { "description": "Invalid or incomplete submission." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Business customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Finalize Alfredpay KYB submission", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/sendKycSubmission": { + "post": { + "description": "Finalizes an API-driven Alfredpay individual KYC submission.", + "operationId": "sendAlfredpayKycSubmission", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AlfredpaySendSubmissionRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "Submission finalized." + }, + "400": { "description": "Invalid or incomplete submission." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Individual customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Finalize Alfredpay KYC submission", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/submitKybFile": { + "post": { + "description": "Uploads one company KYB file. Files are limited to 5 MiB.", + "operationId": "submitAlfredpayKybFile", + "requestBody": { + "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/AlfredpayKybFileUploadRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "File uploaded." + }, + "400": { "description": "Invalid multipart request." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Business customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Upload Alfredpay KYB file", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/submitKybInformation": { + "post": { + "description": "Creates or updates an API-driven Alfredpay business KYB submission for MX or CO.", + "operationId": "submitAlfredpayKybInformation", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubmitKybInformationRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubmitInformationResponse" } } }, + "description": "Submission created or updated." + }, + "400": { "description": "Invalid company, representative, or questionnaire data." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Business customer not found." }, + "409": { "description": "KYB is already in review or completed." }, + "500": { "description": "Alfredpay request failed." } }, - "required": ["corridor", "currency", "direction", "max", "period", "used"], - "type": "object" - }, - "UserLimitPeriod": { - "properties": { - "endsAt": { - "description": "Exclusive end of the reported period.", - "format": "date-time", - "type": "string" - }, - "startsAt": { - "format": "date-time", - "type": "string" + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Submit Alfredpay KYB information", + "tags": ["KYC and KYB"] + } + }, + "/v1/alfredpay/submitKybRelatedPersonFile": { + "post": { + "description": "Uploads one identity-document side for an Alfredpay KYB related person. Files are limited to 5 MiB.", + "operationId": "submitAlfredpayKybRelatedPersonFile", + "requestBody": { + "content": { + "multipart/form-data": { "schema": { "$ref": "#/components/schemas/AlfredpayKybRelatedPersonFileUploadRequest" } } }, - "type": { - "const": "calendar_month", - "type": "string" - } + "required": true }, - "required": ["type", "startsAt", "endsAt"], - "type": "object" - }, - "ValidatePixKeyResponse": { - "properties": { - "valid": { - "description": "Indicates if the PIX key is valid.", - "type": "boolean" - } + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "File uploaded." + }, + "400": { "description": "Invalid multipart request." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Business customer not found." }, + "500": { "description": "Alfredpay request failed." } }, - "type": "object" + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Upload Alfredpay related-person file", + "tags": ["KYC and KYB"] } }, - "securitySchemes": { - "BearerAuth": { - "bearerFormat": "Supabase JWT", - "scheme": "bearer", - "type": "http" - }, - "PublicApiKey": { - "description": "Public credential value (`pk_live_*` or `pk_test_*`) for attribution and approved low-sensitivity reads.", - "in": "header", - "name": "X-Public-Key", - "type": "apiKey" - }, - "SecretApiKey": { - "description": "Server-side secret credential value (`sk_live_*` or `sk_test_*`).", - "in": "header", - "name": "X-API-Key", - "type": "apiKey" + "/v1/alfredpay/submitKycFile": { + "post": { + "description": "Uploads one individual KYC file. Files are limited to 5 MiB.", + "operationId": "submitAlfredpayKycFile", + "requestBody": { + "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/AlfredpayKycFileUploadRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } }, + "description": "File uploaded." + }, + "400": { "description": "Invalid multipart request." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Individual customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Upload Alfredpay KYC file", + "tags": ["KYC and KYB"] } - } - }, - "info": { - "description": "Cross-border payments gateway built on the Pendulum blockchain.\n\n**API credentials:** one credential contains a public (`pk_*`) and secret (`sk_*`) value for one profile subject. Send public values through `X-Public-Key` and server-side secret values through `X-API-Key`. If both are sent, they must belong to the same credential or the request returns `403 CREDENTIAL_MISMATCH`. Public capability is limited to attribution and explicitly sanitized reads; secret capability is required for sensitive and state-changing partner operations.\n\n`Authorization: Bearer ` represents a first-party user session and is required for profile-managed credential lifecycle endpoints.\n\n**Webhook signing:** RSA-PSS 2048 / SHA-256. Fetch the signing key from `GET /v1/public-key`.\n", - "title": "Vortex API", - "version": "1.1.0" - }, - "openapi": "3.1.0", - "paths": { + }, + "/v1/alfredpay/submitKycInformation": { + "post": { + "description": "Creates an API-driven Alfredpay individual KYC submission for MX, CO, or AR.", + "operationId": "submitAlfredpayKycInformation", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubmitKycInformationRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubmitInformationResponse" } } }, + "description": "Submission created." + }, + "400": { "description": "Invalid identity data." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Individual customer not found." }, + "500": { "description": "Alfredpay request failed." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Submit Alfredpay KYC information", + "tags": ["KYC and KYB"] + } + }, "/v1/api-credentials": { "get": { "deprecated": false, @@ -1862,7 +2991,7 @@ "/v1/brla/createSubaccount": { "post": { "deprecated": false, - "description": "`companyName`, `startDate` and `cnpj` are only required when taxIdType is `CNPJ`\n\n`quoteId` is optional: pass it in the normal ramp flow, or omit it for the quote-less KYB deep link where business verification starts before any quote exists.\n\n**Auth:** uses `optionalAuth` \u2014 accepts a Supabase Bearer token if present but does not require one.", + "description": "Creates the Avenia individual or company subaccount owned by the effective profile. `quoteId` and `sessionId` are optional provenance fields. A managed-profile manager supplies `X-Managed-Profile-Id` and must have the BR corridor.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "createSubaccount", "parameters": [], "requestBody": { @@ -1910,7 +3039,7 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Create user or retry KYC", "tags": ["Account Management"] } @@ -1918,7 +3047,7 @@ "/v1/brla/getKycStatus": { "get": { "deprecated": false, - "description": "\n\n**Auth:** requires `Authorization: Bearer `.", + "description": "Returns the latest Avenia KYC attempt status owned by the effective profile.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "fetchSubaccountKycStatus", "parameters": [ { @@ -1977,7 +3106,7 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Get user's KYC status", "tags": ["Account Management"] } @@ -1985,7 +3114,7 @@ "/v1/brla/getSelfieLivenessUrl": { "get": { "deprecated": false, - "description": "Returns the Avenia selfie/liveness-check URL for the subaccount associated with this tax ID.\n\n**Auth:** requires `Authorization: Bearer `.", + "description": "Returns the Avenia selfie/liveness-check URL for the owned subaccount.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "brlaGetSelfieLivenessUrl", "parameters": [ { @@ -2038,7 +3167,7 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Get selfie liveness URL", "tags": ["Account Management"] } @@ -2046,7 +3175,7 @@ "/v1/brla/getUploadUrls": { "post": { "deprecated": false, - "description": "Returns presigned upload URLs for the user's ID document and selfie. Only `ID` and `DRIVERS-LICENSE` are accepted for `documentType` (passport not supported here).\n\n**Auth:** uses `optionalAuth` \u2014 accepts a Supabase Bearer token if present but does not require one.", + "description": "Returns presigned upload URLs for the owned user's ID document and selfie. Only `ID` and `DRIVERS-LICENSE` are accepted.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "brlaGetUploadUrls", "requestBody": { "content": { @@ -2093,7 +3222,7 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Get KYC document upload URLs", "tags": ["Account Management"] } @@ -2101,14 +3230,14 @@ "/v1/brla/getUser": { "get": { "deprecated": false, - "description": "Fetches a user's subaccount information. The response contains only the EVM wallet address and KYC level.\n\n**Auth:** requires `Authorization: Bearer `.", + "description": "Fetches the effective profile's Avenia subaccount information. `taxId` is an optional ownership cross-check.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "getBrlaUser", "parameters": [ { "description": "The user's Tax ID.", "in": "query", "name": "taxId", - "required": true, + "required": false, "schema": { "type": "string" } @@ -2160,7 +3289,7 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Get user information", "tags": ["Account Management"] } @@ -2168,15 +3297,25 @@ "/v1/brla/getUserRemainingLimit": { "get": { "deprecated": false, - "description": "\n\n**Auth:** requires `Authorization: Bearer `.", + "description": "Returns the remaining Avenia limit for one ramp direction. `taxId` is an optional ownership cross-check.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "getBrlaUserRemainingLimit", "parameters": [ { "description": "The user's Tax ID.", "in": "query", "name": "taxId", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Ramp direction whose remaining limit should be returned.", + "in": "query", + "name": "direction", "required": true, "schema": { + "enum": ["BUY", "SELL"], "type": "string" } } @@ -2227,15 +3366,182 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Get user's remaining transaction limits", "tags": ["Account Management"] } }, + "/v1/brla/kyb/attempt-status": { + "get": { + "description": "Reads an Avenia KYB attempt after binding it to the effective profile's company account.", + "operationId": "getAveniaKybAttemptStatus", + "parameters": [{ "in": "query", "name": "attemptId", "required": true, "schema": { "type": "string" } }], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaKybAttemptStatusResponse" } } }, + "description": "KYB attempt status." + }, + "400": { "description": "Invalid attempt identifier." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Attempt does not belong to the effective profile." }, + "404": { "description": "Attempt not found." }, + "409": { "description": "The attempt is no longer the current bound KYB attempt." }, + "502": { "description": "Avenia is unavailable or returned an invalid response." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get Avenia KYB attempt status", + "tags": ["KYC and KYB"] + } + }, + "/v1/brla/kyb/documents": { + "post": { + "description": "Creates an Avenia document and returns presigned upload targets. Upload bytes directly to the returned URLs.", + "operationId": "createAveniaKybDocument", + "parameters": [{ "in": "query", "name": "subAccountId", "required": true, "schema": { "type": "string" } }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaKybDocumentRequest" } } }, + "required": true + }, + "responses": { + "201": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaKybDocumentUploadResponse" } } }, + "description": "Document upload targets created." + }, + "400": { "description": "Invalid document request." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Subaccount not found." }, + "502": { "description": "Avenia is unavailable or returned an invalid response." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Create Avenia KYB document", + "tags": ["KYC and KYB"] + } + }, + "/v1/brla/kyb/documents/{documentId}": { + "get": { + "description": "Reads readiness and upload status for an owned Avenia KYB document.", + "operationId": "getAveniaKybDocument", + "parameters": [ + { "in": "path", "name": "documentId", "required": true, "schema": { "type": "string" } }, + { "in": "query", "name": "subAccountId", "required": true, "schema": { "type": "string" } } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaKybDocumentResponse" } } }, + "description": "Document status." + }, + "400": { "description": "Invalid document identifier." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Document does not belong to the effective profile." }, + "404": { "description": "Document not found." }, + "502": { "description": "Avenia is unavailable or returned an invalid response." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get Avenia KYB document", + "tags": ["KYC and KYB"] + } + }, + "/v1/brla/kyb/new-level-1/api": { + "post": { + "description": "Submits the API-driven Avenia Level 1 KYB attempt after validating the owned corporate documents and UBO references.", + "operationId": "submitAveniaKybLevel1Api", + "parameters": [{ "in": "query", "name": "subAccountId", "required": true, "schema": { "type": "string" } }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaKybLevel1Payload" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/KycLevel1Response" } } }, + "description": "KYB attempt submitted." + }, + "400": { "description": "Invalid submission or document state." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Subaccount or referenced document not found." }, + "409": { "description": "A different KYB submission is already in progress." }, + "502": { "description": "Avenia is unavailable or returned an invalid response." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Submit API-driven Avenia KYB", + "tags": ["KYC and KYB"] + } + }, + "/v1/brla/kyb/new-level-1/web-sdk": { + "post": { + "description": "Starts the existing provider-hosted Avenia Level 1 KYB flow for an owned company subaccount.", + "operationId": "startAveniaKybLevel1Hosted", + "parameters": [{ "in": "query", "name": "subAccountId", "required": true, "schema": { "type": "string" } }], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaKybHostedResponse" } } }, + "description": "Hosted KYB URLs created." + }, + "400": { "description": "Invalid company account or KYB state." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Subaccount not found." }, + "409": { "description": "KYB is already active, approved, or conflicts with an API submission." }, + "502": { "description": "Avenia is unavailable or returned an invalid response." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Start hosted Avenia KYB", + "tags": ["KYC and KYB"] + } + }, + "/v1/brla/kyb/ubos": { + "post": { + "description": "Registers a UBO after verifying that referenced identity documents are ready and owned by the company subaccount.", + "operationId": "createAveniaKybUbo", + "parameters": [{ "in": "query", "name": "subAccountId", "required": true, "schema": { "type": "string" } }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaUboPayload" } } }, + "required": true + }, + "responses": { + "201": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AveniaUboResponse" } } }, + "description": "UBO registered." + }, + "400": { "description": "Invalid UBO or document state." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." }, + "404": { "description": "Subaccount or referenced document not found." }, + "409": { "description": "A referenced document is not ready." }, + "502": { "description": "Avenia is unavailable or returned an invalid response." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Create Avenia KYB UBO", + "tags": ["KYC and KYB"] + } + }, + "/v1/brla/kyc/record-attempt": { + "post": { + "description": "Records the initial Avenia KYC consultation for the effective profile.", + "operationId": "recordInitialAveniaKycAttempt", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordInitialKycAttemptRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { "application/json": { "schema": { "additionalProperties": false, "type": "object" } } }, + "description": "Attempt recorded." + }, + "400": { "description": "Invalid request." }, + "401": { "description": "Authentication required." }, + "403": { "description": "Managed profile or corridor is not authorized." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Record initial Avenia KYC attempt", + "tags": ["KYC and KYB"] + } + }, "/v1/brla/newKyc": { "post": { "deprecated": false, - "description": "Submits the user's KYC level 1 payload to Avenia after documents have been uploaded via `/v1/brla/getUploadUrls`. Includes a built-in 5-second delay to allow upstream document propagation.\n\n**Auth:** uses `optionalAuth`.", + "description": "Submits the owned user's KYC Level 1 payload after documents have been uploaded via `/v1/brla/getUploadUrls`.\n\n**Auth:** Supabase Bearer or profile-bound secret API key.", "operationId": "brlaNewKyc", "requestBody": { "content": { @@ -2282,7 +3588,7 @@ "headers": {} } }, - "security": [], + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], "summary": "Submit KYC level 1 data", "tags": ["Account Management"] } @@ -2326,11 +3632,6 @@ "description": "Missing or invalid pix key.", "headers": {} }, - "401": { - "content": {}, - "description": "Supabase Bearer required.", - "headers": {} - }, "500": { "content": { "application/json": { @@ -2344,7 +3645,7 @@ } }, "security": [], - "summary": "Validate Pix key", + "summary": "Validate PIX key", "tags": ["Account Management"] } }, @@ -2392,6 +3693,90 @@ "tags": ["Account Management"] } }, + "/v1/onboarding/active-entity": { + "put": { + "description": "Selects the authenticated profile's immutable active customer-entity type. Managed-child delegation is not supported.", + "operationId": "selectActiveCustomerEntity", + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SelectActiveCustomerEntityRequest" } } }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/SelectActiveCustomerEntityResponse" } } + }, + "description": "Active customer entity selected." + }, + "400": { "description": "Invalid customer-entity type." }, + "401": { "description": "Supabase Bearer authentication required." }, + "404": { "description": "No active owned entity of the requested type exists." }, + "409": { "description": "The selection conflicts with an existing selection or is ambiguous." }, + "500": { "description": "Selection could not be completed." } + }, + "security": [{ "BearerAuth": [] }], + "summary": "Select active customer entity", + "tags": ["KYC and KYB"] + } + }, + "/v1/onboarding/requirements": { + "get": { + "description": "Returns versioned field, document, and ordered-operation metadata for an existing Avenia or Alfredpay onboarding flow. This endpoint does not return profile state or customer PII and does not replace the provider-specific operations it references. Monerium is outside this discovery proposal.", + "operationId": "getOnboardingRequirements", + "parameters": [ + { + "in": "query", + "name": "country", + "required": true, + "schema": { "enum": ["AR", "BR", "CO", "MX", "US"], "type": "string" } + }, + { + "in": "query", + "name": "customerType", + "required": true, + "schema": { "enum": ["individual", "business"], "type": "string" } + } + ], + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OnboardingRequirementsResponse" } } }, + "description": "Requirements and existing operation sequence." + }, + "400": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/OnboardingRequirementsErrorResponse" } } + }, + "description": "Missing or invalid query." + }, + "404": { + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/OnboardingRequirementsErrorResponse" } } + }, + "description": "No published flow exists for the country and customer type." + } + }, + "security": [], + "summary": "Discover KYC or KYB requirements", + "tags": ["KYC and KYB", "Reference Data"] + } + }, + "/v1/onboarding/status": { + "get": { + "description": "Returns the effective profile's aggregate customer entities, provider accounts, and KYC/KYB cases. A manager may select an authorized child with `X-Managed-Profile-Id`.", + "operationId": "getOnboardingStatus", + "responses": { + "200": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OnboardingStatusResponse" } } }, + "description": "Aggregate onboarding status." + }, + "401": { "description": "Authentication required." }, + "500": { "description": "Onboarding state could not be read." } + }, + "security": [{ "SecretApiKey": [] }, { "BearerAuth": [] }], + "summary": "Get aggregate onboarding status", + "tags": ["KYC and KYB"] + } + }, "/v1/public-key": { "get": { "deprecated": false, @@ -4617,6 +6002,10 @@ "description": "User account, KYC, and BRLA subaccount operations.", "name": "Account Management" }, + { + "description": "Provider-specific identity and business verification operations and discovery metadata.", + "name": "KYC and KYB" + }, { "description": "Email OTP sign-in and user-linked API key provisioning.", "name": "Authentication" diff --git a/docs/api/pages/10-sandbox.md b/docs/api/pages/10-sandbox.md index 44a9d0cdb..1d7ea45a4 100644 --- a/docs/api/pages/10-sandbox.md +++ b/docs/api/pages/10-sandbox.md @@ -33,10 +33,7 @@ To simplify testing, we have pre-configured accounts that are already whiteliste ### Euro Onramps - **Login Method**: Sign in using an EVM wallet. -- **Test Wallet**: - - Public Address: `0x6f64A6a3eBB0Fa2F265bB173407cb2A90AE0D32f` - - Recovery Phrase: `sword joke bomb old couch junior dumb need story grace spirit casual` -- **Note**: This wallet is pre-loaded with testnet funds. +- **Test Wallet**: Use your own test wallet and fund it from a public testnet faucet. Never use or publish a shared recovery phrase. ### Euro Offramps - **Login Method**: Use an email address. diff --git a/docs/api/scripts/check-openapi.ts b/docs/api/scripts/check-openapi.ts index 3118a7960..bfe68b78e 100644 --- a/docs/api/scripts/check-openapi.ts +++ b/docs/api/scripts/check-openapi.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from "node:fs"; +import { ONBOARDING_REQUIREMENTS } from "../../../packages/shared/src/endpoints/onboarding-requirements.endpoints"; const OPENAPI_FILE = "docs/api/openapi/vortex.openapi.json"; const MANIFEST_FILE = "docs/api/apidog/page-manifest.json"; @@ -6,14 +7,41 @@ const MANIFEST_FILE = "docs/api/apidog/page-manifest.json"; const REQUIRED_PATHS = [ "/v1/api-credentials", "/v1/api-credentials/{credentialId}", + "/v1/alfredpay/alfredpayStatus", + "/v1/alfredpay/createBusinessCustomer", + "/v1/alfredpay/createIndividualCustomer", + "/v1/alfredpay/findKybCustomerAndBusiness", + "/v1/alfredpay/getKybRedirectLink", + "/v1/alfredpay/getKycRedirectLink", + "/v1/alfredpay/getKycStatus", + "/v1/alfredpay/kycRedirectFinished", + "/v1/alfredpay/kycRedirectOpened", + "/v1/alfredpay/retryKyc", + "/v1/alfredpay/sendKybSubmission", + "/v1/alfredpay/sendKycSubmission", + "/v1/alfredpay/submitKybFile", + "/v1/alfredpay/submitKybInformation", + "/v1/alfredpay/submitKybRelatedPersonFile", + "/v1/alfredpay/submitKycFile", + "/v1/alfredpay/submitKycInformation", "/v1/brla/createSubaccount", "/v1/brla/getKycStatus", "/v1/brla/getSelfieLivenessUrl", "/v1/brla/getUploadUrls", "/v1/brla/getUser", "/v1/brla/getUserRemainingLimit", + "/v1/brla/kyb/attempt-status", + "/v1/brla/kyb/documents", + "/v1/brla/kyb/documents/{documentId}", + "/v1/brla/kyb/new-level-1/api", + "/v1/brla/kyb/new-level-1/web-sdk", + "/v1/brla/kyb/ubos", + "/v1/brla/kyc/record-attempt", "/v1/brla/newKyc", "/v1/brla/validatePixKey", + "/v1/onboarding/requirements", + "/v1/onboarding/active-entity", + "/v1/onboarding/status", "/v1/public-key", "/v1/quotes", "/v1/quotes/best", @@ -149,6 +177,41 @@ if (unresolvedRefs.length > 0) { throw new Error(`OpenAPI file has unresolved local refs:\n${unresolvedRefs.join("\n")}`); } +const documentedOperations = new Map(); +for (const [path, pathItem] of Object.entries(openapi.paths as JsonObject)) { + if (!pathItem || typeof pathItem !== "object") continue; + for (const [method, operation] of Object.entries(pathItem as JsonObject)) { + if (!operation || typeof operation !== "object") continue; + const operationId = (operation as JsonObject).operationId; + if (typeof operationId !== "string") continue; + if (documentedOperations.has(operationId)) { + throw new Error(`OpenAPI operationId is duplicated: ${operationId}`); + } + documentedOperations.set(operationId, { method: method.toUpperCase(), path }); + } +} + +for (const flows of Object.values(ONBOARDING_REQUIREMENTS)) { + for (const requirements of Object.values(flows)) { + if (!requirements) continue; + for (const step of requirements.steps) { + if (step.kind !== "api" || !step.operationId || !step.method || !step.path) continue; + const operation = documentedOperations.get(step.operationId); + if (!operation) { + throw new Error(`Onboarding discovery references missing OpenAPI operationId: ${step.operationId}`); + } + if (operation.method !== step.method || operation.path !== step.path) { + throw new Error( + `Onboarding discovery operation ${step.operationId} maps to ${step.method} ${step.path}, not ${operation.method} ${operation.path}` + ); + } + if (step.requestSchema && !pointerExists(openapi, step.requestSchema)) { + throw new Error(`Onboarding discovery references missing OpenAPI schema: ${step.requestSchema}`); + } + } + } +} + const manifest = readJson(MANIFEST_FILE); if (!Array.isArray(manifest.pages)) { throw new Error(`${MANIFEST_FILE} must contain a pages array.`); diff --git a/docs/api/wire-contract.snapshot.md b/docs/api/wire-contract.snapshot.md index 951851537..3c4e7ae7e 100644 --- a/docs/api/wire-contract.snapshot.md +++ b/docs/api/wire-contract.snapshot.md @@ -127,7 +127,6 @@ AlfredpayGetKybStatusResponse: { AlfredpayGetKycRedirectLinkRequest: { country: string; - type?: AlfredpayCustomerType.BUSINESS | AlfredpayCustomerType.INDIVIDUAL; } AlfredpayGetKycRedirectLinkResponse: { @@ -210,6 +209,7 @@ AlfredpayRetryKycRequest: { AlfredpayStatusRequest: { country: string; + type?: AlfredpayCustomerType.BUSINESS | AlfredpayCustomerType.INDIVIDUAL; } AlfredpayStatusResponse: { @@ -386,7 +386,7 @@ AveniaKYCDataUpload: { } AveniaKYCDataUploadRequest: { - documentType: enum AveniaDocumentType { DRIVERS_LICENSE = "DRIVERS-LICENSE", ID = "ID", PASSPORT = "PASSPORT", SELFIE = "SELFIE", SELFIE_FROM_LIVENESS = "SELFIE-FROM-LIVENESS" }; + documentType: enum AveniaDocumentType { CERTIFICATE_OF_INCORPORATION = "CERTIFICATE-OF-INCORPORATION", COMPANY_TAX_IDENTIFICATION_DOCUMENT = "COMPANY-TAX-IDENTIFICATION-DOCUMENT", DRIVERS_LICENSE = "DRIVERS-LICENSE", ID = "ID", PASSPORT = "PASSPORT", RESIDENCE_PERMIT = "RESIDENCE-PERMIT", SELFIE = "SELFIE", SELFIE_FROM_LIVENESS = "SELFIE-FROM-LIVENESS" }; isDoubleSided?: boolean; taxId: string; } @@ -427,7 +427,7 @@ BrlaGetKycStatusRequest: { BrlaGetKycStatusResponse: { failureReason?: KycFailureReason.BIRTHDATE | KycFailureReason.FACE | KycFailureReason.NAME | KycFailureReason.TAX_ID | KycFailureReason.UNKNOWN; level: string; - result: enum KycAttemptResult { APPROVED = "APPROVED", REJECTED = "REJECTED" }; + result?: KycAttemptResult.APPROVED | KycAttemptResult.REJECTED; status: enum KycAttemptStatus { COMPLETED = "COMPLETED", EXPIRED = "EXPIRED", PENDING = "PENDING", PROCESSING = "PROCESSING" }; type: "KYC"; } @@ -475,7 +475,7 @@ BrlaGetUserResponse: { BrlaKYCDocType: enum BrlaKYCDocType { CNH = "CNH", RG = "RG" } BrlaPostRecordInitialKycAttemptRequest: { - quoteId: string; + quoteId?: string; sessionId?: string; taxId: string; } @@ -640,6 +640,56 @@ FiatCurrency: FiatToken.ARS | FiatToken.BRL | FiatToken.COP | FiatToken.EURC | F FlowType: OfframpHandlerType.ASSETHUB_TO_BRLA | OfframpHandlerType.EVM_TO_BRLA | OnrampHandlerType.BRLA_TO_ASSETHUB | OnrampHandlerType.BRLA_TO_EVM +GetOnboardingRequirementsErrorResponse: { + error: { + code: "INVALID_ONBOARDING_REQUIREMENTS_QUERY" | "ONBOARDING_REQUIREMENTS_NOT_FOUND"; + message: string; + status: 400 | 404; + }; +} + +GetOnboardingRequirementsResponse: { + country: "AR" | "BR" | "CO" | "MX" | "US"; + customerType: "business" | "individual"; + documentationUrl: string; + documents: Array<{ + acceptedMediaTypes?: Array; + collection?: "direct-upload" | "hosted"; + description?: string; + required: boolean; + requiredWhen?: string; + type: string; + }>; + fields: Array<{ + allowedValues?: Array; + description?: string; + format?: string; + path: string; + required: boolean; + requiredWhen?: string; + type: "array" | "boolean" | "number" | "string"; + }>; + flow: string; + mode: "api" | "hosted" | "hybrid"; + openapiUrl: string; + provider: "alfredpay" | "avenia"; + requirementsVersion: string; + steps: Array<{ + condition?: string; + derivedValues?: Record; + description: string; + fixedBody?: Record; + fixedQuery?: Record; + kind: "api" | "direct-upload" | "hosted"; + method?: "GET" | "POST" | "PUT"; + operationId?: string; + order: number; + path?: string; + repeatFor?: string; + requestSchema?: string; + }>; +} + GetQuoteRequest: { id: string; } @@ -998,8 +1048,92 @@ MoonpayPriceResponse: { totalFee: number; } +ONBOARDING_REQUIREMENTS: Record<"AR" | "BR" | "CO" | "MX" | "US", Partial; + collection?: "direct-upload" | "hosted"; + description?: string; + required: boolean; + requiredWhen?: string; + type: string; + }>; + fields: Array<{ + allowedValues?: Array; + description?: string; + format?: string; + path: string; + required: boolean; + requiredWhen?: string; + type: "array" | "boolean" | "number" | "string"; + }>; + flow: string; + mode: "api" | "hosted" | "hybrid"; + openapiUrl: string; + provider: "alfredpay" | "avenia"; + requirementsVersion: string; + steps: Array<{ + condition?: string; + derivedValues?: Record; + description: string; + fixedBody?: Record; + fixedQuery?: Record; + kind: "api" | "direct-upload" | "hosted"; + method?: "GET" | "POST" | "PUT"; + operationId?: string; + order: number; + path?: string; + repeatFor?: string; + requestSchema?: string; + }>; +}>>> + OfframpHandlerType: enum OfframpHandlerType { ASSETHUB_TO_BRLA = "assethub-to-brla", EVM_TO_BRLA = "evm-to-brla" } +OnboardingDocumentRequirement: { + acceptedMediaTypes?: Array; + collection?: "direct-upload" | "hosted"; + description?: string; + required: boolean; + requiredWhen?: string; + type: string; +} + +OnboardingFlowMode: "api" | "hosted" | "hybrid" + +OnboardingRequirementField: { + allowedValues?: Array; + description?: string; + format?: string; + path: string; + required: boolean; + requiredWhen?: string; + type: "array" | "boolean" | "number" | "string"; +} + +OnboardingRequirementFieldType: "array" | "boolean" | "number" | "string" + +OnboardingRequirementStep: { + condition?: string; + derivedValues?: Record; + description: string; + fixedBody?: Record; + fixedQuery?: Record; + kind: "api" | "direct-upload" | "hosted"; + method?: "GET" | "POST" | "PUT"; + operationId?: string; + order: number; + path?: string; + repeatFor?: string; + requestSchema?: string; +} + +OnboardingRequirementsCountry: "AR" | "BR" | "CO" | "MX" | "US" + +OnboardingStepKind: "api" | "direct-upload" | "hosted" + OnrampHandlerType: enum OnrampHandlerType { BRLA_TO_ASSETHUB = "brla-to-assethub", BRLA_TO_EVM = "brla-to-evm" } PaymentData: { @@ -2312,6 +2446,48 @@ WebhookPayloadBase: { transactionType: enum RampDirection { BUY = "BUY", SELL = "SELL" }; } +getOnboardingRequirements: (country: "AR" | "BR" | "CO" | "MX" | "US", customerType: "business" | "individual") => undefined | { + country: "AR" | "BR" | "CO" | "MX" | "US"; + customerType: "business" | "individual"; + documentationUrl: string; + documents: Array<{ + acceptedMediaTypes?: Array; + collection?: "direct-upload" | "hosted"; + description?: string; + required: boolean; + requiredWhen?: string; + type: string; + }>; + fields: Array<{ + allowedValues?: Array; + description?: string; + format?: string; + path: string; + required: boolean; + requiredWhen?: string; + type: "array" | "boolean" | "number" | "string"; + }>; + flow: string; + mode: "api" | "hosted" | "hybrid"; + openapiUrl: string; + provider: "alfredpay" | "avenia"; + requirementsVersion: string; + steps: Array<{ + condition?: string; + derivedValues?: Record; + description: string; + fixedBody?: Record; + fixedQuery?: Record; + kind: "api" | "direct-upload" | "hosted"; + method?: "GET" | "POST" | "PUT"; + operationId?: string; + order: number; + path?: string; + repeatFor?: string; + requestSchema?: string; + }>; +} + isEvmTransactionData: (data: Array<{ domain: { chainId?: number; diff --git a/docs/proposal-api-driven-kyc-kyb.md b/docs/proposal-api-driven-kyc-kyb.md new file mode 100644 index 000000000..244134927 --- /dev/null +++ b/docs/proposal-api-driven-kyc-kyb.md @@ -0,0 +1,291 @@ +# Proposal: API-Driven KYC and KYB + +Status: proposed direction, with the initial requirements discovery contract and KYC/KYB +OpenAPI coverage implemented. This document records the intended direction for API-driven +customer verification while preserving the provider-specific endpoints and workflows used by +current Vortex consumers. Last updated: 2026-08-10. + +Related material: + +- [`Proposal: Managed Headless Profiles`](proposal-headless-profiles-and-pricing-plans.md) +- [`Identity, Customer, and Partner Model`](architecture-identity-model.md) +- [`Vortex API Docs Source`](api/README.md) +- [`Avenia KYB Level 1 - API`](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) +- [`Avenia KYB Level 1 - Web SDK`](https://integration-guide.avenia.io/docs/KYB/kybLevel1) + +## Objective + +Allow a customer, or an authorized manager acting for a managed headless customer, to +complete corridor-supported KYC or KYB through the Vortex API without requiring the Vortex +Dashboard, Widget, or another Vortex UI where the provider supports an API-driven flow. + +Vortex will preserve the existing provider-specific endpoints, request contracts, and call +sequences. They already support the Dashboard and other first-party consumers, and replacing +them with a provider-neutral execution API would create migration risk without improving the +underlying provider workflows. + +To make these existing flows usable by external API clients, Vortex will expose a discovery +endpoint that describes the requirements and ordered operations for a country and customer +type. The discovery response guides an integrator through the existing API; it does not +introduce a second execution layer or alter the operations themselves. + +## Scope + +- Keep the current Avenia and Alfredpay endpoint families and their call order. +- Support both self-service profiles and manager-to-child delegated operations. +- Complete API credential support for provider operations that currently depend on a browser + session, where provider capabilities permit a headless flow. +- Publish machine-readable field, document, and operation requirements for each supported + country and customer type. +- Keep OpenAPI and corridor-specific integration documentation authoritative for complete + request and response contracts. +- Continue using the existing provider customer and KYC/KYB case records for ownership and + status tracking. +- Do not redesign Monerium or other provider flows as part of this proposal. + +"API-driven" means that an integrator can collect data in its own experience and perform the +workflow through Vortex API operations. Pre-signed document uploads and unavoidable identity +or liveness steps may still involve a provider-controlled URL, but the flow must not depend on +a Vortex UI. + +## Existing foundation + +The persistence model already separates the customer, provider account, and verification +attempt: + +```text +profile + -> customer entity + -> provider customer + -> KYC/KYB case +``` + +`provider_customers` owns the durable corridor/provider account, while `kyc_cases` owns a +verification attempt and its canonical status. This model remains unchanged. + +The public execution surface is intentionally provider-specific. Avenia and Alfredpay have +different request shapes, document handling, hosted-flow exceptions, statuses, and retry +rules. The Dashboard already orchestrates those sequences through shared KYC/KYB state +machines and reads the aggregated `GET /v1/onboarding/status` view. + +Avenia's API-based Level 1 KYB flow is the first headless business-verification slice. It uses +the existing BRLA route family to create or reuse the company subaccount, create and upload +company and UBO documents, register UBOs, submit the attempt, and track its result. It does not +need to become an adapter behind a new provider-neutral route family. + +Authentication, delegated authorization, and manager-to-child ownership are defined by the +[managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md). This +proposal applies those controls to the existing provider operations. + +## Endpoint preservation invariants + +- Existing Dashboard, Widget, and other first-party workflows must continue to work without + migrating to a new endpoint family or reordered sequence. +- Existing provider-specific endpoint names and request contracts remain compatibility + contracts. Changes follow the normal public API compatibility policy. +- Discovery metadata describes the existing operations; it does not proxy, combine, or + replace them. +- The authenticated profile, or authorized managed child, remains the owner of every provider + customer, verification case, and uploaded document. +- Delegated operations retain both the manager actor and child subject for authorization and + audit while keeping the child as resource owner. +- Provider-confirmed state remains authoritative. A client completion event cannot mark a + case approved. +- Side-effecting operations must define retry-safe behavior so a client timeout cannot + silently create duplicate provider-side resources or submissions. +- Requirements may differ by country, customer type, provider, and verification level. Vortex + will represent those differences explicitly instead of forcing them into a common request + schema. + +## Requirements discovery + +The discovery operation is: + +```http +GET /v1/onboarding/requirements?country=BR&customerType=business +``` + +`country` uses the ISO 3166-1 alpha-2 country code that selects the current onboarding flow. +`customerType` distinguishes individual KYC from business KYB. If a country supports multiple +verification levels, the final contract must also define how the requested or applicable level +is selected. + +The response contains: + +- a stable flow identifier and requirements version; +- required fields, formats, constraints, and conditional applicability; +- required document types, accepted media types, and multiplicity; +- the existing operations in their required order, identified by OpenAPI `operationId`, HTTP + method, and path; +- a stable, machine-readable URL for the OpenAPI document against which schema fragments + resolve; +- links or references to canonical OpenAPI request schemas; +- repeat or conditional execution rules where a step applies to each document or UBO; +- the corridor-specific integration guide; and +- any unavoidable hosted continuation, liveness, or asynchronous status behavior. + +For example: + +```json +{ + "country": "BR", + "customerType": "business", + "flow": "avenia-kyb-level-1-api", + "version": "2026-08-10", + "openapiUrl": "https://raw.githubusercontent.com/pendulum-chain/vortex/main/docs/api/openapi/vortex.openapi.json", + "requirements": { + "fields": [ + { "name": "legalName", "required": true, "type": "string" }, + { "name": "taxId", "required": true, "format": "cnpj" } + ], + "documents": [ + { + "type": "articlesOfAssociation", + "required": true, + "acceptedMediaTypes": ["application/pdf"] + } + ] + }, + "steps": [ + { + "order": 1, + "operationId": "createSubaccount", + "method": "POST", + "path": "/v1/brla/createSubaccount", + "requestSchema": "#/components/schemas/CreateSubaccountRequest" + }, + { + "order": 2, + "operationId": "createAveniaKybDocument", + "method": "POST", + "path": "/v1/brla/kyb/documents", + "repeatFor": "requirements.documents", + "requestSchema": "#/components/schemas/AveniaKybDocumentRequest" + }, + { + "order": 3, + "operationId": "submitAveniaKybLevel1", + "method": "POST", + "path": "/v1/brla/kyb/new-level-1/api", + "requestSchema": "#/components/schemas/AveniaKybLevel1Request" + } + ], + "documentationUrl": "https://api-docs.vortexfinance.co/kyb/br" +} +``` + +The example is illustrative rather than the final BR schema or complete BR sequence. The +implemented metadata must be derived from the actual route and provider requirements. + +The endpoint does not return populated request bodies or customer PII. Returning example +values as if they were executable bodies would blur required fields with actual customer data +and duplicate the OpenAPI contract. Clients use the referenced schema to construct each body +from data they collect. + +## Contract authority and synchronization + +The requirements response is a discovery index, not a second schema source. OpenAPI remains +authoritative for each operation's complete request, response, and error contract. The +corridor-specific guide remains authoritative for behavioral details such as sequencing, +branching, retries, custody, and asynchronous completion. + +The OpenAPI document covers the existing Avenia and Alfredpay KYC/KYB operations advertised by +discovery, including API credential and managed-profile authentication. Discovery links to the +reviewed repository document through its stable raw GitHub URL while Apidog remains the +human-facing endpoint catalog. + +Every published discovery step must resolve to an OpenAPI operation and request schema. CI +fails when a referenced operation or schema is absent or when its method or path differs from +the mapping. Requirement metadata, runtime validators, OpenAPI, and documentation must be +reviewed together when a provider requirement changes. + +The requirements version identifies the exact metadata revision returned to a client. The +implementation must define its compatibility policy before release, including whether old +versions remain queryable and how clients learn that a previously fetched version is no longer +accepted. + +## Authentication and state + +Requirements that contain only public provider and corridor metadata may be exposed without +authentication. The final design must confirm that responses contain no private provider +configuration, account state, or PII before making the endpoint public. + +Requirements discovery is not a profile status API. It describes the complete flow for a +country and customer type, not which steps a particular profile has completed. Clients must use +the existing provider status operations and `GET /v1/onboarding/status` where applicable. +Profile-specific next-action guidance is outside this proposal and may be considered later if +static requirements plus documented status behavior prove insufficient. + +## Consequences + +### Benefits + +- Existing first-party consumers avoid a risky endpoint and state-machine migration. +- External API clients can discover required data, documents, and call order without reverse + engineering a Dashboard workflow. +- OpenAPI schemas and integration guides remain reusable instead of embedding full duplicate + request bodies in discovery responses. +- Provider-specific differences remain visible and accurately modeled. +- A requirements version gives clients and Vortex support a concrete contract revision to + discuss when provider rules change. + +### Costs and constraints + +- Integrators must implement country- and provider-specific operation sequences, statuses, + errors, retries, and hosted-flow branches. Vortex does not provide one portable execution + client across providers. +- Publishing an ordered operation makes that route and its position in the flow part of the + external compatibility surface. +- Discovery metadata can drift from route validation, OpenAPI, or provider behavior unless the + sources share validation and CI checks. +- Completing and maintaining KYC/KYB OpenAPI coverage becomes a prerequisite for reliable + discovery. +- Static discovery cannot account for a profile's current progress or every provider-requested + remediation. Status responses and provider-specific documentation remain necessary. +- Provider requirement changes need an explicit versioning and compatibility process rather + than an undocumented metadata replacement. + +## Delivery order + +1. Preserve and complete API credential and managed-profile authorization for each existing + provider-specific operation required by supported headless flows. Implemented. +2. Reconcile the public OpenAPI document with the implemented Avenia and Alfredpay KYC/KYB + endpoints, authentication, request schemas, responses, and errors, then publish it at a + stable machine-readable URL. Implemented. +3. Define the requirements response schema and synchronization checks. Implemented. The + long-term version compatibility policy remains open. +4. Implement requirements discovery for Avenia Level 1 KYC/KYB in Brazil using the existing + BRLA operations and sequence. Implemented. +5. Add Alfredpay countries using their existing API-based or hosted-flow operations without + renaming or reordering those routes. Implemented for AR, CO, MX, and US product-supported + customer types. +6. Publish corridor-specific guides and examples, then add SDK discovery conveniences only + where they reduce integration work without hiding provider-specific behavior. + +## Non-goals + +- A provider-neutral KYC/KYB execution endpoint family. +- A common request body, document resource, status vocabulary, or retry operation across all + providers. +- Reordering, combining, proxying, or retiring existing provider-specific operations. +- Migrating the Dashboard, Widget, or shared KYC/KYB state machines to a new workflow. +- Letting callers select arbitrary provider accounts or write compliance decisions. +- Returning customer PII or pre-populated executable request bodies from requirements + discovery. +- Profile-specific next-action orchestration. + +## Open decisions + +- Should discovery use `country=BR`, which matches current onboarding selection, or a fiat + corridor such as `corridor=BRL`? The contract must use one term consistently. +- How is a verification level selected when a country supports more than one level? +- Is static requirements metadata safe and useful to expose without authentication? +- Which JSON Schema vocabulary will represent conditional fields, repeated UBO data, and file + requirements without duplicating complete OpenAPI request schemas? +- How long are old requirements versions queryable, and what response tells a client that a + version has expired? +- Which CI check guarantees that every discovery `operationId` and schema reference exists in + the reviewed OpenAPI document? +- Should operation metadata be maintained beside runtime validators and generated into both + discovery and OpenAPI in the future, or is reviewed manual synchronization sufficient for + the first release? diff --git a/docs/proposal-unified-kyc-kyb-api.md b/docs/proposal-unified-kyc-kyb-api.md deleted file mode 100644 index c92b6fd9a..000000000 --- a/docs/proposal-unified-kyc-kyb-api.md +++ /dev/null @@ -1,160 +0,0 @@ -# Proposal: Unified KYC and KYB API - -Status: proposed, early discussion draft. This document currently seeks agreement on -scope, invariants, and delivery order. Exact routes, schemas, and provider-specific field -contracts remain open. Last updated: 2026-08-06. - -Related material: - -- [`Proposal: Managed Headless Profiles`](proposal-headless-profiles-and-pricing-plans.md) -- [`Identity, Customer, and Partner Model`](architecture-identity-model.md) -- [`Avenia KYB Level 1 - API`](https://integration-guide.avenia.io/docs/KYB/kybLevel1Api) -- [`Avenia KYB Level 1 - Web SDK`](https://integration-guide.avenia.io/docs/KYB/kybLevel1) - -## Objective - -Allow a customer, or an authorized manager acting for a managed headless customer, to -complete corridor-supported KYC or KYB through the Vortex API without requiring the -Vortex dashboard, widget, or a provider-hosted onboarding UI. - -In parallel, replace the current collection of provider-named onboarding endpoints with -the smallest practical common API. The API should select the provider from the corridor -and customer type, expose Vortex-owned verification resources and canonical statuses, -and retain provider-specific input only where the underlying checks genuinely differ. - -The first delivery item is Avenia's new API-based Level 1 KYB flow. Today, Vortex starts -Avenia company KYB through the Web SDK endpoint and sends the customer to separate hosted -company and representative URLs. The new provider flow lets Vortex submit company data, -UBOs, and documents server-to-server and track the resulting attempt. - -Avenia is the first vertical slice, not the scope of the unified API. The common envelope -and lifecycle must continue to accommodate Alfredpay and future integrators without making -their callers depend on Avenia-specific routing or identifiers. - -## Initial scope - -- Focus on Avenia and Alfredpay. -- Start with Avenia Level 1 KYB for the BRL corridor. -- Support both self-service profiles and manager-to-child delegated operations as the - managed-headless-profile authorization work becomes available. -- Keep the resource model and provider boundary suitable for another future integrator. -- Do not redesign Monerium or other provider flows in this proposal. -- Do not let callers approve a case, override a provider decision, or write canonical - compliance status directly. - -"API-driven" means that an integrator can collect data in its own experience and perform -the workflow through Vortex API operations. Pre-signed document uploads and unavoidable -identity/liveness steps may still involve a provider-controlled URL, but the flow must not -depend on a Vortex UI. - -## Existing foundation - -The persistence model is already mostly provider-neutral: - -```text -profile - -> customer entity - -> provider customer - -> KYC/KYB case -``` - -`provider_customers` owns the durable corridor/provider account, while `kyc_cases` owns a -verification attempt and its canonical `started`, `pending`, `in_review`, `approved`, or -`rejected` status. This model should be reused rather than introducing a second onboarding -or compliance identity. - -The current API is less unified than the storage model: - -- most Avenia and all Alfredpay KYC/KYB routes require a Supabase browser session; -- route names, request shapes, document handling, retries, and status responses expose - provider workflow details; -- the dashboard orchestrates separate provider XState machines and polls the aggregated - `GET /v1/onboarding/status` read model. - -Authentication, delegated authorization, and manager-to-child ownership are defined by -the [managed-headless-profiles proposal](proposal-headless-profiles-and-pricing-plans.md) -and are not repeated here. This proposal defines the verification workflow applied after -the operation profile has been resolved. - -## Tentative generic flow - -The ideal API exposes the workflow as discoverable stages instead of requiring an -integrator to know a provider's sequence in advance: - -1. **Discover requirements.** The caller requests the requirements for a KYC or KYB by - corridor and customer type. Vortex derives the provider and returns an overview of the - required data fields, document types, and any liveness or selfie requirement. The - requirements are provider- and country-specific even though their envelope is common. - The response includes a stable requirements version. -2. **Create the attempt and submit initial data.** The caller creates a verification case - with the structured data already available, such as personal or company name, address, - tax information, representatives, or beneficial owners. The exact fields follow the - requirements returned for that corridor, and the case pins that requirements version. -3. **Upload documents when required.** The caller creates and uploads each required - document using the mechanism supported by Vortex for that provider. Vortex creates and - returns its own stable identifier for each document or document batch before upload; - upstream identifiers are stored only as internal mappings. -4. **Complete liveness or selfie evidence when required.** The case may return a liveness - continuation step or accept a selfie document upload, depending on the provider and - country. -5. **Submit and track the case.** Once all required stages are complete, Vortex submits or - finalizes the provider attempt and exposes its canonical status until it is approved, - rejected, or requires another supported action. - -Not every provider needs every stage. The requirements response determines which stages -apply and gives API clients enough information to build their own collection experience -without embedding Vortex's dashboard workflow. - -## API principles - -- The server derives the provider from corridor and customer type. A caller cannot select - an arbitrary provider account or provider case belonging to another subject. -- Public responses use Vortex case identifiers and canonical status. Provider identifiers - stay internal unless a specific continuation step requires an opaque reference. -- Provider-specific data is represented explicitly rather than forced into a misleading - lowest-common-denominator schema. -- Document operations are scoped to the subject, provider customer, case, and expected - document type before Vortex issues an upload target or forwards content. -- Provider-confirmed state remains authoritative. Client completion events cannot mark a - case approved. -- Delegated operations retain both the manager actor and child subject for authorization - and audit while keeping the child as the resource owner. -- Case creation, document submission, and final submission define retry-safe behavior so - a client timeout cannot silently create duplicate provider-side effects. - -## Delivery order - -1. Implement Avenia Level 1 KYB through its API flow: create or reuse the company - subaccount, create and upload company and UBO documents, register UBOs, submit the KYB - attempt, and synchronize its result into the existing provider customer and KYB case. -2. Use that vertical slice to define the common Vortex case lifecycle and operations for - starting, continuing, submitting, reading, and retrying verification. -3. Make those operations available to self-service API credentials and to manager - credentials acting on an authorized managed child, without changing resource ownership. -4. Adapt Alfredpay's API-based KYC/KYB flows to the same lifecycle while retaining its - corridor-specific forms, document sets, and hosted-flow exceptions. -5. Migrate first-party UI consumers, then retire provider-named public onboarding routes - only after compatibility requirements are known. - -The first item must not wait for the complete cross-provider API design. It should reuse -the current canonical tables and status rules so the Avenia work becomes the first adapter -behind the unified API rather than a parallel compliance model. - -The first vertical slice does not need to settle every cross-provider resource or webhook -decision. It must preserve the common envelope, pin the requirements version, use Vortex -resource identifiers, keep provider identifiers internal, and make external side effects -safe to retry. - -## First open decisions - -- What is the smallest common resource shape: one provider customer with a current case, - or an explicit append-only list of attempts? -- Which operation vocabulary fits both providers without hiding meaningful differences? -- Should Vortex proxy document bytes, issue provider pre-signed upload URLs, or support - both patterns behind one document resource? -- Which contact data must be supplied as case data when a provider requires it? -- Which status changes should produce API webhooks so headless callers do not have to - poll indefinitely? - -The next revision should answer these questions before fixing exact endpoint paths or -request schemas. diff --git a/docs/security-spec/05-integrations/alfredpay.md b/docs/security-spec/05-integrations/alfredpay.md index 38e7ef6b8..6cf905d5f 100644 --- a/docs/security-spec/05-integrations/alfredpay.md +++ b/docs/security-spec/05-integrations/alfredpay.md @@ -45,7 +45,7 @@ For routed Alfredpay onramps (any non-passthrough output), the final quote outpu **Request validation:** Alfredpay middleware (`alfredpay.middleware.ts`) validates the `country` parameter against the `AlfredPayCountry` enum for all Alfredpay-related requests. -**Customer, KYC/KYB, and fiat-account routes:** These routes accept either a Supabase Bearer token or a user-scoped secret API key via `requirePartnerOrUserAuth()`. The controller resolves the effective profile so a manager-selected child or direct child credential uses the child's provider records. Managed-child mutations require the controlling manager's current country corridor. Customer creation uses the child's immutable managed-profile contact email and provisioned entity type; it never inherits the manager's login email. Fiat-account PII is passed directly to Alfredpay and is not persisted in Vortex's database, browser storage, analytics, or notifications. Provider 4xx rejections are sanitized before reaching callers; provider 5xx and transport failures remain opaque. +**Customer, KYC/KYB, and fiat-account routes:** These routes accept either a Supabase Bearer token or a user-scoped secret API key via `requirePartnerOrUserAuth()`. The controller resolves the effective profile so a manager-selected child or direct child credential uses the child's provider records. `GET /alfredpayStatus` accepts an optional `type` selector so headless business flows resolve the business customer explicitly; omitting it retains the active-entity lookup used by existing UI consumers. Managed-child mutations require the controlling manager's current country corridor. Customer creation uses the child's immutable managed-profile contact email and provisioned entity type; it never inherits the manager's login email. Fiat-account PII is passed directly to Alfredpay and is not persisted in Vortex's database, browser storage, analytics, or notifications. Provider 4xx rejections are sanitized before reaching callers; provider 5xx and transport failures remain opaque. ## Security Invariants diff --git a/docs/security-spec/07-operations/api-surface.md b/docs/security-spec/07-operations/api-surface.md index 436496056..7f065e220 100644 --- a/docs/security-spec/07-operations/api-surface.md +++ b/docs/security-spec/07-operations/api-surface.md @@ -69,6 +69,7 @@ This spec covers the external-facing attack surface of the Vortex API (`apps/api 24. **Managed-profile context MUST be route-authorized** — On supported child-oriented routes, `X-Managed-Profile-Id` is a selector only. A Supabase session or secret API credential establishes the manager actor; middleware verifies active manager configuration, a direct active relationship, the managed child and its single active customer entity, and every required corridor before attaching an immutable actor/subject context. Public API keys cannot establish the manager actor, raw headers never alter `req.userId`, and ownership checks use the verified child subject. A child-owned credential instead authenticates directly as its child and dynamically derives the same controlling relationship and current corridors; it cannot select another child. Manager or relationship deactivation and corridor removal block authorization decisions begun after the committed policy change but do not cancel already-authorized requests in flight. The header is explicitly CORS-allowlisted for browser-based manager sessions, and relationship authorization precedes multipart buffering on delegated upload routes. 25. **Headless profile lifecycle MUST fail closed** — Manager lifecycle routes derive the manager from a Supabase session or profile-bound secret credential and require its current manager configuration to be active. Creation requires an immutable provider contact email separate from the child's null login email; normalized contact emails are unique and permanently reserved within each manager. Child reads, credential management, and deletion are scoped by both manager and child profile IDs so foreign relationships are indistinguishable from missing rows. Only the manager-scoped child-credential route may issue credentials for a managed subject; generic profile-managed and admin partner-managed creation reject them. Credential creation and logical deletion lock the child profile and relationship in a common order; deletion is idempotent, revokes child credentials in the same transaction, and leaves retained provider, KYC, quote, ramp, and callback state intact. Managed profiles cannot create a second customer-entity type after provisioning. 26. **Unsupported managed operations MUST fail explicitly** — Recipient invitation routes reject `X-Managed-Profile-Id` rather than silently applying the request to the manager. Direct child credentials are rejected from webhook and manager lifecycle routes. Managed children have one immutable active customer entity from provisioning, so `PUT /v1/onboarding/active-entity` is not a delegated child operation. +27. **Public onboarding discovery MUST expose metadata only** — `GET /v1/onboarding/requirements` is unauthenticated and returns only the reviewed static Avenia/Alfredpay product matrix, field/document requirements, public route metadata, and documentation/OpenAPI links. It MUST NOT inspect profile state, return customer or provider identifiers, accept an owner selector, or advertise unsupported combinations such as AR business or Monerium flows. Every advertised API step and request-schema fragment is checked against the reviewed OpenAPI document so stale mappings fail the documentation gate. ## Threat Vectors & Mitigations @@ -107,6 +108,7 @@ This spec covers the external-facing attack surface of the Vortex API (`apps/api - [x] Verify no route accidentally uses `publicKeyAuth` (public key only, no secret key) for operations that should require `apiKeyAuth` (secret key). **PASS** — auth middleware usage reviewed per route. - [ ] Verify controllers do not pass raw `req.body` to database operations — check for Sequelize `.create(req.body)` or `.update(req.body)` patterns. **N/A** — deferred; requires comprehensive Sequelize usage audit. - [x] Verify no endpoint returns `process.env`, server config, or internal paths in responses. **PASS** — no endpoint exposes internal configuration. +- [x] Public onboarding requirements expose static metadata only, reject unsupported country/customer-type combinations, and have no authentication-derived response branch. **PASS** — the controller reads only the reviewed shared mapping and OpenAPI CI verifies every advertised operation and schema reference. - [ ] Check whether Supabase auth cookies use `SameSite=Strict` or `SameSite=Lax` — and whether CSRF tokens are required for state-changing operations. **PARTIAL** — cookie parser enabled but cookie attributes not explicitly configured for `SameSite`. - [x] Verify the 404 handler does not reveal Express version or framework information. **PASS** — custom 404 handler returns generic JSON error. - [x] Check all 27 route files for endpoints that accept file uploads — verify file size limits and type validation if present. **PASS** — no file upload endpoints found. diff --git a/packages/shared/src/endpoints/alfredpay.endpoints.ts b/packages/shared/src/endpoints/alfredpay.endpoints.ts index 3ae28695b..4fd5651e9 100644 --- a/packages/shared/src/endpoints/alfredpay.endpoints.ts +++ b/packages/shared/src/endpoints/alfredpay.endpoints.ts @@ -12,6 +12,7 @@ import { // GET /alfredpay/alfredpayStatus?country=:country export interface AlfredpayStatusRequest { country: string; + type?: AlfredpayCustomerType; } export interface AlfredpayStatusResponse { @@ -32,7 +33,6 @@ export interface AlfredpayCreateCustomerResponse { // GET /alfredpay/getKycRedirectLink?country=:country export interface AlfredpayGetKycRedirectLinkRequest { country: string; - type?: AlfredpayCustomerType; } export type AlfredpayGetKycRedirectLinkResponse = GetKycRedirectLinkResponse; diff --git a/packages/shared/src/endpoints/brla.endpoints.ts b/packages/shared/src/endpoints/brla.endpoints.ts index ef41d52d3..d130b3835 100644 --- a/packages/shared/src/endpoints/brla.endpoints.ts +++ b/packages/shared/src/endpoints/brla.endpoints.ts @@ -21,7 +21,7 @@ export interface BrlaGetUserRequest { } export interface BrlaPostRecordInitialKycAttemptRequest extends BrlaGetUserRequest { - quoteId: string; + quoteId?: string; sessionId?: string; } export interface BrlaGetUserResponse { diff --git a/packages/shared/src/endpoints/index.ts b/packages/shared/src/endpoints/index.ts index 4fb011570..0f9425d50 100644 --- a/packages/shared/src/endpoints/index.ts +++ b/packages/shared/src/endpoints/index.ts @@ -5,6 +5,7 @@ export * from "./contact.endpoints"; export * from "./email.endpoints"; export * from "./limits.endpoints"; export * from "./moonbeam.endpoints"; +export * from "./onboarding-requirements.endpoints"; export * from "./payment-methods.endpoints"; export * from "./pendulum.endpoints"; export * from "./price.endpoints"; diff --git a/packages/shared/src/endpoints/onboarding-requirements.endpoints.test.ts b/packages/shared/src/endpoints/onboarding-requirements.endpoints.test.ts new file mode 100644 index 000000000..66c016f84 --- /dev/null +++ b/packages/shared/src/endpoints/onboarding-requirements.endpoints.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "bun:test"; +import { ONBOARDING_REQUIREMENTS } from "./onboarding-requirements.endpoints"; + +describe("ONBOARDING_REQUIREMENTS", () => { + test("publishes every supported Avenia and Alfredpay product flow", () => { + expect(Object.fromEntries(Object.entries(ONBOARDING_REQUIREMENTS).map(([country, flows]) => [country, Object.keys(flows).sort()]))).toEqual({ + AR: ["individual"], + BR: ["business", "individual"], + CO: ["business", "individual"], + MX: ["business", "individual"], + US: ["business", "individual"] + }); + }); + + test("uses unique ordered steps and OpenAPI component references", () => { + for (const flows of Object.values(ONBOARDING_REQUIREMENTS)) { + for (const requirements of Object.values(flows)) { + expect(requirements).toBeDefined(); + if (!requirements) continue; + + expect(requirements.steps.map(step => step.order)).toEqual( + Array.from({ length: requirements.steps.length }, (_, index) => index + 1) + ); + for (const step of requirements.steps) { + if (step.kind === "api") expect(step.operationId).toBeString(); + if (step.requestSchema) expect(step.requestSchema).toStartWith("#/components/schemas/"); + } + } + } + }); + + test("keeps provider-hosted collection explicit", () => { + expect(ONBOARDING_REQUIREMENTS.US.individual?.mode).toBe("hosted"); + expect(ONBOARDING_REQUIREMENTS.US.business?.fields).toEqual([]); + expect(ONBOARDING_REQUIREMENTS.BR.individual?.mode).toBe("hybrid"); + }); + + test("includes fixed provider discriminators needed to execute business flows", () => { + const mxBusinessStatus = ONBOARDING_REQUIREMENTS.MX.business?.steps.find( + step => step.operationId === "getAlfredpayKycStatus" + ); + expect(mxBusinessStatus?.fixedQuery).toEqual({ country: "MX", type: "BUSINESS" }); + + const usBusinessOpened = ONBOARDING_REQUIREMENTS.US.business?.steps.find( + step => step.operationId === "notifyAlfredpayKycRedirectOpened" + ); + expect(usBusinessOpened?.fixedBody).toEqual({ country: "US", type: "BUSINESS" }); + + const brBusinessFields = ONBOARDING_REQUIREMENTS.BR.business?.fields.map(field => field.path); + expect(brBusinessFields).toContain("fullName"); + expect(brBusinessFields).not.toContain("ubo.fullName"); + }); +}); diff --git a/packages/shared/src/endpoints/onboarding-requirements.endpoints.ts b/packages/shared/src/endpoints/onboarding-requirements.endpoints.ts new file mode 100644 index 000000000..cc6bd8125 --- /dev/null +++ b/packages/shared/src/endpoints/onboarding-requirements.endpoints.ts @@ -0,0 +1,674 @@ +import type { CorridorCustomerType } from "../corridors"; + +export type OnboardingRequirementsCountry = "AR" | "BR" | "CO" | "MX" | "US"; +export type OnboardingFlowMode = "api" | "hosted" | "hybrid"; +export type OnboardingRequirementFieldType = "array" | "boolean" | "number" | "string"; +export type OnboardingStepKind = "api" | "direct-upload" | "hosted"; + +export interface OnboardingRequirementField { + path: string; + required: boolean; + type: OnboardingRequirementFieldType; + allowedValues?: string[]; + description?: string; + format?: string; + requiredWhen?: string; +} + +export interface OnboardingDocumentRequirement { + type: string; + required: boolean; + acceptedMediaTypes?: string[]; + collection?: "direct-upload" | "hosted"; + description?: string; + requiredWhen?: string; +} + +export interface OnboardingRequirementStep { + order: number; + kind: OnboardingStepKind; + description: string; + operationId?: string; + method?: "GET" | "POST" | "PUT"; + path?: string; + requestSchema?: string; + condition?: string; + derivedValues?: Record; + fixedBody?: Record; + fixedQuery?: Record; + repeatFor?: string; +} + +export interface GetOnboardingRequirementsResponse { + country: OnboardingRequirementsCountry; + customerType: CorridorCustomerType; + documentationUrl: string; + fields: OnboardingRequirementField[]; + flow: string; + mode: OnboardingFlowMode; + openapiUrl: string; + provider: "alfredpay" | "avenia"; + requirementsVersion: string; + documents: OnboardingDocumentRequirement[]; + steps: OnboardingRequirementStep[]; +} + +export interface GetOnboardingRequirementsErrorResponse { + error: { + code: "INVALID_ONBOARDING_REQUIREMENTS_QUERY" | "ONBOARDING_REQUIREMENTS_NOT_FOUND"; + message: string; + status: 400 | 404; + }; +} + +const REQUIREMENTS_VERSION = "2026-08-10"; +const OPENAPI_URL = "https://raw.githubusercontent.com/pendulum-chain/vortex/main/docs/api/openapi/vortex.openapi.json"; +const ALFREDPAY_MEDIA_TYPES = ["image/jpeg", "image/png", "application/pdf"]; + +const alfredpayInitialSteps = ( + country: OnboardingRequirementsCountry, + customerType: CorridorCustomerType +): OnboardingRequirementStep[] => [ + { + description: "Read the current provider onboarding state.", + fixedQuery: { country, type: customerType === "business" ? "BUSINESS" : "INDIVIDUAL" }, + kind: "api", + method: "GET", + operationId: "getAlfredpayStatus", + order: 1, + path: "/v1/alfredpay/alfredpayStatus" + }, + { + condition: "Run only when no provider customer exists.", + description: `Create the ${customerType} provider customer.`, + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: customerType === "business" ? "createAlfredpayBusinessCustomer" : "createAlfredpayIndividualCustomer", + order: 2, + path: customerType === "business" ? "/v1/alfredpay/createBusinessCustomer" : "/v1/alfredpay/createIndividualCustomer", + requestSchema: "#/components/schemas/AlfredpayCreateCustomerRequest" + } +]; + +const alfredpayIndividualFields = (country: "AR" | "CO" | "MX"): OnboardingRequirementField[] => { + const fields: OnboardingRequirementField[] = [ + { path: "firstName", required: true, type: "string" }, + { path: "lastName", required: true, type: "string" }, + { format: "date", path: "dateOfBirth", required: true, type: "string" }, + { allowedValues: [country], path: "country", required: true, type: "string" }, + { path: "city", required: true, type: "string" }, + { path: "state", required: true, type: "string" }, + { path: "zipCode", required: true, type: "string" }, + { path: "address", required: true, type: "string" }, + { path: "dni", required: true, type: "string" } + ]; + + if (country === "MX") { + fields.push({ format: "email", path: "email", required: true, type: "string" }); + fields.push({ path: "typeDocument", required: false, type: "string" }); + } + if (country === "CO") { + fields.push({ allowedValues: ["CC", "CE"], path: "typeDocumentCol", required: true, type: "string" }); + fields.push({ format: "phone", path: "phoneNumber", required: true, type: "string" }); + } + if (country === "AR") { + fields.push( + { format: "email", path: "email", required: true, type: "string" }, + { format: "phone", path: "phoneNumber", required: true, type: "string" }, + { allowedValues: ["AR"], path: "countryCode", required: true, type: "string" }, + { allowedValues: ["AR"], path: "nationalities", required: true, type: "array" }, + { allowedValues: ["DNI"], path: "typeDocumentAr", required: true, type: "string" }, + { path: "pep", required: true, type: "boolean" }, + { description: "Optional 11-digit CUIT.", path: "cuit", required: false, type: "string" } + ); + } + + return fields; +}; + +const alfredpayIndividualDocuments = (includeSelfie: boolean): OnboardingDocumentRequirement[] => [ + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "National ID Front" }, + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "National ID Back" }, + ...(includeSelfie ? [{ acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "Selfie" }] : []) +]; + +const alfredpayIndividualFlow = (country: "AR" | "CO" | "MX"): GetOnboardingRequirementsResponse => ({ + country, + customerType: "individual", + documentationUrl: "https://api-docs.vortexfinance.co/fiat-corridors", + documents: alfredpayIndividualDocuments(country === "AR"), + fields: alfredpayIndividualFields(country), + flow: `alfredpay-${country.toLowerCase()}-individual-api-kyc`, + mode: "api", + openapiUrl: OPENAPI_URL, + provider: "alfredpay", + requirementsVersion: REQUIREMENTS_VERSION, + steps: [ + ...alfredpayInitialSteps(country, "individual"), + { + description: "Create the KYC submission with the collected identity data.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "submitAlfredpayKycInformation", + order: 3, + path: "/v1/alfredpay/submitKycInformation", + requestSchema: "#/components/schemas/SubmitKycInformationRequest" + }, + { + derivedValues: { + "body.fileType": "current document type", + "body.submissionId": "step 3 response submissionId" + }, + description: "Upload each required identity document.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "submitAlfredpayKycFile", + order: 4, + path: "/v1/alfredpay/submitKycFile", + repeatFor: "documents", + requestSchema: "#/components/schemas/AlfredpayKycFileUploadRequest" + }, + { + derivedValues: { "body.submissionId": "step 3 response submissionId" }, + description: "Finalize the KYC submission.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "sendAlfredpayKycSubmission", + order: 5, + path: "/v1/alfredpay/sendKycSubmission", + requestSchema: "#/components/schemas/AlfredpaySendSubmissionRequest" + }, + { + description: "Poll until the provider reports a terminal status.", + fixedQuery: { country, type: "INDIVIDUAL" }, + kind: "api", + method: "GET", + operationId: "getAlfredpayKycStatus", + order: 6, + path: "/v1/alfredpay/getKycStatus" + } + ] +}); + +const alfredpayBusinessFields: OnboardingRequirementField[] = [ + { path: "businessName", required: true, type: "string" }, + { path: "taxId", required: true, type: "string" }, + { format: "uri", path: "website", required: true, type: "string" }, + { path: "address", required: true, type: "string" }, + { path: "city", required: true, type: "string" }, + { path: "state", required: true, type: "string" }, + { path: "zipCode", required: true, type: "string" }, + { path: "relatedPersons[].firstName", required: true, type: "string" }, + { path: "relatedPersons[].lastName", required: true, type: "string" }, + { format: "email", path: "relatedPersons[].email", required: true, type: "string" }, + { format: "date", path: "relatedPersons[].dateOfBirth", required: true, type: "string" }, + { path: "relatedPersons[].nationalities", required: true, type: "array" }, + { path: "walletAddresses", required: true, type: "string" }, + { path: "sourceOfFunds", required: true, type: "string" }, + { path: "transmitsCustomerFunds", required: true, type: "boolean" }, + { + path: "conductsComplianceScreening", + required: false, + requiredWhen: "transmitsCustomerFunds is true", + type: "boolean" + }, + { + path: "complianceScreeningDescription", + required: false, + requiredWhen: "conductsComplianceScreening is true", + type: "string" + }, + { path: "operatesInSanctionedCountries", required: true, type: "boolean" }, + { path: "isRegulatedBusiness", required: true, type: "boolean" }, + { path: "businessActivities", required: true, type: "string" }, + { path: "accountPurpose", required: true, type: "string" }, + { path: "expectedMonthlyVolumeUsd", required: true, type: "number" }, + { path: "expectedMonthlyTransactions", required: true, type: "number" } +]; + +const alfredpayBusinessDocuments: OnboardingDocumentRequirement[] = [ + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "taxIdDocument" }, + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "articlesIncorporation" }, + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "proofAddress" }, + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "shareholderRegistry" }, + { + acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, + required: false, + requiredWhen: "isRegulatedBusiness is true", + type: "businessLicense" + }, + { + acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, + required: false, + requiredWhen: "isRegulatedBusiness is true", + type: "uploadAmlPolicy" + }, + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "docFront" }, + { acceptedMediaTypes: ALFREDPAY_MEDIA_TYPES, required: true, type: "docBack" } +]; + +const alfredpayBusinessFlow = (country: "CO" | "MX"): GetOnboardingRequirementsResponse => ({ + country, + customerType: "business", + documentationUrl: "https://api-docs.vortexfinance.co/fiat-corridors", + documents: alfredpayBusinessDocuments, + fields: [{ allowedValues: [country], path: "country", required: true, type: "string" }, ...alfredpayBusinessFields], + flow: `alfredpay-${country.toLowerCase()}-business-api-kyb`, + mode: "api", + openapiUrl: OPENAPI_URL, + provider: "alfredpay", + requirementsVersion: REQUIREMENTS_VERSION, + steps: [ + ...alfredpayInitialSteps(country, "business"), + { + description: "Create or update the KYB submission with company, representative, and questionnaire data.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "submitAlfredpayKybInformation", + order: 3, + path: "/v1/alfredpay/submitKybInformation", + requestSchema: "#/components/schemas/SubmitKybInformationRequest" + }, + { + derivedValues: { + "body.fileType": "current company document type", + "body.submissionId": "step 3 response submissionId" + }, + description: "Upload each required company document.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "submitAlfredpayKybFile", + order: 4, + path: "/v1/alfredpay/submitKybFile", + repeatFor: "company documents", + requestSchema: "#/components/schemas/AlfredpayKybFileUploadRequest" + }, + { + description: "Read the provider identifiers assigned to the submitted related persons.", + fixedQuery: { country }, + kind: "api", + method: "GET", + operationId: "findAlfredpayKybCustomerAndBusiness", + order: 5, + path: "/v1/alfredpay/findKybCustomerAndBusiness" + }, + { + derivedValues: { + "body.fileType": "current related-person document type", + "body.relatedPersonId": "step 5 response relatedPersons[].idRelatedPerson" + }, + description: "Upload both identity document sides for each related person.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "submitAlfredpayKybRelatedPersonFile", + order: 6, + path: "/v1/alfredpay/submitKybRelatedPersonFile", + repeatFor: "related persons and their required documents", + requestSchema: "#/components/schemas/AlfredpayKybRelatedPersonFileUploadRequest" + }, + { + derivedValues: { "body.submissionId": "step 3 response submissionId" }, + description: "Finalize the KYB submission.", + fixedBody: { country }, + kind: "api", + method: "POST", + operationId: "sendAlfredpayKybSubmission", + order: 7, + path: "/v1/alfredpay/sendKybSubmission", + requestSchema: "#/components/schemas/AlfredpaySendSubmissionRequest" + }, + { + description: "Poll the business status until the provider reports a terminal state.", + fixedQuery: { country, type: "BUSINESS" }, + kind: "api", + method: "GET", + operationId: "getAlfredpayKycStatus", + order: 8, + path: "/v1/alfredpay/getKycStatus" + } + ] +}); + +const alfredpayHostedFlow = (customerType: CorridorCustomerType): GetOnboardingRequirementsResponse => ({ + country: "US", + customerType, + documentationUrl: "https://api-docs.vortexfinance.co/fiat-corridors", + documents: [], + fields: [], + flow: `alfredpay-us-${customerType}-hosted-${customerType === "business" ? "kyb" : "kyc"}`, + mode: "hosted", + openapiUrl: OPENAPI_URL, + provider: "alfredpay", + requirementsVersion: REQUIREMENTS_VERSION, + steps: [ + ...alfredpayInitialSteps("US", customerType), + { + description: "Create the provider-hosted verification session.", + fixedQuery: { country: "US" }, + kind: "api", + method: "GET", + operationId: customerType === "business" ? "getAlfredpayKybRedirectLink" : "getAlfredpayKycRedirectLink", + order: 3, + path: customerType === "business" ? "/v1/alfredpay/getKybRedirectLink" : "/v1/alfredpay/getKycRedirectLink" + }, + { + description: "Open the returned provider URL so the customer can supply the hosted requirements.", + kind: "hosted", + order: 4 + }, + { + description: "Record that the provider-hosted session was opened.", + fixedBody: { country: "US", type: customerType === "business" ? "BUSINESS" : "INDIVIDUAL" }, + kind: "api", + method: "POST", + operationId: "notifyAlfredpayKycRedirectOpened", + order: 5, + path: "/v1/alfredpay/kycRedirectOpened", + requestSchema: "#/components/schemas/AlfredpayRedirectNotificationRequest" + }, + { + condition: "Call when the customer confirms that the hosted form is complete.", + description: "Record customer completion without treating it as provider approval.", + fixedBody: { country: "US", type: customerType === "business" ? "BUSINESS" : "INDIVIDUAL" }, + kind: "api", + method: "POST", + operationId: "notifyAlfredpayKycRedirectFinished", + order: 6, + path: "/v1/alfredpay/kycRedirectFinished", + requestSchema: "#/components/schemas/AlfredpayRedirectNotificationRequest" + }, + { + description: "Poll until the provider reports a terminal status.", + fixedQuery: { country: "US", type: customerType === "business" ? "BUSINESS" : "INDIVIDUAL" }, + kind: "api", + method: "GET", + operationId: "getAlfredpayKycStatus", + order: 7, + path: "/v1/alfredpay/getKycStatus" + } + ] +}); + +const aveniaIndividualFields: OnboardingRequirementField[] = [ + { allowedValues: ["INDIVIDUAL"], path: "accountType", required: true, type: "string" }, + { path: "name", required: true, type: "string" }, + { format: "cpf", path: "taxId", required: true, type: "string" }, + { path: "fullName", required: true, type: "string" }, + { format: "date", path: "dateOfBirth", required: true, type: "string" }, + { allowedValues: ["BRA"], path: "countryOfTaxId", required: true, type: "string" }, + { format: "cpf", path: "taxIdNumber", required: true, type: "string" }, + { format: "email", path: "email", required: true, type: "string" }, + { allowedValues: ["BRA"], path: "country", required: true, type: "string" }, + { path: "state", required: true, type: "string" }, + { path: "city", required: true, type: "string" }, + { path: "zipCode", required: true, type: "string" }, + { path: "streetAddress", required: true, type: "string" } +]; + +const aveniaBusinessFields: OnboardingRequirementField[] = [ + { allowedValues: ["COMPANY"], path: "accountType", required: true, type: "string" }, + { path: "name", required: true, type: "string" }, + { format: "cnpj", path: "taxId", required: true, type: "string" }, + { path: "fullName", required: true, type: "string" }, + { format: "date", path: "dateOfBirth", required: true, type: "string" }, + { format: "iso-3166-1-alpha-3", path: "countryOfTaxId", required: true, type: "string" }, + { path: "taxIdNumber", required: true, type: "string" }, + { path: "percentageOfOwnership", required: true, type: "string" }, + { format: "iso-3166-1-alpha-3", path: "documentCountry", required: true, type: "string" }, + { path: "streetLine1", required: true, type: "string" }, + { path: "city", required: true, type: "string" }, + { path: "state", required: true, type: "string" }, + { path: "zipCode", required: true, type: "string" }, + { format: "iso-3166-1-alpha-3", path: "country", required: true, type: "string" }, + { path: "companyLegalName", required: true, type: "string" }, + { path: "companyRegistrationNumber", required: true, type: "string" }, + { path: "taxIdentificationNumberTin", required: true, type: "string" }, + { path: "businessActivityDescription", required: true, type: "string" }, + { path: "reasonForAccountOpening", required: true, type: "string" }, + { path: "sourceOfFundsAndIncome", required: true, type: "string" }, + { path: "numberOfEmployees", required: true, type: "string" }, + { path: "estimatedAnnualRevenueUsd", required: true, type: "string" }, + { path: "estimatedMonthlyVolumeUsd", required: true, type: "string" }, + { format: "iso-3166-1-alpha-3", path: "countryTaxResidence", required: true, type: "string" }, + { path: "companyStreetLine1", required: true, type: "string" }, + { path: "companyCity", required: true, type: "string" }, + { path: "companyState", required: true, type: "string" }, + { path: "companyZipCode", required: true, type: "string" }, + { format: "iso-3166-1-alpha-3", path: "companyCountry", required: true, type: "string" } +]; + +const AVENIA_INDIVIDUAL: GetOnboardingRequirementsResponse = { + country: "BR", + customerType: "individual", + documentationUrl: "https://api-docs.vortexfinance.co/fiat-corridors", + documents: [ + { collection: "direct-upload", description: "Use ID or DRIVERS-LICENSE.", required: true, type: "identity document" }, + { + collection: "hosted", + description: "Completed through the Avenia liveness URL.", + required: true, + type: "selfie" + } + ], + fields: aveniaIndividualFields, + flow: "avenia-br-individual-level-1-kyc", + mode: "hybrid", + openapiUrl: OPENAPI_URL, + provider: "avenia", + requirementsVersion: REQUIREMENTS_VERSION, + steps: [ + { + description: "Read the current Avenia subaccount and KYC level.", + kind: "api", + method: "GET", + operationId: "getBrlaUser", + order: 1, + path: "/v1/brla/getUser" + }, + { + condition: "Run only when no Avenia subaccount exists.", + description: "Create the individual Avenia subaccount.", + kind: "api", + method: "POST", + operationId: "createSubaccount", + order: 2, + path: "/v1/brla/createSubaccount", + requestSchema: "#/components/schemas/CreateSubaccountRequest" + }, + { + description: "Create identity-document and selfie upload targets.", + kind: "api", + method: "POST", + operationId: "brlaGetUploadUrls", + order: 3, + path: "/v1/brla/getUploadUrls", + requestSchema: "#/components/schemas/AveniaKYCDataUploadRequest" + }, + { + description: "Upload identity-document bytes to the returned presigned URL.", + kind: "direct-upload", + method: "PUT", + order: 4 + }, + { + description: "Open and complete the returned provider-hosted liveness URL.", + kind: "hosted", + order: 5 + }, + { + derivedValues: { + "body.subAccountId": "step 1 or 2 response subAccountId", + "body.uploadedDocumentId": "step 3 response idUpload.id", + "body.uploadedSelfieId": "step 3 response selfieUpload.id" + }, + description: "Submit the Level 1 KYC data after both uploads are ready.", + kind: "api", + method: "POST", + operationId: "brlaNewKyc", + order: 6, + path: "/v1/brla/newKyc", + requestSchema: "#/components/schemas/KycLevel1Payload" + }, + { + description: "Poll until Avenia reports a terminal KYC decision.", + kind: "api", + method: "GET", + operationId: "fetchSubaccountKycStatus", + order: 7, + path: "/v1/brla/getKycStatus" + } + ] +}; + +const AVENIA_BUSINESS: GetOnboardingRequirementsResponse = { + country: "BR", + customerType: "business", + documentationUrl: "https://api-docs.vortexfinance.co/fiat-corridors", + documents: [ + { collection: "direct-upload", required: true, type: "CERTIFICATE-OF-INCORPORATION" }, + { collection: "direct-upload", required: true, type: "COMPANY-TAX-IDENTIFICATION-DOCUMENT" }, + { + collection: "direct-upload", + description: "Required for each UBO.", + required: true, + type: "ID, DRIVERS-LICENSE, PASSPORT, or RESIDENCE-PERMIT" + }, + { + collection: "hosted", + description: "Optional provider-hosted liveness evidence for a UBO.", + required: false, + type: "SELFIE-FROM-LIVENESS" + } + ], + fields: aveniaBusinessFields, + flow: "avenia-br-business-level-1-api-kyb", + mode: "api", + openapiUrl: OPENAPI_URL, + provider: "avenia", + requirementsVersion: REQUIREMENTS_VERSION, + steps: [ + { + description: "Read the current Avenia company subaccount.", + kind: "api", + method: "GET", + operationId: "getBrlaUser", + order: 1, + path: "/v1/brla/getUser" + }, + { + condition: "Run only when no Avenia company subaccount exists.", + description: "Create the company Avenia subaccount.", + kind: "api", + method: "POST", + operationId: "createSubaccount", + order: 2, + path: "/v1/brla/createSubaccount", + requestSchema: "#/components/schemas/CreateSubaccountRequest" + }, + { + derivedValues: { + "body.documentType": "current document type", + "query.subAccountId": "step 1 or 2 response subAccountId" + }, + description: "Create an upload target for each company and UBO document.", + kind: "api", + method: "POST", + operationId: "createAveniaKybDocument", + order: 3, + path: "/v1/brla/kyb/documents", + repeatFor: "documents", + requestSchema: "#/components/schemas/AveniaKybDocumentRequest" + }, + { + description: "Upload document bytes to each returned presigned URL.", + kind: "direct-upload", + method: "PUT", + order: 4, + repeatFor: "documents where collection is direct-upload" + }, + { + condition: "Run for each optional SELFIE-FROM-LIVENESS document the integrator chooses to collect.", + description: "Open and complete the provider-hosted liveness URL returned when the document was created.", + kind: "hosted", + order: 5, + repeatFor: "documents where collection is hosted" + }, + { + derivedValues: { + "path.documentId": "step 3 response id for the current document", + "query.subAccountId": "step 1 or 2 response subAccountId" + }, + description: "Poll each document until it is ready before referencing it.", + kind: "api", + method: "GET", + operationId: "getAveniaKybDocument", + order: 6, + path: "/v1/brla/kyb/documents/{documentId}", + repeatFor: "documents" + }, + { + derivedValues: { + "body.uploadedIdentificationId": "step 3 response id for the current UBO identity document", + "body.uploadedSelfieId": "step 3 response id for the current optional SELFIE-FROM-LIVENESS document", + "query.subAccountId": "step 1 or 2 response subAccountId" + }, + description: "Register each UBO using ready identity documents.", + kind: "api", + method: "POST", + operationId: "createAveniaKybUbo", + order: 7, + path: "/v1/brla/kyb/ubos", + repeatFor: "UBOs", + requestSchema: "#/components/schemas/AveniaUboPayload" + }, + { + derivedValues: { + "body.certificateOfIncorporationDocumentId": "step 3 response id for CERTIFICATE-OF-INCORPORATION", + "body.taxIdentificationDocumentId": "step 3 response id for COMPANY-TAX-IDENTIFICATION-DOCUMENT", + "body.uboIds": "step 7 response ids", + "query.subAccountId": "step 1 or 2 response subAccountId" + }, + description: "Submit the company Level 1 KYB attempt.", + kind: "api", + method: "POST", + operationId: "submitAveniaKybLevel1Api", + order: 8, + path: "/v1/brla/kyb/new-level-1/api", + requestSchema: "#/components/schemas/AveniaKybLevel1Payload" + }, + { + derivedValues: { "query.attemptId": "step 8 response id" }, + description: "Poll until Avenia reports a terminal KYB decision.", + kind: "api", + method: "GET", + operationId: "getAveniaKybAttemptStatus", + order: 9, + path: "/v1/brla/kyb/attempt-status" + } + ] +}; + +export const ONBOARDING_REQUIREMENTS: Record< + OnboardingRequirementsCountry, + Partial> +> = { + AR: { individual: alfredpayIndividualFlow("AR") }, + BR: { business: AVENIA_BUSINESS, individual: AVENIA_INDIVIDUAL }, + CO: { business: alfredpayBusinessFlow("CO"), individual: alfredpayIndividualFlow("CO") }, + MX: { business: alfredpayBusinessFlow("MX"), individual: alfredpayIndividualFlow("MX") }, + US: { business: alfredpayHostedFlow("business"), individual: alfredpayHostedFlow("individual") } +}; + +export function getOnboardingRequirements( + country: OnboardingRequirementsCountry, + customerType: CorridorCustomerType +): GetOnboardingRequirementsResponse | undefined { + return ONBOARDING_REQUIREMENTS[country][customerType]; +} From ab3f13727c10dd9c63963be7223c8f90b535bd2d Mon Sep 17 00:00:00 2001 From: Gianfranco Date: Tue, 11 Aug 2026 12:05:24 -0300 Subject: [PATCH 9/9] docs(api): refresh merged OpenAPI declarations --- docs/api/openapi/vortex.openapi.d.ts | 7530 +++++++++++++------------- 1 file changed, 3765 insertions(+), 3765 deletions(-) diff --git a/docs/api/openapi/vortex.openapi.d.ts b/docs/api/openapi/vortex.openapi.d.ts index bf0ca6067..36de3b6a5 100644 --- a/docs/api/openapi/vortex.openapi.d.ts +++ b/docs/api/openapi/vortex.openapi.d.ts @@ -4,7 +4,7 @@ */ export interface paths { - "/v1/api-credentials": { + "/v1/alfredpay/alfredpayStatus": { parameters: { query?: never; header?: never; @@ -12,27 +12,19 @@ export interface paths { cookie?: never; }; /** - * List API credentials - * @description Lists all profile-managed credentials owned by the authenticated profile, newest first. Each item represents one public/secret credential. Public values and safe secret prefixes are included; secret values are never returned. - * - * **Auth:** Supabase Bearer session only. + * Get Alfredpay customer status + * @description Returns the local Alfredpay onboarding state after refreshing the latest provider submission when available. */ - get: operations["listApiCredentials"]; + get: operations["getAlfredpayStatus"]; put?: never; - /** - * Create an API credential - * @description Creates one credential row containing a public value and a hashed secret value for the authenticated profile. The secret is returned only in this response. Expiry defaults to one year and cannot exceed two years. At most five non-revoked, non-expired credentials may exist per profile. - * - * **Auth:** Supabase Bearer session only. - */ - post: operations["createApiCredential"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/api-credentials/{credentialId}": { + "/v1/alfredpay/createBusinessCustomer": { parameters: { query?: never; header?: never; @@ -41,20 +33,18 @@ export interface paths { }; get?: never; put?: never; - post?: never; /** - * Revoke an API credential - * @description Sets `revokedAt` on one profile-managed credential owned by the authenticated profile, atomically disabling its public and secret values. No request body or paired key ID is accepted. - * - * **Auth:** Supabase Bearer session only. + * Create a business Alfredpay customer + * @description Creates a business Alfredpay customer for the effective profile. Managed profiles use their immutable contact email. */ - delete: operations["revokeApiCredential"]; + post: operations["createAlfredpayBusinessCustomer"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/auth/request-otp": { + "/v1/alfredpay/createIndividualCustomer": { parameters: { query?: never; header?: never; @@ -64,41 +54,41 @@ export interface paths { get?: never; put?: never; /** - * Request an email OTP - * @description Sends a 6-digit one-time password to the given email address. Use it with `POST /v1/auth/verify-otp` to obtain a user session. - * - * **Auth:** none. + * Create an individual Alfredpay customer + * @description Creates an individual Alfredpay customer for the effective profile. Managed profiles use their immutable contact email. */ - post: operations["requestOTP"]; + post: operations["createAlfredpayIndividualCustomer"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/auth/verify-otp": { + "/v1/alfredpay/fiatAccounts": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * List Alfredpay fiat accounts + * @description Lists payout fiat accounts for the effective Alfredpay customer. + */ + get: operations["listAlfredpayFiatAccounts"]; put?: never; /** - * Verify an email OTP - * @description Verifies the emailed one-time password and returns a user session. First-time sign-ins create the user profile; `user_id` identifies the profile that API keys minted with this session are linked to. - * - * **Auth:** none. + * Create an Alfredpay fiat account + * @description Creates a payout fiat account for the effective Alfredpay customer. Required optional fields depend on the selected account type and corridor. */ - post: operations["verifyOTP"]; + post: operations["createAlfredpayFiatAccount"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/createSubaccount": { + "/v1/alfredpay/fiatAccounts/{fiatAccountId}": { parameters: { query?: never; header?: never; @@ -107,22 +97,18 @@ export interface paths { }; get?: never; put?: never; + post?: never; /** - * Create user or retry KYC - * @description `companyName`, `startDate` and `cnpj` are only required when taxIdType is `CNPJ` - * - * `quoteId` is optional: pass it in the normal ramp flow, or omit it for the quote-less KYB deep link where business verification starts before any quote exists. - * - * **Auth:** secret `X-API-Key` or Supabase Bearer session. + * Delete an Alfredpay fiat account + * @description Deletes one payout fiat account belonging to the effective Alfredpay customer. */ - post: operations["createSubaccount"]; - delete?: never; + delete: operations["deleteAlfredpayFiatAccount"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/getKycStatus": { + "/v1/alfredpay/findKybCustomerAndBusiness": { parameters: { query?: never; header?: never; @@ -130,10 +116,10 @@ export interface paths { cookie?: never; }; /** - * Get user's KYC status - * @description **Auth:** secret `X-API-Key` or Supabase Bearer session. + * Find Alfredpay KYB submission details + * @description Returns only KYB submission IDs and related-person IDs needed for document uploads. */ - get: operations["fetchSubaccountKycStatus"]; + get: operations["findAlfredpayKybCustomerAndBusiness"]; put?: never; post?: never; delete?: never; @@ -142,7 +128,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/brla/getSelfieLivenessUrl": { + "/v1/alfredpay/getKybRedirectLink": { parameters: { query?: never; header?: never; @@ -150,12 +136,10 @@ export interface paths { cookie?: never; }; /** - * Get selfie liveness URL - * @description Returns the Avenia selfie/liveness-check URL for the subaccount associated with this tax ID. - * - * **Auth:** secret `X-API-Key` or Supabase Bearer session. + * Get an Alfredpay KYB redirect link + * @description Creates a hosted business KYB redirect link when no verification is already in review or complete. */ - get: operations["brlaGetSelfieLivenessUrl"]; + get: operations["getAlfredpayKybRedirectLink"]; put?: never; post?: never; delete?: never; @@ -164,29 +148,27 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/brla/getUploadUrls": { + "/v1/alfredpay/getKycRedirectLink": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Get KYC document upload URLs - * @description Returns presigned upload URLs for the user's ID document and selfie. Only `ID` and `DRIVERS-LICENSE` are accepted for `documentType` (passport not supported here). - * - * **Auth:** secret `X-API-Key` or Supabase Bearer session. + * Get an Alfredpay KYC redirect link + * @description Creates a hosted individual KYC redirect link when no verification is already in review or complete. */ - post: operations["brlaGetUploadUrls"]; + get: operations["getAlfredpayKycRedirectLink"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/getUser": { + "/v1/alfredpay/getKycStatus": { parameters: { query?: never; header?: never; @@ -194,10 +176,10 @@ export interface paths { cookie?: never; }; /** - * Get user information - * @description Fetches a user's subaccount information. The response contains only the EVM wallet address and KYC level. Anonymous access is retained when no profile selector is supplied; managed-profile selection requires the manager's secret key or Bearer session. + * Get Alfredpay KYC or KYB status + * @description Returns and persists the latest KYC or KYB submission status. Omit `type` for individual KYC. */ - get: operations["getBrlaUser"]; + get: operations["getAlfredpayKycStatus"]; put?: never; post?: never; delete?: never; @@ -206,27 +188,27 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/brla/getUserRemainingLimit": { + "/v1/alfredpay/kycRedirectFinished": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get user's remaining transaction limits - * @description Anonymous access is retained when no profile selector is supplied; managed-profile selection requires the manager's secret key or Bearer session. + * Mark an Alfredpay redirect finished + * @description Records that the effective customer finished the hosted KYC or KYB redirect flow. */ - get: operations["getBrlaUserRemainingLimit"]; - put?: never; - post?: never; + post: operations["notifyAlfredpayKycRedirectFinished"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/newKyc": { + "/v1/alfredpay/kycRedirectOpened": { parameters: { query?: never; header?: never; @@ -236,41 +218,37 @@ export interface paths { get?: never; put?: never; /** - * Submit KYC level 1 data - * @description Submits the user's KYC level 1 payload to Avenia after documents have been uploaded via `/v1/brla/getUploadUrls`. Includes a built-in 5-second delay to allow upstream document propagation. - * - * **Auth:** secret `X-API-Key` or Supabase Bearer session. + * Mark an Alfredpay redirect opened + * @description Records that the effective customer's hosted KYC or KYB redirect was opened. */ - post: operations["brlaNewKyc"]; + post: operations["notifyAlfredpayKycRedirectOpened"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/validatePixKey": { + "/v1/alfredpay/retryKyc": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Validate PIX key - * @description Checks whether a Pix key exists and is valid. The key value itself is intentionally not echoed back in the response for security. - * - * **Auth:** requires `Authorization: Bearer `. + * Retry Alfredpay KYC or KYB + * @description Retries a failed KYC or KYB submission. Hosted flows return a redirect link; API-based MX, CO, and AR individual KYC returns `{ success: true }`. */ - get: operations["brlaValidatePixKey"]; - put?: never; - post?: never; + post: operations["retryAlfredpayKyc"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/limits": { + "/v1/alfredpay/sendKybSubmission": { parameters: { query?: never; header?: never; @@ -280,66 +258,37 @@ export interface paths { get?: never; put?: never; /** - * Get user ramp limits - * @description Returns onramp and offramp limits for the authenticated user's requested fiat corridors. Alfredpay usage is calculated from completed Vortex ramps in the current UTC calendar month and may be delayed by the 60-second in-memory cache. Avenia BRL maximums, usage, and period are read from Avenia. - * - * **Auth:** requires either `X-API-Key: sk_*` linked to a user or `Authorization: Bearer `. Unlinked partner keys are rejected. + * Send an Alfredpay KYB submission + * @description Finalizes an API-based business KYB submission. */ - post: operations["getUserLimits"]; + post: operations["sendAlfredpayKybSubmission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/public-key": { + "/v1/alfredpay/sendKycSubmission": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Public Key - * @description Returns the RSA-PSS 2048 / SHA-256 public key used to verify Vortex webhook signatures. This is NOT a partner `pk_*` API key. + * Send an Alfredpay KYC submission + * @description Finalizes an API-based individual KYC submission. */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description RSA-PSS public key in PEM format. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...replace-with-actual-key...\n-----END PUBLIC KEY-----\n" - * } - */ - "application/json": { - /** @description RSA-PSS 2048-bit public key in PEM format. Use this key to verify webhook signatures with RSA-PSS / SHA-256. */ - publicKey: string; - }; - }; - }; - }; - }; - put?: never; - post?: never; + post: operations["sendAlfredpayKycSubmission"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/quotes": { + "/v1/alfredpay/submitKybFile": { parameters: { query?: never; header?: never; @@ -349,60 +298,37 @@ export interface paths { get?: never; put?: never; /** - * Create a new quote - * @description Generates a quote for a specified ramp transaction, detailing input and output amounts, fees, and expiration. + * Upload an Alfredpay KYB file + * @description Uploads one business KYB document. Files are buffered in memory and limited to 5 MiB. */ - post: operations["createQuote"]; + post: operations["submitAlfredpayKybFile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/quotes/{id}": { + "/v1/alfredpay/submitKybInformation": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get existing quote - * @description Get a quote by ID. - * - * **Auth:** none. This endpoint is fully public; anyone with the quote ID can read it. + * Submit Alfredpay KYB information + * @description Creates or updates an API-based business KYB submission, including Alfredpay's compliance questionnaire. */ - get: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Quote Id. */ - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["QuoteResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; + post: operations["submitAlfredpayKybInformation"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/quotes/best": { + "/v1/alfredpay/submitKybRelatedPersonFile": { parameters: { query?: never; header?: never; @@ -412,168 +338,57 @@ export interface paths { get?: never; put?: never; /** - * Create a quote for the best network - * @description Generates a new quote for the network that yields the highest output amount for the given parameters. This endpoint compares the output for a given input amount over all supported networks and returns the 'best' quote, defined as the one with the highest output. + * Upload a related-person KYB file + * @description Uploads the front or back identity document for one KYB related person. Files are limited to 5 MiB. */ - post: operations["createBestQuote"]; + post: operations["submitAlfredpayKybRelatedPersonFile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp-info": { + "/v1/alfredpay/submitKycFile": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get sanitized ramp eligibility - * @description Returns only sanitized per-corridor KYC state and buy/sell eligibility for the profile derived from the validated API credential. A manager secret may select one directly managed child with `X-Managed-Profile-Id`; public keys cannot use the selector. The endpoint never returns PII, provider/customer IDs, KYC failure reasons, bank/wallet data, ramp history, or exact financial limits. When both public and secret headers are supplied they must belong to the same credential. Supabase Bearer sessions do not authorize this endpoint. - * - * **Auth:** `X-Public-Key` or `X-API-Key`. + * Upload an Alfredpay KYC file + * @description Uploads one individual KYC document. Files are buffered in memory and limited to 5 MiB. */ - get: operations["getRampInfo"]; - put?: never; - post?: never; + post: operations["submitAlfredpayKycFile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/{id}": { + "/v1/alfredpay/submitKycInformation": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get ramp status - * @description Fetches an updated ramp process. + * Submit Alfredpay KYC information + * @description Creates or resumes an API-based individual KYC submission. */ - get: { - parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; - path: { - /** @description Ramp ID. */ - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - countryCode?: components["schemas"]["CountryCode"]; - /** - * Format: date-time - * @description Timestamp of when the ramp process was created. - */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; - inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; - }; - }; - }; - /** @description The managed-profile selector is invalid. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; - }; - }; - /** @description Authentication is required for an owned ramp or managed-profile selection. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; - }; - /** @description Ramp ownership or managed-profile authorization failed. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; - }; - }; - }; - put?: never; - post?: never; + post: operations["submitAlfredpayKycInformation"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/{id}/errors": { + "/v1/api-credentials": { parameters: { query?: never; header?: never; @@ -581,139 +396,71 @@ export interface paths { cookie?: never; }; /** - * Get ramp error logs - * @description Returns the chronological error log for a ramp. + * List API credentials + * @description Lists all profile-managed credentials owned by the authenticated profile, newest first. Each item represents one public/secret credential. Public values and safe secret prefixes are included; secret values are never returned. * - * **Auth:** requires either `X-API-Key: sk_*` (partner) OR `Authorization: Bearer ` (user). Ownership is enforced. + * **Auth:** Supabase Bearer session only. */ - get: operations["getRampErrorLogs"]; + get: operations["listApiCredentials"]; put?: never; - post?: never; + /** + * Create an API credential + * @description Creates one credential row containing a public value and a hashed secret value for the authenticated profile. The secret is returned only in this response. Expiry defaults to one year and cannot exceed two years. At most five non-revoked, non-expired credentials may exist per profile. + * + * **Auth:** Supabase Bearer session only. + */ + post: operations["createApiCredential"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/history": { + "/v1/api-credentials/{credentialId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Get authenticated user ramp history - * @description Fetches all non-initial ramps owned by the authenticated user across wallet addresses. Requires a Supabase session or user-scoped secret API key. Partner-only credentials are not sufficient. - */ - get: { - parameters: { - query?: { - /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ - limit?: number; - /** @description The offset for querying older transactions. */ - offset?: number; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authenticated user's ramp history. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetRampHistoryResponse"]; - }; - }; - /** @description The managed-profile selector is invalid. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; - }; - }; - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - }; - }; + get?: never; put?: never; post?: never; - delete?: never; + /** + * Revoke an API credential + * @description Sets `revokedAt` on one profile-managed credential owned by the authenticated profile, atomically disabling its public and secret values. No request body or paired key ID is accepted. + * + * **Auth:** Supabase Bearer session only. + */ + delete: operations["revokeApiCredential"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/history/{walletAddress}": { + "/v1/auth/request-otp": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get ramp history for wallet address - * @description Fetches the transaction history for a given wallet address. The response returns the last 20 items by default. This can be adjusted by using the `limit` and `offset` query parameters. + * Request an email OTP + * @description Sends a 6-digit one-time password to the given email address. Use it with `POST /v1/auth/verify-otp` to obtain a user session. + * + * **Auth:** none. */ - get: { - parameters: { - query?: { - /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ - limit?: number; - /** @description The offset for querying the transactions. Necessary if the number of transaction items of the address is larger than the maximum limit. A larger value will return older transaction items. */ - offset?: number; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; - path: { - /** @description The wallet address for which the ramp history is queried for. */ - walletAddress: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetRampHistoryResponse"]; - }; - }; - /** @description The managed-profile selector is invalid. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; - }; - }; - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - }; - }; - put?: never; - post?: never; + post: operations["requestOTP"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/register": { + "/v1/auth/verify-otp": { parameters: { query?: never; header?: never; @@ -723,17 +470,19 @@ export interface paths { get?: never; put?: never; /** - * Register new ramp process - * @description Initiates a new on-ramp or off-ramp process by providing quote details, signing accounts, and additional data. - */ - post: operations["registerRamp"]; + * Verify an email OTP + * @description Verifies the emailed one-time password and returns a user session. First-time sign-ins create the user profile; `user_id` identifies the profile that API keys minted with this session are linked to. + * + * **Auth:** none. + */ + post: operations["verifyOTP"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/start": { + "/v1/brla/createSubaccount": { parameters: { query?: never; header?: never; @@ -743,50 +492,63 @@ export interface paths { get?: never; put?: never; /** - * Start ramp process - * @description Starts a ramp process. + * Create user or retry KYC + * @description `companyName`, `startDate` and `cnpj` are only required when taxIdType is `CNPJ` * - * It is assumed all required information from the client has already been sent using the `update` endpoint. This endpoint is only used to tell the backend any external operation (like a bank transfer) has been completed, and the ramp can start. + * `quoteId` is optional: pass it in the normal ramp flow, or omit it for the quote-less KYB deep link where business verification starts before any quote exists. + * + * **Auth:** secret `X-API-Key` or Supabase Bearer session. */ - post: operations["startRamp"]; + post: operations["createSubaccount"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/ramp/update": { + "/v1/brla/getKycStatus": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * Get user's KYC status + * @description **Auth:** secret `X-API-Key` or Supabase Bearer session. + */ + get: operations["fetchSubaccountKycStatus"]; put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/brla/getSelfieLivenessUrl": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; /** - * Update ramp process - * @description Submits presigned transactions and additional data to an existing ramp process before starting it. - * This endpoint can be called many times, and data can be incrementally added to the ramp. - * - * Note: For both pre-signed transactions and the generic `additionalData` object, existing properties will be overriden by new values. - * - * ### Required data for ramps. - * The signed counterpart of the initial unsignedTxs object must be provided for all ramps, as required by the object. - * For offramps, the `additionalData` field must contain the confirmation hash corresponding to the inital transaction in which the user sends the funds. - * If the originating chain is `Assethub`, then `assetHubToPendulumHash` must be provided. - * If the originating chain is any `EVM` chain, then `squidRouterSwapHash` must be provided. `squidRouterApproveHash` is only required when an approval transaction was actually submitted; if the wallet already holds a sufficient allowance for the router, it can be omitted. + * Get selfie liveness URL + * @description Returns the Avenia selfie/liveness-check URL for the subaccount associated with this tax ID. * - * For onramps, no additional data is required after registering the ramp. + * **Auth:** secret `X-API-Key` or Supabase Bearer session. */ - post: operations["updateRamp"]; + get: operations["brlaGetSelfieLivenessUrl"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/session/create": { + "/v1/brla/getUploadUrls": { parameters: { query?: never; header?: never; @@ -796,140 +558,30 @@ export interface paths { get?: never; put?: never; /** - * Create widget session - * @description Creates a hosted Vortex Widget session and returns the URL to open for the user. - * - * This single endpoint supports two mutually exclusive request shapes: - * - * - **Fixed quote** (`GetWidgetUrlLocked`) — pass a `quoteId` you created via `POST /v1/quotes`. The widget uses that exact quote and does not refresh it. If the quote expires before the user finishes, they must close the window and start over. - * - * - **Auto-refresh** (`GetWidgetUrlRefresh`) — pass the route parameters (`network`, `rampType`, `inputAmount`, plus `fiat` / `cryptoLocked` / `paymentMethod` as relevant for the direction). The widget creates and refreshes quotes on demand for the user. + * Get KYC document upload URLs + * @description Returns presigned upload URLs for the user's ID document and selfie. Only `ID` and `DRIVERS-LICENSE` are accepted for `documentType` (passport not supported here). * - * Use the example switcher below to see the request shape for each mode. `externalSessionId` is required in both modes and is echoed back in webhook payloads. + * **Auth:** secret `X-API-Key` or Supabase Bearer session. */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["GetWidgetUrlLocked"] | components["schemas"]["GetWidgetUrlRefresh"]; - }; - }; - responses: { - /** @description Returned when a fixed-quote session was created. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id"eId=quote_01HXY..." - * } - */ - "application/json": { - /** @description The widget URL to open for the user. */ - url: string; - }; - }; - }; - /** @description Returned when an auto-refresh session was created. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id&rampType=BUY&network=polygon&inputAmount=150&fiat=BRL&cryptoLocked=USDC&paymentMethod=pix" - * } - */ - "application/json": { - /** @description The widget URL to open for the user. */ - url: string; - }; - }; - }; - /** @description Missing required fields, or `quoteId` not provided for fixed-quote mode and route fields not provided for auto-refresh mode. */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Quote not found or expired (fixed-quote mode only). */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; + post: operations["brlaGetUploadUrls"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/supported-countries": { + "/v1/brla/getUser": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Supported Countries */ - get: { - parameters: { - query?: { - /** - * @description ISO code: "BR", "AR", etc. - * @example - */ - countryCode?: string; - /** @description e.g. "Brazil", "Germany" */ - name?: string; - /** @description e.g. "BRL". All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ - fiatCurrency?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - countries: { - /** @description e.g. `DE` */ - countryCode: string; - }[]; - /** @description e.g. 🇩🇪 */ - emoji: string; - /** @description e.g. `Germany` */ - name: string; - support: { - /** @description e.g. `true` */ - buy: boolean; - /** @description e.g. `true` */ - sell: boolean; - }; - /** @description All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ - supportedCurrencies: string[]; - }; - }; - }; - }; - }; + /** + * Get user information + * @description Fetches a user's subaccount information. The response contains only the EVM wallet address and KYC level. Anonymous access is retained when no profile selector is supplied; managed-profile selection requires the manager's secret key or Bearer session. + */ + get: operations["getBrlaUser"]; put?: never; post?: never; delete?: never; @@ -938,7 +590,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/supported-cryptocurrencies": { + "/v1/brla/getUserRemainingLimit": { parameters: { query?: never; header?: never; @@ -946,44 +598,30 @@ export interface paths { cookie?: never; }; /** - * Supported Cryptocurrencies - * @description Retrieve all supported cryptocurrencies, filtered by network. + * Get user's remaining transaction limits + * @description Anonymous access is retained when no profile selector is supplied; managed-profile selection requires the manager's secret key or Bearer session. */ - get: { - parameters: { - query?: { - /** - * @description Filter supported cryptocurrencies by network. Allowed values: `assethub`, `avalanche`, `base`, `bsc`, `ethereum`, `polygon` - * @example - */ - network?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - cryptocurrencies: { - /** @description Defined if network is EVM. */ - assetContractAddress?: string | null; - assetDecimals: number; - /** @description Defined if network is Assethub. */ - assetForeignAssetId?: string | null; - assetNetwork: components["schemas"]["Networks"]; - assetSymbol: string; - }[]; - }; - }; - }; - }; + get: operations["getBrlaUserRemainingLimit"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/brla/kyb/attempt-status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + /** + * Get Avenia KYB attempt status + * @description Refreshes an owned Avenia KYB attempt and persists its normalized verification state. + */ + get: operations["getAveniaKybAttemptStatus"]; put?: never; post?: never; delete?: never; @@ -992,51 +630,27 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/supported-fiat-currencies": { + "/v1/brla/kyb/documents": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Supported Fiat Currencies */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - currencies: { - /** @description e.g. `2` */ - decimals: number; - /** @description e.g. `Brazilian Real` */ - name: string; - /** @description e.g. `BRL` */ - symbol: string; - }[]; - }; - }; - }; - }; - }; + get?: never; put?: never; - post?: never; + /** + * Create Avenia KYB document + * @description Creates an Avenia document and returns presigned upload targets. Upload bytes directly to the returned URLs. + */ + post: operations["createAveniaKybDocument"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/supported-payment-methods": { + "/v1/brla/kyb/documents/{documentId}": { parameters: { query?: never; header?: never; @@ -1044,54 +658,10 @@ export interface paths { cookie?: never; }; /** - * Supported Payment Methods - * @description Retrieve all available payment methods, filtered by type or fiat. + * Get Avenia KYB document + * @description Reads readiness and upload status for an owned Avenia KYB document. */ - get: { - parameters: { - query?: { - /** - * @description Filter supported payment methods by the ramp type. Allowed values: `sell` or `buy`. - * @example - */ - type?: string; - /** - * @description Filter supported payment methods by fiat currency. Allowed values: `EUR`, `ARS`, `BRL`, `USD`, `MXN`, `COP`. - * @example - */ - fiat?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - /** @description Array of supported payment methods matching the params. */ - "paymentMethods:": { - /** @description Unique identifier of the payment method: `sepa`, `pix`, `cbu` */ - id: string; - /** @description Payment method limits in USD */ - limits: { - max: number; - min: number; - }; - /** @description Unique name of the payment method: `SEPA`, `PIX`, `CBU` */ - name: string; - /** @description Array of supported fiat currencies by payment method. */ - supportedFiats: string[]; - }[]; - }; - }; - }; - }; - }; + get: operations["getAveniaKybDocument"]; put?: never; post?: never; delete?: never; @@ -1100,7 +670,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/webhook": { + "/v1/brla/kyb/new-level-1/api": { parameters: { query?: never; header?: never; @@ -1110,80 +680,17 @@ export interface paths { get?: never; put?: never; /** - * Register Webhook - * @description Register a new webhook to receive event notifications. - * - * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. - * - * Webhooks are bound to the account behind your secret key: a `quoteId` must belong to a quote created with your key (any other quote returns `404`). The callback URL must use HTTPS, must not embed credentials, and must resolve to a publicly routable address; private or reserved IP ranges are rejected. + * Submit API-driven Avenia KYB + * @description Submits the API-driven Avenia Level 1 KYB attempt after validating the owned corporate documents and UBO references. */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": { - events?: string[]; - /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific quote. The quote must have been created with your API key. */ - quoteId?: string; - /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific session */ - sessionId?: string; - /** @description Your HTTPS webhook endpoint URL. No embedded credentials; must resolve to a publicly routable address. */ - url: string; - }; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "createdAt": "2025-10-01T16:21:04.648Z", - * "events": [ - * "TRANSACTION_CREATED", - * "STATUS_CHANGE" - * ], - * "id": "340ba946-f3f3-4007-893c-3374bfcd096b", - * "isActive": true, - * "quoteId": "3258910e-93ee-443e-b793-28cc1d4ccdf3", - * "sessionId": null, - * "url": "https://your-website.com" - * } - */ - "application/json": { - /** @description The creation date of the webhook */ - createdAt: string; - /** @description The events the webhook is subscribed for */ - events: string[]; - /** @description Webhook UUID */ - id: string; - /** @description Is the webhook active */ - isActive: boolean; - /** @description (optional): The specific transactionId that the events are subscribed for */ - quoteId?: string; - /** @description (optional): The specific sessionId that the events are subscribed for */ - sessionId?: string; - /** @description Your HTTPS webhook endpoint URL */ - url: string; - }; - }; - }; - }; - }; + post: operations["submitAveniaKybLevel1Api"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/webhook/{id}": { + "/v1/brla/kyb/new-level-1/web-sdk": { parameters: { query?: never; header?: never; @@ -1192,65 +699,38 @@ export interface paths { }; get?: never; put?: never; - post?: never; /** - * Delete Webhook - * @description Remove a webhook subscription. - * - * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. - * - * Deletion is scoped to your account: a webhook registered by another account returns `404`. + * Start Avenia hosted KYB + * @description Starts or resumes Avenia's hosted KYB level-1 flow for an owned company subaccount. */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - message: string; - success: boolean; - }; - }; - }; - }; - }; + post: operations["startAveniaKybLevel1Hosted"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/alfredpayStatus": { + "/v1/brla/kyb/ubos": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get Alfredpay customer status - * @description Returns the local Alfredpay onboarding state after refreshing the latest provider submission when available. + * Create Avenia KYB UBO + * @description Registers a UBO after verifying that referenced identity documents are ready and owned by the company subaccount. */ - get: operations["getAlfredpayStatus"]; - put?: never; - post?: never; + post: operations["createAveniaKybUbo"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/createBusinessCustomer": { + "/v1/brla/kyc/record-attempt": { parameters: { query?: never; header?: never; @@ -1260,17 +740,17 @@ export interface paths { get?: never; put?: never; /** - * Create a business Alfredpay customer - * @description Creates a business Alfredpay customer for the effective profile. Managed profiles use their immutable contact email. + * Record an initial Avenia KYC attempt + * @description Records the first observed KYC attempt for a CPF or CNPJ when no provider-customer record exists yet. */ - post: operations["createAlfredpayBusinessCustomer"]; + post: operations["recordInitialAveniaKycAttempt"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/createIndividualCustomer": { + "/v1/brla/newKyc": { parameters: { query?: never; header?: never; @@ -1280,17 +760,19 @@ export interface paths { get?: never; put?: never; /** - * Create an individual Alfredpay customer - * @description Creates an individual Alfredpay customer for the effective profile. Managed profiles use their immutable contact email. + * Submit KYC level 1 data + * @description Submits the user's KYC level 1 payload to Avenia after documents have been uploaded via `/v1/brla/getUploadUrls`. Includes a built-in 5-second delay to allow upstream document propagation. + * + * **Auth:** secret `X-API-Key` or Supabase Bearer session. */ - post: operations["createAlfredpayIndividualCustomer"]; + post: operations["brlaNewKyc"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/findKybCustomerAndBusiness": { + "/v1/brla/validatePixKey": { parameters: { query?: never; header?: never; @@ -1298,10 +780,12 @@ export interface paths { cookie?: never; }; /** - * Find Alfredpay KYB submission details - * @description Returns only KYB submission IDs and related-person IDs needed for document uploads. + * Validate PIX key + * @description Checks whether a Pix key exists and is valid. The key value itself is intentionally not echoed back in the response for security. + * + * **Auth:** requires `Authorization: Bearer `. */ - get: operations["findAlfredpayKybCustomerAndBusiness"]; + get: operations["brlaValidatePixKey"]; put?: never; post?: never; delete?: never; @@ -1310,27 +794,29 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/alfredpay/getKybRedirectLink": { + "/v1/limits": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get an Alfredpay KYB redirect link - * @description Creates a hosted business KYB redirect link when no verification is already in review or complete. + * Get user ramp limits + * @description Returns onramp and offramp limits for the authenticated user's requested fiat corridors. Alfredpay usage is calculated from completed Vortex ramps in the current UTC calendar month and may be delayed by the 60-second in-memory cache. Avenia BRL maximums, usage, and period are read from Avenia. + * + * **Auth:** requires either `X-API-Key: sk_*` linked to a user or `Authorization: Bearer `. Unlinked partner keys are rejected. */ - get: operations["getAlfredpayKybRedirectLink"]; - put?: never; - post?: never; + post: operations["getUserLimits"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/getKycRedirectLink": { + "/v1/managed-profiles": { parameters: { query?: never; header?: never; @@ -1338,19 +824,27 @@ export interface paths { cookie?: never; }; /** - * Get an Alfredpay KYC redirect link - * @description Creates a hosted individual KYC redirect link when no verification is already in review or complete. + * List managed profiles + * @description Lists children owned by the authenticated active manager, newest first. The default filter returns only active children. Use `status=deleted` or `status=all` to include retained logical-deletion records. + * + * **Auth:** controlling manager Supabase Bearer session or secret API key. Public API keys and direct managed-child credentials are rejected. */ - get: operations["getAlfredpayKycRedirectLink"]; + get: operations["listManagedProfiles"]; put?: never; - post?: never; + /** + * Create a managed profile + * @description Creates one headless individual or business child for the authenticated active manager. `externalSubjectId`, normalized `contactEmail`, and `customerType` are immutable. An exact retry for the same external subject is idempotent and returns the existing active child with `200`; the first creation returns `201`. Reusing either reserved identifier with different data returns `409`, including after deletion. No corridor grant is accepted. + * + * **Auth:** controlling manager Supabase Bearer session or secret API key. Public API keys and direct managed-child credentials are rejected. + */ + post: operations["createManagedProfile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/getKycStatus": { + "/v1/managed-profiles/{profileId}": { parameters: { query?: never; header?: never; @@ -1358,79 +852,55 @@ export interface paths { cookie?: never; }; /** - * Get Alfredpay KYC or KYB status - * @description Returns and persists the latest KYC or KYB submission status. Omit `type` for individual KYC. + * Get a managed profile + * @description Returns one owned child, including a retained logically deleted child. Foreign children are hidden with `404`. + * + * **Auth:** controlling manager Supabase Bearer session or secret API key. */ - get: operations["getAlfredpayKycStatus"]; + get: operations["getManagedProfile"]; put?: never; post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v1/alfredpay/kycRedirectFinished": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; /** - * Mark an Alfredpay redirect finished - * @description Records that the effective customer finished the hosted KYC or KYB redirect flow. + * Delete a managed profile + * @description Logically deletes an owned child and atomically revokes all of its credentials. Customer, provider, KYC, ramp, external-subject, and contact-email records are retained. Repeating deletion of the same owned child is idempotent and returns `204`. + * + * **Auth:** controlling manager Supabase Bearer session or secret API key. */ - post: operations["notifyAlfredpayKycRedirectFinished"]; - delete?: never; + delete: operations["deleteManagedProfile"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/kycRedirectOpened": { + "/v1/managed-profiles/{profileId}/api-credentials": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Mark an Alfredpay redirect opened - * @description Records that the effective customer's hosted KYC or KYB redirect was opened. + * List a managed profile's API credentials + * @description Lists all credentials owned by one active child, newest first, including revoked and expired records. Public values and safe secret prefixes are returned; secret values are never returned. + * + * **Auth:** active controlling manager Supabase Bearer session or secret API key. */ - post: operations["notifyAlfredpayKycRedirectOpened"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v1/alfredpay/retryKyc": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; + get: operations["listManagedProfileApiCredentials"]; put?: never; /** - * Retry Alfredpay KYC or KYB - * @description Retries a failed KYC or KYB submission. Hosted flows return a redirect link; API-based MX, CO, and AR individual KYC returns `{ success: true }`. + * Create a managed profile API credential + * @description Issues one child-owned public/secret credential pair. The secret is returned only in this creation response and cannot be retrieved later. Expiry defaults to one year and cannot exceed two years. The child's shared cap is five active, non-expired credentials. + * + * **Auth:** active controlling manager Supabase Bearer session or secret API key. */ - post: operations["retryAlfredpayKyc"]; + post: operations["createManagedProfileApiCredential"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/sendKybSubmission": { + "/v1/managed-profiles/{profileId}/api-credentials/{credentialId}": { parameters: { query?: never; header?: never; @@ -1439,18 +909,20 @@ export interface paths { }; get?: never; put?: never; + post?: never; /** - * Send an Alfredpay KYB submission - * @description Finalizes an API-based business KYB submission. + * Revoke a managed profile API credential + * @description Revokes one credential owned by an active child, disabling both its public and secret values. Repeating revocation of the same owned credential is idempotent and returns `204`. + * + * **Auth:** active controlling manager Supabase Bearer session or secret API key. */ - post: operations["sendAlfredpayKybSubmission"]; - delete?: never; + delete: operations["revokeManagedProfileApiCredential"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/sendKycSubmission": { + "/v1/onboarding/active-entity": { parameters: { query?: never; header?: never; @@ -1458,99 +930,106 @@ export interface paths { cookie?: never; }; get?: never; - put?: never; /** - * Send an Alfredpay KYC submission - * @description Finalizes an API-based individual KYC submission. + * Select active customer entity + * @description Selects the authenticated profile's immutable active customer-entity type. Managed-child delegation is not supported. */ - post: operations["sendAlfredpayKycSubmission"]; + put: operations["selectActiveCustomerEntity"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/submitKybFile": { + "/v1/onboarding/requirements": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Upload an Alfredpay KYB file - * @description Uploads one business KYB document. Files are buffered in memory and limited to 5 MiB. + * Discover KYC or KYB requirements + * @description Returns versioned field, document, and ordered-operation metadata for an existing Avenia or Alfredpay onboarding flow. This endpoint does not return profile state or customer PII and does not replace the provider-specific operations it references. Monerium is outside this discovery proposal. */ - post: operations["submitAlfredpayKybFile"]; + get: operations["getOnboardingRequirements"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/submitKybInformation": { + "/v1/onboarding/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Submit Alfredpay KYB information - * @description Creates or updates an API-based business KYB submission, including Alfredpay's compliance questionnaire. + * Get aggregate onboarding status + * @description Returns the effective profile's customer entities and aggregated provider/KYC state. Non-terminal provider statuses may be refreshed before the response is built. */ - post: operations["submitAlfredpayKybInformation"]; + get: operations["getOnboardingStatus"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/submitKybRelatedPersonFile": { + "/v1/public-key": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Upload a related-person KYB file - * @description Uploads the front or back identity document for one KYB related person. Files are limited to 5 MiB. + * Public Key + * @description Returns the RSA-PSS 2048 / SHA-256 public key used to verify Vortex webhook signatures. This is NOT a partner `pk_*` API key. */ - post: operations["submitAlfredpayKybRelatedPersonFile"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v1/alfredpay/submitKycFile": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description RSA-PSS public key in PEM format. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...replace-with-actual-key...\n-----END PUBLIC KEY-----\n" + * } + */ + "application/json": { + /** @description RSA-PSS 2048-bit public key in PEM format. Use this key to verify webhook signatures with RSA-PSS / SHA-256. */ + publicKey: string; + }; + }; + }; + }; }; - get?: never; put?: never; - /** - * Upload an Alfredpay KYC file - * @description Uploads one individual KYC document. Files are buffered in memory and limited to 5 MiB. - */ - post: operations["submitAlfredpayKycFile"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/submitKycInformation": { + "/v1/quotes": { parameters: { query?: never; header?: never; @@ -1560,17 +1039,17 @@ export interface paths { get?: never; put?: never; /** - * Submit Alfredpay KYC information - * @description Creates or resumes an API-based individual KYC submission. + * Create a new quote + * @description Generates a quote for a specified ramp transaction, detailing input and output amounts, fees, and expiration. */ - post: operations["submitAlfredpayKycInformation"]; + post: operations["createQuote"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/kyb/attempt-status": { + "/v1/quotes/{id}": { parameters: { query?: never; header?: never; @@ -1578,10 +1057,33 @@ export interface paths { cookie?: never; }; /** - * Get Avenia KYB attempt status - * @description Refreshes an owned Avenia KYB attempt and persists its normalized verification state. + * Get existing quote + * @description Get a quote by ID. + * + * **Auth:** none. This endpoint is fully public; anyone with the quote ID can read it. */ - get: operations["getAveniaKybAttemptStatus"]; + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Quote Id. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["QuoteResponse"]; + }; + }; + }; + }; put?: never; post?: never; delete?: never; @@ -1590,7 +1092,7 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/brla/kyb/documents": { + "/v1/quotes/best": { parameters: { query?: never; header?: never; @@ -1600,17 +1102,17 @@ export interface paths { get?: never; put?: never; /** - * Create Avenia KYB document - * @description Creates an Avenia document and returns presigned upload targets. Upload bytes directly to the returned URLs. + * Create a quote for the best network + * @description Generates a new quote for the network that yields the highest output amount for the given parameters. This endpoint compares the output for a given input amount over all supported networks and returns the 'best' quote, defined as the one with the highest output. */ - post: operations["createAveniaKybDocument"]; + post: operations["createBestQuote"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/kyb/documents/{documentId}": { + "/v1/ramp-info": { parameters: { query?: never; header?: never; @@ -1618,10 +1120,12 @@ export interface paths { cookie?: never; }; /** - * Get Avenia KYB document - * @description Reads readiness and upload status for an owned Avenia KYB document. + * Get sanitized ramp eligibility + * @description Returns only sanitized per-corridor KYC state and buy/sell eligibility for the profile derived from the validated API credential. A manager secret may select one directly managed child with `X-Managed-Profile-Id`; public keys cannot use the selector. The endpoint never returns PII, provider/customer IDs, KYC failure reasons, bank/wallet data, ramp history, or exact financial limits. When both public and secret headers are supplied they must belong to the same credential. Supabase Bearer sessions do not authorize this endpoint. + * + * **Auth:** `X-Public-Key` or `X-API-Key`. */ - get: operations["getAveniaKybDocument"]; + get: operations["getRampInfo"]; put?: never; post?: never; delete?: never; @@ -1630,99 +1134,268 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/brla/kyb/new-level-1/api": { + "/v1/ramp/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Submit API-driven Avenia KYB - * @description Submits the API-driven Avenia Level 1 KYB attempt after validating the owned corporate documents and UBO references. + * Get ramp status + * @description Fetches an updated ramp process. */ - post: operations["submitAveniaKybLevel1Api"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v1/brla/kyb/new-level-1/web-sdk": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + get: { + parameters: { + query?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; + path: { + /** @description Ramp ID. */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; + }; + }; + }; + /** @description The managed-profile selector is invalid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; + }; + }; + /** @description Authentication is required for an owned ramp or managed-profile selection. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + }; + }; + /** @description Ramp ownership or managed-profile authorization failed. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + }; + }; + }; }; - get?: never; put?: never; - /** - * Start Avenia hosted KYB - * @description Starts or resumes Avenia's hosted KYB level-1 flow for an owned company subaccount. - */ - post: operations["startAveniaKybLevel1Hosted"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/kyb/ubos": { + "/v1/ramp/{id}/errors": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Create Avenia KYB UBO - * @description Registers a UBO after verifying that referenced identity documents are ready and owned by the company subaccount. + * Get ramp error logs + * @description Returns the chronological error log for a ramp. + * + * **Auth:** requires either `X-API-Key: sk_*` (partner) OR `Authorization: Bearer ` (user). Ownership is enforced. */ - post: operations["createAveniaKybUbo"]; + get: operations["getRampErrorLogs"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/brla/kyc/record-attempt": { + "/v1/ramp/history": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; /** - * Record an initial Avenia KYC attempt - * @description Records the first observed KYC attempt for a CPF or CNPJ when no provider-customer record exists yet. + * Get authenticated user ramp history + * @description Fetches all non-initial ramps owned by the authenticated user across wallet addresses. Requires a Supabase session or user-scoped secret API key. Partner-only credentials are not sufficient. */ - post: operations["recordInitialAveniaKycAttempt"]; + get: { + parameters: { + query?: { + /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ + limit?: number; + /** @description The offset for querying older transactions. */ + offset?: number; + }; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authenticated user's ramp history. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetRampHistoryResponse"]; + }; + }; + /** @description The managed-profile selector is invalid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; + }; + }; + 401: components["responses"]["ManagedSelectorUnauthorized"]; + 403: components["responses"]["ManagedSelectorForbidden"]; + }; + }; + put?: never; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/onboarding/active-entity": { + "/v1/ramp/history/{walletAddress}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; /** - * Select active customer entity - * @description Selects the authenticated profile's immutable active customer-entity type. Managed-child delegation is not supported. + * Get ramp history for wallet address + * @description Fetches the transaction history for a given wallet address. The response returns the last 20 items by default. This can be adjusted by using the `limit` and `offset` query parameters. */ - put: operations["selectActiveCustomerEntity"]; + get: { + parameters: { + query?: { + /** @description The maximum count of transaction items returned in this query. The maximum value is `100`. */ + limit?: number; + /** @description The offset for querying the transactions. Necessary if the number of transaction items of the address is larger than the maximum limit. A larger value will return older transaction items. */ + offset?: number; + }; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; + path: { + /** @description The wallet address for which the ramp history is queried for. */ + walletAddress: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetRampHistoryResponse"]; + }; + }; + /** @description The managed-profile selector is invalid. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; + }; + }; + 401: components["responses"]["ManagedSelectorUnauthorized"]; + 403: components["responses"]["ManagedSelectorForbidden"]; + }; + }; + put?: never; post?: never; delete?: never; options?: never; @@ -1730,71 +1403,80 @@ export interface paths { patch?: never; trace?: never; }; - "/v1/onboarding/requirements": { + "/v1/ramp/register": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Discover KYC or KYB requirements - * @description Returns versioned field, document, and ordered-operation metadata for an existing Avenia or Alfredpay onboarding flow. This endpoint does not return profile state or customer PII and does not replace the provider-specific operations it references. Monerium is outside this discovery proposal. + * Register new ramp process + * @description Initiates a new on-ramp or off-ramp process by providing quote details, signing accounts, and additional data. */ - get: operations["getOnboardingRequirements"]; - put?: never; - post?: never; + post: operations["registerRamp"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/onboarding/status": { + "/v1/ramp/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * Get aggregate onboarding status - * @description Returns the effective profile's customer entities and aggregated provider/KYC state. Non-terminal provider statuses may be refreshed before the response is built. + * Start ramp process + * @description Starts a ramp process. + * + * It is assumed all required information from the client has already been sent using the `update` endpoint. This endpoint is only used to tell the backend any external operation (like a bank transfer) has been completed, and the ramp can start. */ - get: operations["getOnboardingStatus"]; - put?: never; - post?: never; + post: operations["startRamp"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/fiatAccounts": { + "/v1/ramp/update": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * List Alfredpay fiat accounts - * @description Lists payout fiat accounts for the effective Alfredpay customer. - */ - get: operations["listAlfredpayFiatAccounts"]; + get?: never; put?: never; /** - * Create an Alfredpay fiat account - * @description Creates a payout fiat account for the effective Alfredpay customer. Required optional fields depend on the selected account type and corridor. + * Update ramp process + * @description Submits presigned transactions and additional data to an existing ramp process before starting it. + * This endpoint can be called many times, and data can be incrementally added to the ramp. + * + * Note: For both pre-signed transactions and the generic `additionalData` object, existing properties will be overriden by new values. + * + * ### Required data for ramps. + * The signed counterpart of the initial unsignedTxs object must be provided for all ramps, as required by the object. + * For offramps, the `additionalData` field must contain the confirmation hash corresponding to the inital transaction in which the user sends the funds. + * If the originating chain is `Assethub`, then `assetHubToPendulumHash` must be provided. + * If the originating chain is any `EVM` chain, then `squidRouterSwapHash` must be provided. `squidRouterApproveHash` is only required when an approval transaction was actually submitted; if the wallet already holds a sufficient allowance for the router, it can be omitted. + * + * For onramps, no additional data is required after registering the ramp. */ - post: operations["createAlfredpayFiatAccount"]; + post: operations["updateRamp"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/alfredpay/fiatAccounts/{fiatAccountId}": { + "/v1/session/create": { parameters: { query?: never; header?: never; @@ -1803,74 +1485,150 @@ export interface paths { }; get?: never; put?: never; - post?: never; - /** - * Delete an Alfredpay fiat account - * @description Deletes one payout fiat account belonging to the effective Alfredpay customer. - */ - delete: operations["deleteAlfredpayFiatAccount"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/v1/managed-profiles": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; /** - * List managed profiles - * @description Lists children owned by the authenticated active manager, newest first. The default filter returns only active children. Use `status=deleted` or `status=all` to include retained logical-deletion records. + * Create widget session + * @description Creates a hosted Vortex Widget session and returns the URL to open for the user. * - * **Auth:** controlling manager Supabase Bearer session or secret API key. Public API keys and direct managed-child credentials are rejected. - */ - get: operations["listManagedProfiles"]; - put?: never; - /** - * Create a managed profile - * @description Creates one headless individual or business child for the authenticated active manager. `externalSubjectId`, normalized `contactEmail`, and `customerType` are immutable. An exact retry for the same external subject is idempotent and returns the existing active child with `200`; the first creation returns `201`. Reusing either reserved identifier with different data returns `409`, including after deletion. No corridor grant is accepted. + * This single endpoint supports two mutually exclusive request shapes: * - * **Auth:** controlling manager Supabase Bearer session or secret API key. Public API keys and direct managed-child credentials are rejected. + * - **Fixed quote** (`GetWidgetUrlLocked`) — pass a `quoteId` you created via `POST /v1/quotes`. The widget uses that exact quote and does not refresh it. If the quote expires before the user finishes, they must close the window and start over. + * + * - **Auto-refresh** (`GetWidgetUrlRefresh`) — pass the route parameters (`network`, `rampType`, `inputAmount`, plus `fiat` / `cryptoLocked` / `paymentMethod` as relevant for the direction). The widget creates and refreshes quotes on demand for the user. + * + * Use the example switcher below to see the request shape for each mode. `externalSessionId` is required in both modes and is echoed back in webhook payloads. */ - post: operations["createManagedProfile"]; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GetWidgetUrlLocked"] | components["schemas"]["GetWidgetUrlRefresh"]; + }; + }; + responses: { + /** @description Returned when a fixed-quote session was created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id"eId=quote_01HXY..." + * } + */ + "application/json": { + /** @description The widget URL to open for the user. */ + url: string; + }; + }; + }; + /** @description Returned when an auto-refresh session was created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "url": "https://www.vortexfinance.co/widget?externalSessionId=my-session-id&rampType=BUY&network=polygon&inputAmount=150&fiat=BRL&cryptoLocked=USDC&paymentMethod=pix" + * } + */ + "application/json": { + /** @description The widget URL to open for the user. */ + url: string; + }; + }; + }; + /** @description Missing required fields, or `quoteId` not provided for fixed-quote mode and route fields not provided for auto-refresh mode. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Quote not found or expired (fixed-quote mode only). */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/managed-profiles/{profileId}": { + "/v1/supported-countries": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Get a managed profile - * @description Returns one owned child, including a retained logically deleted child. Foreign children are hidden with `404`. - * - * **Auth:** controlling manager Supabase Bearer session or secret API key. - */ - get: operations["getManagedProfile"]; + /** Supported Countries */ + get: { + parameters: { + query?: { + /** + * @description ISO code: "BR", "AR", etc. + * @example + */ + countryCode?: string; + /** @description e.g. "Brazil", "Germany" */ + name?: string; + /** @description e.g. "BRL". All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ + fiatCurrency?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + countries: { + /** @description e.g. `DE` */ + countryCode: string; + }[]; + /** @description e.g. 🇩🇪 */ + emoji: string; + /** @description e.g. `Germany` */ + name: string; + support: { + /** @description e.g. `true` */ + buy: boolean; + /** @description e.g. `true` */ + sell: boolean; + }; + /** @description All the supported currencies you can get from `supported-fiat-currencies` endpoint. */ + supportedCurrencies: string[]; + }; + }; + }; + }; + }; put?: never; post?: never; - /** - * Delete a managed profile - * @description Logically deletes an owned child and atomically revokes all of its credentials. Customer, provider, KYC, ramp, external-subject, and contact-email records are retained. Repeating deletion of the same owned child is idempotent and returns `204`. - * - * **Auth:** controlling manager Supabase Bearer session or secret API key. - */ - delete: operations["deleteManagedProfile"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/managed-profiles/{profileId}/api-credentials": { + "/v1/supported-cryptocurrencies": { parameters: { query?: never; header?: never; @@ -1878,43 +1636,285 @@ export interface paths { cookie?: never; }; /** - * List a managed profile's API credentials - * @description Lists all credentials owned by one active child, newest first, including revoked and expired records. Public values and safe secret prefixes are returned; secret values are never returned. - * - * **Auth:** active controlling manager Supabase Bearer session or secret API key. + * Supported Cryptocurrencies + * @description Retrieve all supported cryptocurrencies, filtered by network. */ - get: operations["listManagedProfileApiCredentials"]; + get: { + parameters: { + query?: { + /** + * @description Filter supported cryptocurrencies by network. Allowed values: `assethub`, `avalanche`, `base`, `bsc`, `ethereum`, `polygon` + * @example + */ + network?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + cryptocurrencies: { + /** @description Defined if network is EVM. */ + assetContractAddress?: string | null; + assetDecimals: number; + /** @description Defined if network is Assethub. */ + assetForeignAssetId?: string | null; + assetNetwork: components["schemas"]["Networks"]; + assetSymbol: string; + }[]; + }; + }; + }; + }; + }; put?: never; - /** - * Create a managed profile API credential - * @description Issues one child-owned public/secret credential pair. The secret is returned only in this creation response and cannot be retrieved later. Expiry defaults to one year and cannot exceed two years. The child's shared cap is five active, non-expired credentials. - * - * **Auth:** active controlling manager Supabase Bearer session or secret API key. - */ - post: operations["createManagedProfileApiCredential"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v1/managed-profiles/{profileId}/api-credentials/{credentialId}": { + "/v1/supported-fiat-currencies": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; - /** - * Revoke a managed profile API credential - * @description Revokes one credential owned by an active child, disabling both its public and secret values. Repeating revocation of the same owned credential is idempotent and returns `204`. - * - * **Auth:** active controlling manager Supabase Bearer session or secret API key. - */ - delete: operations["revokeManagedProfileApiCredential"]; + /** Supported Fiat Currencies */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + currencies: { + /** @description e.g. `2` */ + decimals: number; + /** @description e.g. `Brazilian Real` */ + name: string; + /** @description e.g. `BRL` */ + symbol: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/supported-payment-methods": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Supported Payment Methods + * @description Retrieve all available payment methods, filtered by type or fiat. + */ + get: { + parameters: { + query?: { + /** + * @description Filter supported payment methods by the ramp type. Allowed values: `sell` or `buy`. + * @example + */ + type?: string; + /** + * @description Filter supported payment methods by fiat currency. Allowed values: `EUR`, `ARS`, `BRL`, `USD`, `MXN`, `COP`. + * @example + */ + fiat?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Array of supported payment methods matching the params. */ + "paymentMethods:": { + /** @description Unique identifier of the payment method: `sepa`, `pix`, `cbu` */ + id: string; + /** @description Payment method limits in USD */ + limits: { + max: number; + min: number; + }; + /** @description Unique name of the payment method: `SEPA`, `PIX`, `CBU` */ + name: string; + /** @description Array of supported fiat currencies by payment method. */ + supportedFiats: string[]; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhook": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register Webhook + * @description Register a new webhook to receive event notifications. + * + * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. + * + * Webhooks are bound to the account behind your secret key: a `quoteId` must belong to a quote created with your key (any other quote returns `404`). The callback URL must use HTTPS, must not embed credentials, and must resolve to a publicly routable address; private or reserved IP ranges are rejected. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + events?: string[]; + /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific quote. The quote must have been created with your API key. */ + quoteId?: string; + /** @description (required* one of two: quoteId or sessionId): Subscribe to events for a specific session */ + sessionId?: string; + /** @description Your HTTPS webhook endpoint URL. No embedded credentials; must resolve to a publicly routable address. */ + url: string; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "createdAt": "2025-10-01T16:21:04.648Z", + * "events": [ + * "TRANSACTION_CREATED", + * "STATUS_CHANGE" + * ], + * "id": "340ba946-f3f3-4007-893c-3374bfcd096b", + * "isActive": true, + * "quoteId": "3258910e-93ee-443e-b793-28cc1d4ccdf3", + * "sessionId": null, + * "url": "https://your-website.com" + * } + */ + "application/json": { + /** @description The creation date of the webhook */ + createdAt: string; + /** @description The events the webhook is subscribed for */ + events: string[]; + /** @description Webhook UUID */ + id: string; + /** @description Is the webhook active */ + isActive: boolean; + /** @description (optional): The specific transactionId that the events are subscribed for */ + quoteId?: string; + /** @description (optional): The specific sessionId that the events are subscribed for */ + sessionId?: string; + /** @description Your HTTPS webhook endpoint URL */ + url: string; + }; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/webhook/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete Webhook + * @description Remove a webhook subscription. + * + * **Auth:** requires `X-API-Key: sk_*`. Supabase Bearer is NOT accepted on webhook endpoints. + * + * Deletion is scoped to your account: a webhook registered by another account returns `404`. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + message: string; + success: boolean; + }; + }; + }; + }; + }; options?: never; head?: never; patch?: never; @@ -1933,844 +1933,744 @@ export interface components { */ type: "EVM" | "Substrate"; }; - ApiCredential: { - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - environment: "live" | "test"; - /** Format: date-time */ - expiresAt: string; - /** Format: uuid */ - id: string; - name: string; - /** Format: uuid */ - partnerId: string | null; - /** Format: uuid */ - profileId: string; - /** @description Retrievable public half of the credential. */ - publicKey: string; - /** Format: date-time */ - publicLastUsedAt: string | null; - /** Format: date-time */ - revokedAt: string | null; - /** @description Non-secret 16-character lookup/display prefix. The secret value is not retrievable. */ - secretKeyPrefix: string; - /** Format: date-time */ - secretLastUsedAt: string | null; - /** Format: date-time */ - updatedAt: string; - }; - ApiCredentialErrorResponse: { - error: { - /** @description Machine-readable error code such as `AUTHENTICATION_REQUIRED`, `INVALID_PUBLIC_KEY`, `INVALID_SECRET_KEY`, `CREDENTIAL_MISMATCH`, `CREDENTIAL_LIMIT_REACHED`, `CREDENTIAL_NOT_FOUND`, `CREDENTIAL_SUBJECT_REQUIRED`, `INVALID_CREDENTIAL_EXPIRY`, or `INVALID_CREDENTIAL_NAME`. */ - code: string; - message: string; - status: number; - }; + AlfredpayAddFiatAccountRequest: { + accountBankCode?: string; + accountName?: string; + accountNumber: string; + accountType?: string; + bankCity?: string; + bankCountry?: string; + bankPostalCode?: string; + bankState?: string; + bankStreet?: string; + beneficiaryCity?: string; + beneficiaryCountry?: string; + beneficiaryPostalCode?: string; + beneficiaryState?: string; + beneficiaryStreet?: string; + country: components["schemas"]["AlfredpayCountry"]; + documentNumber?: string; + documentType?: string; + isExternal?: boolean; + routingNumber?: string; + type: components["schemas"]["AlfredpayFiatAccountType"]; }; /** @enum {string} */ - AveniaDocumentType: "ID" | "DRIVERS-LICENSE" | "PASSPORT" | "RESIDENCE-PERMIT" | "SELFIE" | "SELFIE-FROM-LIVENESS" | "CERTIFICATE-OF-INCORPORATION" | "COMPANY-TAX-IDENTIFICATION-DOCUMENT"; - AveniaKYCDataUploadRequest: { - /** @enum {string} */ - documentType: "ID" | "DRIVERS-LICENSE"; - isDoubleSided?: boolean; - /** @description CPF or CNPJ. */ - taxId: string; + AlfredpayCountry: "AR" | "CO" | "MX" | "US"; + AlfredpayCountryAndCustomerTypeRequest: { + country: components["schemas"]["AlfredpayCountry"]; + type?: components["schemas"]["AlfredpayCustomerType"]; }; - AveniaKYCDataUploadResponse: { - idUpload: components["schemas"]["DocumentUploadEntry"]; - selfieUpload: components["schemas"]["DocumentUploadEntry"]; + AlfredpayCountryRequest: { + country: components["schemas"]["AlfredpayCountry"]; }; - BrlaAddress: { - cep: string; - city: string; - complement?: string | null; - district: string; - number: string; - state: string; - street: string; + AlfredpayCreateCustomerRequest: { + country: components["schemas"]["AlfredpayCountry"]; }; - BrlaErrorResponse: { - /** @description Detailed error message or object from BRLA API or server. */ - details?: null & (string | { - [key: string]: unknown; - }); - /** @description A summary of the error. */ - error?: string; + AlfredpayCreateCustomerResponse: { + /** Format: date-time */ + createdAt: string; }; - BrlaGetSelfieLivenessUrlResponse: { - id: string; - livenessUrl: string; - uploadURLFront: string; - validateLivenessToken: string; + AlfredpayCreateFiatAccountResponse: { + fiatAccountId: string; }; - BrlaValidatePixKeyResponse: { - valid: boolean; + /** @enum {string} */ + AlfredpayCustomerType: "INDIVIDUAL" | "BUSINESS"; + AlfredpayErrorResponse: { + error: string; + fields?: { + field: string; + message: string; + }[]; }; - CleanupPhase: { - /** @enum {string} */ - string?: "moonbeamCleanup" | "pendulumCleanup" | "stellarCleanup"; + AlfredpayFiatAccount: { + accountName?: string; + accountNumber: string; + accountType: string; + /** Format: date-time */ + createdAt?: string; + customerId: string; + fiatAccountId: string; + routingNumber?: string; + type: components["schemas"]["AlfredpayFiatAccountType"]; + } & { + [key: string]: unknown; }; - /** @description Allowed values: `AR`, `BR`, `EU` */ - CountryCode: string; - CreateApiCredentialRequest: { - /** - * Format: date-time - * @description Optional future ISO-8601 expiry, at most two years from creation. Defaults to one year. - */ - expiresAt?: string; - /** @description Optional credential display name. The server uses `API Credential` when omitted. */ - name?: string; + /** @enum {string} */ + AlfredpayFiatAccountType: "ACH" | "ACH_BOL" | "ACH_CHL" | "ACH_DOM" | "B89" | "BANK_CN" | "BANK_USA" | "COELSA" | "PIX" | "SPEI"; + AlfredpayKybBusinessSummary: { + relatedPersons: { + idRelatedPerson: string; + }[]; + submissionId: string; }; - CreateApiCredentialResponse: components["schemas"]["ApiCredential"] & { - /** @description Returned only at creation. Store it immediately in a server-side secret manager. */ - secretKey: string; + AlfredpayKybDetailsResponse: { + relatedPersons: { + idRelatedPerson: string; + }[]; + submissionId: string; + }[]; + AlfredpayKybFileUploadRequest: { + /** @enum {string} */ + country: components["schemas"]["AlfredpayCountry"]; + /** Format: binary */ + file: string; + /** @enum {string} */ + fileType: "taxIdDocument" | "articlesIncorporation" | "proofAddress" | "shareholderRegistry" | "businessLicense" | "uploadAmlPolicy"; + submissionId: string; }; - CreateBestQuoteRequest: { - /** @description Your api key, if available. */ - apiKey?: string; - countryCode?: components["schemas"]["CountryCode"]; - /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "BUY". */ - from?: components["schemas"]["PaymentMethod"]; - /** - * @description The amount of currency to be input. - * @example 100.00 - */ - inputAmount: string; - /** @description The currency type for the input amount. */ - inputCurrency: components["schemas"]["RampCurrency"]; - /** @description Optional whitelist of networks to evaluate when searching for the best quote. If omitted or empty, all eligible networks for the corridor are considered. */ - networks?: components["schemas"]["Networks"][]; - /** @description The desired currency type for the output amount. */ - outputCurrency: components["schemas"]["RampCurrency"]; - /** @description Your partner ID, if available. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - /** @description The type of ramp process (on-ramp or off-ramp). */ - rampType: components["schemas"]["RampDirection"]; - /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "SELL". */ - to?: components["schemas"]["PaymentMethod"]; + AlfredpayKybRelatedPerson: { + cpf?: string; + /** Format: date */ + dateOfBirth: string; + dni?: string; + /** Format: email */ + email: string; + firstName: string; + lastName: string; + nationalities: string[]; + pep?: boolean; }; - CreateQuoteRequest: { - /** @description Your api key, if available. */ - apiKey?: string; - countryCode?: components["schemas"]["CountryCode"]; - /** @description From destination */ - from: components["schemas"]["DestinationType"]; - /** - * @description The amount of currency to be input. - * @example 100.00 - */ - inputAmount: string; - /** @description The currency type for the input amount. */ - inputCurrency: components["schemas"]["RampCurrency"]; - network?: components["schemas"]["Networks"]; - /** @description The desired currency type for the output amount. */ - outputCurrency: components["schemas"]["RampCurrency"]; - /** @description Your partner ID, if available. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - /** @description The type of ramp process (on-ramp or off-ramp). */ - rampType: components["schemas"]["RampDirection"]; - /** @description To destination */ - to: components["schemas"]["DestinationType"]; + AlfredpayKybRelatedPersonFileUploadRequest: { + /** @enum {string} */ + country: "CO" | "MX"; + /** Format: binary */ + file: string; + /** @enum {string} */ + fileType: "docFront" | "docBack"; + relatedPersonId: string; }; - CreateSubaccountRequest: { + AlfredpayKycFileUploadRequest: { /** @enum {string} */ - accountType: "INDIVIDUAL" | "COMPANY"; - /** @description Individual full name or company legal name. */ - name: string; - quoteId?: string; - sessionId?: string; - /** @description CPF for an individual or CNPJ for a company. */ - taxId: string; + country: components["schemas"]["AlfredpayCountry"]; + /** Format: binary */ + file: string; + /** @enum {string} */ + fileType: "National ID Front" | "National ID Back" | "Selfie"; + submissionId: string; }; - CreateSubaccountResponse: { - /** @description The ID of the created or processed subaccount. */ - subAccountId: string; + AlfredpayKycStatusResponse: { + alfred_pay_id: string; + country: components["schemas"]["AlfredpayCountry"]; + lastFailure?: string; + status: components["schemas"]["AlfredpayStatus"]; + /** Format: date-time */ + updated_at: string; }; - /** - * @description Represents either a blockchain network or a traditional payment method. - * @enum {string} - */ - DestinationType: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam" | "pendulum" | "stellar" | "pix" | "sepa" | "cbu" | "ach" | "spei"; - DocumentUploadEntry: { - id: string; - livenessUrl?: string; - uploadURLBack?: string; - uploadURLFront: string; - validateLivenessToken?: string; + AlfredpayManagedBadRequestResponse: components["schemas"]["AlfredpayErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; + AlfredpayRedirectLinkResponse: { + submissionId: string; + /** Format: uri */ + verification_url: string; }; - ErrorResponse: { - /** @description HTTP status code returned by the API error handler. */ - code?: number; - /** @description Validation error details, when the request fails schema or input validation. */ - errors?: Record[]; - /** @description A human-readable error message. */ - message?: string; - /** @description HTTP status code included by selected provider-style error responses. */ - statusCode?: number; - /** @description Provider-style error category, when available. */ - type?: string; + AlfredpayRedirectNotificationRequest: { + country: components["schemas"]["AlfredpayCountry"]; + type?: components["schemas"]["AlfredpayCustomerType"]; }; - /** @enum {string} */ - FiatToken: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP"; - GetKycStatusResponse: { + AlfredpayRelatedPersonFileUploadRequest: { + country: components["schemas"]["AlfredpayCountry"]; + /** Format: binary */ + file: string; /** @enum {string} */ - failureReason?: "face" | "name" | "birthdate" | "unknown" | "tax_id"; - /** @description The KYC level achieved. */ - level: string; - /** @enum {string} */ - result?: "APPROVED" | "REJECTED"; - /** - * @description The KYC status. - * @enum {string} - */ - status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; - /** - * @description Event type, typically "KYC". - * @enum {string} - */ - type: "KYC"; - }; - GetRampErrorLogsResponse: components["schemas"]["RampErrorLog"][]; - GetRampHistoryResponse: { - totalCount: string; - transactions: components["schemas"]["GetRampHistoryTransaction"]; - }; - GetRampHistoryTransaction: { - currentPhase: components["schemas"]["RampPhase"]; - date: string; - /** @description The deadline for starting an initial ramp. */ - expiresAt: string; - /** @description A link to the transaction explorer of the blockchain showing the details of the transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ - externalTxExplorerLink?: string; - /** @description The hash of the blockchain transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ - externalTxHash?: string; - from: components["schemas"]["DestinationType"]; - fromAmount: string; - fromCurrency: components["schemas"]["RampCurrency"]; - id: string; - status: components["schemas"]["SimpleStatus"]; - to: components["schemas"]["DestinationType"]; - toAmount: string; - toCurrency: components["schemas"]["RampCurrency"]; - type: components["schemas"]["RampDirection"]; - /** @description Destination address for a BUY ramp when available. */ - walletAddress?: string; - }; - GetUserLimitsRequest: { - corridors: ("AR" | "BR" | "CO" | "MX" | "US")[]; - }; - GetUserLimitsResponse: { - limits: components["schemas"]["UserLimit"][]; - }; - GetUserRemainingLimitResponse: { - /** - * Format: double - * @description The remaining limit for the requested direction. - */ - remainingLimit: number; + fileType: "docFront" | "docBack"; + relatedPersonId: string; }; - GetUserResponse: { - /** @description The user's EVM wallet address. */ - evmAddress: string; + AlfredpayRetryRequest: components["schemas"]["AlfredpayRedirectNotificationRequest"]; + AlfredpayRetryResponse: components["schemas"]["AlfredpayRedirectLinkResponse"] | components["schemas"]["SuccessResponse"]; + AlfredpaySendSubmissionRequest: { /** @enum {string} */ - identityStatus: "NOT-IDENTIFIED" | "CONFIRMED"; - /** @description The user's KYC level. */ - kycLevel: number; - subAccountId: string; - }; - GetWidgetUrlLocked: { - /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ - callbackUrl?: string; - /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ - externalSessionId?: string; - /** @description Pass the ID of an existing quote to make the widget lock in that particular quote without allowing to change it. */ - quoteId: string; - /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ - walletAddressLocked?: string; + country: components["schemas"]["AlfredpayCountry"]; + submissionId: string; }; - GetWidgetUrlRefresh: { - /** @description Your api key, if available. This is passed to all the quotes generated in this widget session. */ - apiKey?: string; - /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ - callbackUrl?: string; - countryCode?: components["schemas"]["CountryCode"]; - cryptoLocked?: components["schemas"]["OnChainToken"]; - /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ - externalSessionId: string; - fiat?: components["schemas"]["FiatToken"]; - inputAmount: string; - network: components["schemas"]["Networks"]; - /** @description The identifier of a partner. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - rampType: components["schemas"]["RampDirection"]; - /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ - walletAddressLocked?: string; + /** @enum {string} */ + AlfredpayStatus: "CONSULTED" | "LINK_OPENED" | "USER_COMPLETED" | "VERIFYING" | "FAILED" | "SUCCESS" | "UPDATE_REQUIRED"; + AlfredpayStatusResponse: { + country: components["schemas"]["AlfredpayCountry"]; + /** Format: date-time */ + creationTime: string; + status: components["schemas"]["AlfredpayStatus"]; }; - KYCDataUploadFileFiles: { - /** Format: url */ - CNHUploadUrl?: string; - /** Format: url */ - RGBackUploadUrl?: string; - /** Format: url */ - RGFrontUploadUrl?: string; - /** Format: url */ - selfieUploadUrl?: string; + AlfredpaySubmissionResponse: { + submissionId: string; }; - /** @enum {string} */ - KYCDocType: "RG" | "CNH"; - KycLevel1Payload: { + AlfredpaySubmitKybInformationRequest: { + accountPurpose: string; + address: string; + businessActivities: string; + businessName: string; city: string; - country: string; - countryOfTaxId: string; - /** @description ISO date (YYYY-MM-DD). */ + complianceScreeningDescription?: string; + conductsComplianceScreening?: boolean; + country: components["schemas"]["AlfredpayCountry"]; + expectedMonthlyTransactions: number; + expectedMonthlyVolumeUsd: number; + isRegulatedBusiness: boolean; + operatesInSanctionedCountries: boolean; + relatedPersons: components["schemas"]["AlfredpayKybRelatedPerson"][]; + sourceOfFunds: string; + state: string; + taxId: string; + transmitsCustomerFunds: boolean; + walletAddresses: string; + website: string; + zipCode: string; + } & (unknown & unknown); + AlfredpaySubmitKycInformationRequest: { + address: string; + city: string; + country: components["schemas"]["AlfredpayCountry"]; + countryCode?: string; + cuit?: string; + /** Format: date */ dateOfBirth: string; + dni: string; /** Format: email */ - email: string; - fullName: string; + email?: string; + firstName: string; + lastName: string; + nationalities?: string[]; + pep?: boolean; + phoneNumber?: string; state: string; - streetAddress: string; - subAccountId: string; - taxIdNumber: string; - uploadedDocumentId: string; - uploadedSelfieId: string; + typeDocument?: string; + /** @enum {string} */ + typeDocumentAr?: "DNI"; + /** @enum {string} */ + typeDocumentCol?: "CC" | "CE"; zipCode: string; + } & unknown; + AlfredpaySuccessResponse: { + /** @constant */ + success: true; }; - KycLevel1Response: { + AlfredpayValidationBadRequestResponse: components["schemas"]["AlfredpayErrorResponse"] | components["schemas"]["ApiValidationErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; + ApiCredential: { + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + environment: "live" | "test"; + /** Format: date-time */ + expiresAt: string; + /** Format: uuid */ id: string; + name: string; + /** Format: uuid */ + partnerId: string | null; + /** Format: uuid */ + profileId: string; + /** @description Retrievable public half of the credential. */ + publicKey: string; + /** Format: date-time */ + publicLastUsedAt: string | null; + /** Format: date-time */ + revokedAt: string | null; + /** @description Non-secret 16-character lookup/display prefix. The secret value is not retrievable. */ + secretKeyPrefix: string; + /** Format: date-time */ + secretLastUsedAt: string | null; + /** Format: date-time */ + updatedAt: string; }; - ListApiCredentialsResponse: { - credentials: components["schemas"]["ApiCredential"][]; + ApiCredentialErrorResponse: { + error: { + /** @description Machine-readable error code such as `AUTHENTICATION_REQUIRED`, `INVALID_PUBLIC_KEY`, `INVALID_SECRET_KEY`, `CREDENTIAL_MISMATCH`, `CREDENTIAL_LIMIT_REACHED`, `CREDENTIAL_NOT_FOUND`, `CREDENTIAL_SUBJECT_REQUIRED`, `INVALID_CREDENTIAL_EXPIRY`, or `INVALID_CREDENTIAL_NAME`. */ + code: string; + message: string; + status: number; + }; + }; + ApiCredentialManagedSelectorErrorResponse: components["schemas"]["ApiCredentialErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; + ApiValidationErrorResponse: { + code: number; + errors?: { + message: string; + }[]; + message: string; }; - /** - * @description Supported blockchain networks. - * @enum {string} - */ - Networks: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam"; /** @enum {string} */ - OnChainToken: "USDC" | "USDT" | "ETH" | "USDC.E"; - /** @description Data related to the payment for the ramp transaction. */ - PaymentData: { - /** - * @description The amount for the payment. - * @example 0.05 - */ - amount?: string; - /** - * @description The target account for an anchor operation. - * @example GDSDQLBVDD5RZYKNDM2LAX5JDNNQOTSZOKECUYEXYMUZMAPXTMDUJCVF - */ - anchorTargetAccount?: string; - /** - * @description The memo content. - * @example 1204asjfnaksf10982e4 - */ - memo?: string; - /** - * @description Type of memo (e.g., text, id). - * @example text - */ - memoType?: string; + AveniaDocumentType: "ID" | "DRIVERS-LICENSE" | "PASSPORT" | "RESIDENCE-PERMIT" | "SELFIE" | "SELFIE-FROM-LIVENESS" | "CERTIFICATE-OF-INCORPORATION" | "COMPANY-TAX-IDENTIFICATION-DOCUMENT"; + AveniaKYCDataUploadRequest: { + /** @enum {string} */ + documentType: "ID" | "DRIVERS-LICENSE"; + isDoubleSided?: boolean; + /** @description CPF or CNPJ. */ + taxId: string; }; - /** @description `PIX`, `SEPA`, `CBU` */ - PaymentMethod: string; - /** @description Represents a transaction that has been presigned. Based on UnsignedTx structure. */ - PresignedTx: { - /** @description Any additional metadata associated with the transaction. Can be an empty object. */ - meta?: { - [key: string]: unknown; + AveniaKYCDataUploadResponse: { + idUpload: components["schemas"]["DocumentUploadEntry"]; + selfieUpload: components["schemas"]["DocumentUploadEntry"]; + }; + AveniaKybAttemptStatusResponse: { + failureReason?: string; + /** @enum {string} */ + result?: "APPROVED" | "REJECTED"; + retryable?: boolean; + /** @enum {string} */ + status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; + }; + AveniaKybDocumentRequest: { + documentType: components["schemas"]["AveniaDocumentType"]; + isDoubleSided?: boolean; + }; + AveniaKybDocumentResponse: { + document: { + documentType: components["schemas"]["AveniaDocumentType"]; + id: string; + ready: boolean; + uploadErrorBack?: string; + uploadErrorFront?: string; + uploadStatusBack?: string; + uploadStatusFront: string; }; - /** - * Format: int64 - * @description Nonce for the transaction, if applicable. - */ - nonce?: number; - /** - * @description The phase this transaction belongs to within the ramp logic. - * @enum {string} - */ - phase?: "RampPhase" | "CleanupPhase"; - /** @description Address of the account that signed/will sign this transaction. */ - signer?: string; - /** - * @description The presigned transaction payload or relevant data. - * @example AAAAAKg... - */ - txData?: string; - } & { - [key: string]: unknown; }; - QuoteResponse: { - anchorFeeFiat: string; - anchorFeeUSD: string; + AveniaKybDocumentUploadResponse: { + id: string; + /** Format: uri */ + livenessUrl?: string; + /** Format: uri */ + uploadURLBack?: string; + /** Format: uri */ + uploadURLFront: string; + validateLivenessToken?: string; + }; + AveniaKybHostedResponse: { + attemptId: string; + /** Format: uri */ + authorizedRepresentativeUrl: string; + /** Format: uri */ + basicCompanyDataUrl: string; + }; + AveniaKybLevel1Payload: { + businessActivityDescription: string; + certificateOfIncorporationDocumentId: string; + companyCity: string; + companyCountry: string; + companyLegalName: string; + companyRegistrationNumber: string; + companyState: string; + companyStreetLine1: string; + companyStreetLine2?: string; + companyStreetLine3?: string; + companyZipCode: string; + countrySubdivisionTaxResidence?: string; + countryTaxResidence: string; + /** Format: email */ + emailPixKey?: string; + /** @enum {string} */ + estimatedAnnualRevenueUsd: "less_than_100k" | "100k_to_1m" | "1m_to_10m" | "10m_to_50m" | "50m_to_100m" | "more_than_100m"; + estimatedMonthlyVolumeUsd: string; + /** @enum {string} */ + numberOfEmployees: "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001+"; + /** @enum {string} */ + reasonForAccountOpening: "charitable_donations" | "ecommerce_retail_payments" | "investment_purposes" | "other" | "payments_to_friends_or_family_abroad" | "payroll" | "personal_or_living_expenses" | "protect_wealth" | "purchase_goods_and_services" | "receive_payments_for_goods_and_services" | "tax_optimization" | "third_party_money_transmission" | "treasury_management"; + sandboxReject?: boolean; + /** Format: uri */ + socialMedia?: string; + /** @enum {string} */ + sourceOfFundsAndIncome: "business_loans" | "grants" | "inter_company_funds" | "investment_proceeds" | "legal_settlement" | "owners_capital" | "pension_retirement" | "sale_of_assets" | "sales_of_goods_and_services" | "third_party_funds" | "treasury_reserves"; + taxIdentificationDocumentId: string; + taxIdentificationNumberTin: string; + uboIds: string[]; + /** Format: uri */ + website?: string; + }; + /** @enum {string} */ + AveniaUboControlRole: "CEO" | "CFO" | "COO" | "CTO" | "President" | "Vice President" | "Director" | "Managing Director" | "Managing Partner" | "General Partner" | "Partner" | "Secretary" | "Treasurer" | "Chairman" | "Board Member" | "Authorized Signatory" | "General Counsel" | "Owner" | "Founder" | "Manager" | "Member" | "Comptroller" | "Chief Compliance Officer"; + AveniaUboPayload: { + city: string; + country: string; + countryOfTaxId: string; + /** Format: date */ + dateOfBirth: string; + documentCountry: string; + /** Format: email */ + email?: string; + fullName: string; + hasControl?: components["schemas"]["AveniaUboControlRole"]; + percentageOfOwnership: string; + phone?: string; + state: string; + streetLine1: string; + streetLine2?: string; + streetLine3?: string; + taxIdNumber: string; + uploadedIdentificationId: string; + uploadedSelfieId?: string; + zipCode: string; + }; + AveniaUboResponse: { + id: string; + }; + BrlaAddress: { + cep: string; + city: string; + complement?: string | null; + district: string; + number: string; + state: string; + street: string; + }; + BrlaErrorResponse: { + /** @description Detailed error message or object from BRLA API or server. */ + details?: null & (string | { + [key: string]: unknown; + }); + /** @description A summary of the error. */ + error?: string; + }; + BrlaGetSelfieLivenessUrlResponse: { + id: string; + livenessUrl: string; + uploadURLFront: string; + validateLivenessToken: string; + }; + BrlaManagedBadRequestResponse: components["schemas"]["BrlaErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; + BrlaValidatePixKeyResponse: { + valid: boolean; + }; + CleanupPhase: { + /** @enum {string} */ + string?: "moonbeamCleanup" | "pendulumCleanup" | "stellarCleanup"; + }; + /** @description Allowed values: `AR`, `BR`, `EU` */ + CountryCode: string; + CreateApiCredentialRequest: { /** * Format: date-time - * @description The timestamp when this quote expires. + * @description Optional future ISO-8601 expiry, at most two years from creation. Defaults to one year. */ expiresAt?: string; - feeCurrency: components["schemas"]["RampCurrency"]; - from?: components["schemas"]["DestinationType"]; - /** - * Format: uuid - * @description Unique identifier for the quote. - */ - id?: string; - /** @description The input amount specified in the request. */ - inputAmount?: string; - inputCurrency?: components["schemas"]["RampCurrency"]; - networkFeeFiat: string; - networkFeeUSD: string; - /** @description The calculated output amount after fees and conversions. */ - outputAmount?: string; - outputCurrency?: components["schemas"]["RampCurrency"]; - partnerFeeFiat: string; - partnerFeeUSD: string; - processingFeeFiat: string; - processingFeeUSD: string; - /** @description The type of ramp process. */ - rampType?: components["schemas"]["RampDirection"]; - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - }; - /** - * @description Represents supported currencies for ramp operations, including fiat and on-chain tokens. - * @example USDC - * @enum {string} - */ - RampCurrency: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP" | "USDC" | "USDT" | "USDC.E"; - /** @enum {string} */ - RampDirection: "BUY" | "SELL"; - RampErrorLog: { - details?: string; - error: string; - phase: components["schemas"]["RampPhase"]; - recoverable?: boolean; - /** Format: date-time */ - timestamp: string; + /** @description Optional credential display name. The server uses `API Credential` when omitted. */ + name?: string; }; - RampInfoResponse: { - /** @description Sanitized eligibility keyed by corridor country code. No exact limits, PII, provider IDs, or failure reasons are returned. */ - corridors: { - [key: string]: { - canBuy: boolean; - canSell: boolean; - /** @enum {string} */ - kycStatus: "not_started" | "pending" | "approved" | "rejected"; - }; - }; + CreateApiCredentialResponse: components["schemas"]["ApiCredential"] & { + /** @description Returned only at creation. Store it immediately in a server-side secret manager. */ + secretKey: string; }; - /** - * @description The current phase of the ramp process. - * @enum {string} - */ - RampPhase: "initial" | "timedOut" | "stellarCreateAccount" | "squidrouterApprove" | "squidrouterSwap" | "fundEphemeral" | "nablaApprove" | "nablaSwap" | "moonbeamToPendulum" | "moonbeamToPendulumXcm" | "pendulumToMoonbeam" | "assethubToPendulum" | "pendulumToAssethub" | "spacewalkRedeem" | "stellarPayment" | "subsidizePreSwap" | "subsidizePostSwap" | "brlaTeleport" | "onHoldForComplianceCheck" | "brlaPayoutOnMoonbeam" | "failed"; - RampProcess: { - anchorFeeFiat: string; - anchorFeeUSD: string; + CreateBestQuoteRequest: { + /** @description Your api key, if available. */ + apiKey?: string; countryCode?: components["schemas"]["CountryCode"]; + /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "BUY". */ + from?: components["schemas"]["PaymentMethod"]; /** - * Format: date-time - * @description Timestamp of when the ramp process was created. + * @description The amount of currency to be input. + * @example 100.00 */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; + /** @description The currency type for the input amount. */ + inputCurrency: components["schemas"]["RampCurrency"]; + /** @description Optional whitelist of networks to evaluate when searching for the best quote. If omitted or empty, all eligible networks for the corridor are considered. */ + networks?: components["schemas"]["Networks"][]; + /** @description The desired currency type for the output amount. */ + outputCurrency: components["schemas"]["RampCurrency"]; + /** @description Your partner ID, if available. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + /** @description The type of ramp process (on-ramp or off-ramp). */ + rampType: components["schemas"]["RampDirection"]; + /** @description `PIX`, `SEPA`, `CBU`. Only required if `rampType` is "SELL". */ + to?: components["schemas"]["PaymentMethod"]; }; - RegisterRampRequest: { + CreateManagedProfileRequest: { /** - * @description Optional additional data for the ramp process. - * - * For Brazil onramps, destinationAddress is required. - * - * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. + * Format: email + * @description Provider contact email. It is normalized to lowercase and remains permanently reserved within the manager, including after deletion. */ - additionalData?: { - /** @description Destination address, used for onramp. */ - destinationAddress?: string; - /** @description Auth token obtained from Monerium's API, for the current user. Only required for Monerium-related ramps. */ - moneriumAuthToken: string; - paymentData?: components["schemas"]["PaymentData"]; - /** @description PIX key for the destination account in an onramp. */ - pixDestination?: string; - /** @description Tax ID of the receiver for onramp. */ - receiverTaxId?: string; - /** @description Tax ID of the user. */ - taxId?: string; - /** @description Wallet address initiating the offramp. */ - walletAddress: string; - } & { - [key: string]: unknown; - }; + contactEmail: string; + /** @enum {string} */ + customerType: "individual" | "business"; + /** @description Immutable idempotency key for this subject within the authenticated manager. */ + externalSubjectId: string; + }; + CreateQuoteRequest: { + /** @description Your api key, if available. */ + apiKey?: string; + countryCode?: components["schemas"]["CountryCode"]; + /** @description From destination */ + from: components["schemas"]["DestinationType"]; /** - * Format: uuid - * @description The unique identifier for the quote. + * @description The amount of currency to be input. + * @example 100.00 */ - quoteId: string; - /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ - signingAccounts: { - /** @description The account address. */ - address: string; - /** - * @description The type of the account. - * @enum {string} - */ - type: "EVM" | "Substrate"; - }[]; + inputAmount: string; + /** @description The currency type for the input amount. */ + inputCurrency: components["schemas"]["RampCurrency"]; + network?: components["schemas"]["Networks"]; + /** @description The desired currency type for the output amount. */ + outputCurrency: components["schemas"]["RampCurrency"]; + /** @description Your partner ID, if available. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + /** @description The type of ramp process (on-ramp or off-ramp). */ + rampType: components["schemas"]["RampDirection"]; + /** @description To destination */ + to: components["schemas"]["DestinationType"]; }; - /** @description `PENDING`, `FAILED`, `COMPLETED` */ - SimpleStatus: string; - StartKYC2Request: { - documentType: components["schemas"]["KYCDocType"]; + CreateSubaccountRequest: { + /** @enum {string} */ + accountType: "INDIVIDUAL" | "COMPANY"; + /** @description Individual full name or company legal name. */ + name: string; + quoteId?: string; + sessionId?: string; + /** @description CPF for an individual or CNPJ for a company. */ taxId: string; }; - StartKYC2Response: { - uploadUrls?: components["schemas"]["KYCDataUploadFileFiles"]; + CreateSubaccountResponse: { + /** @description The ID of the created or processed subaccount. */ + subAccountId: string; }; - StartRampRequest: { - rampId: string; + /** + * @description Represents either a blockchain network or a traditional payment method. + * @enum {string} + */ + DestinationType: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam" | "pendulum" | "stellar" | "pix" | "sepa" | "cbu" | "ach" | "spei"; + DocumentUploadEntry: { + id: string; + livenessUrl?: string; + uploadURLBack?: string; + uploadURLFront: string; + validateLivenessToken?: string; }; - /** @enum {string} */ - TaxIdType: "CPF" | "CNPJ"; - TriggerOfframpRequest: { - /** - * @description The amount to offramp. - * @example 100.50 - */ - amount: string; - /** @description The recipient's PIX key. */ - pixKey: string; - /** @description The recipient's Tax ID for validation. */ - receiverTaxId: string; - /** @description The sender's Tax ID. */ - taxId: string; + ErrorManagedSelectorResponse: components["schemas"]["ErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; + ErrorResponse: { + /** @description HTTP status code returned by the API error handler. */ + code?: number; + /** @description Validation error details, when the request fails schema or input validation. */ + errors?: Record[]; + /** @description A human-readable error message. */ + message?: string; + /** @description HTTP status code included by selected provider-style error responses. */ + statusCode?: number; + /** @description Provider-style error category, when available. */ + type?: string; }; - TriggerOfframpResponse: { - /** @description The ID of the triggered offramp transaction. */ - offrampId?: string; + /** @enum {string} */ + FiatToken: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP"; + FlatErrorResponse: { + error: string; }; - /** @description Represents an unsigned transaction that requires user signature. Actual properties will depend on the transaction type and network. */ - UnsignedTx: { - meta?: Record; - nonce?: number; + FlatManagedSelectorErrorResponse: components["schemas"]["FlatErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; + GetKycStatusResponse: { /** @enum {string} */ - phase?: "RampPhase" | "CleanupPhase"; - signer?: string; + failureReason?: "face" | "name" | "birthdate" | "unknown" | "tax_id"; + /** @description The KYC level achieved. */ + level: string; + /** @enum {string} */ + result?: "APPROVED" | "REJECTED"; /** - * @description The unsigned transaction payload or relevant data. - * @example AAAAAKu... + * @description The KYC status. + * @enum {string} */ - txData?: string; - } & { - [key: string]: unknown; + status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; + /** + * @description Event type, typically "KYC". + * @enum {string} + */ + type: "KYC"; }; - UpdateRampRequest: { - /** @description Optional additional data, like transaction hashes from external services. */ - additionalData?: ({ - /** @description Transaction hash for AssetHub to Pendulum transfer, if applicable. */ - assetHubToPendulumHash?: string | null; - /** @description Signed message to trigger a Monerium offramp. */ - moneriumOfframpSignature: string; - /** @description Transaction hash for Squid Router approval. Optional: omit when the wallet already holds a sufficient allowance and no approval transaction was submitted. */ - squidRouterApproveHash?: string | null; - /** @description Transaction hash for Squid Router swap, if applicable. */ - squidRouterSwapHash?: string | null; - } & { - [key: string]: unknown; - }) | null; - /** @description An array of transactions that have been pre-signed by the user. */ - presignedTxs: components["schemas"]["PresignedTx"][]; + GetRampErrorLogsResponse: components["schemas"]["RampErrorLog"][]; + GetRampHistoryResponse: { + totalCount: string; + transactions: components["schemas"]["GetRampHistoryTransaction"]; + }; + GetRampHistoryTransaction: { + currentPhase: components["schemas"]["RampPhase"]; + date: string; + /** @description The deadline for starting an initial ramp. */ + expiresAt: string; + /** @description A link to the transaction explorer of the blockchain showing the details of the transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ + externalTxExplorerLink?: string; + /** @description The hash of the blockchain transaction sending the tokens to the user's wallet address. Only available for 'BUY' ramps. */ + externalTxHash?: string; + from: components["schemas"]["DestinationType"]; + fromAmount: string; + fromCurrency: components["schemas"]["RampCurrency"]; + id: string; + status: components["schemas"]["SimpleStatus"]; + to: components["schemas"]["DestinationType"]; + toAmount: string; + toCurrency: components["schemas"]["RampCurrency"]; + type: components["schemas"]["RampDirection"]; + /** @description Destination address for a BUY ramp when available. */ + walletAddress?: string; + }; + GetUserLimitsRequest: { + corridors: ("AR" | "BR" | "CO" | "MX" | "US")[]; + }; + GetUserLimitsResponse: { + limits: components["schemas"]["UserLimit"][]; + }; + GetUserRemainingLimitResponse: { /** - * @description The unique identifier of the ramp process to start. - * @example proc_12345 + * Format: double + * @description The remaining limit for the requested direction. */ - rampId: string; + remainingLimit: number; }; - UserLimit: { + GetUserResponse: { + /** @description The user's EVM wallet address. */ + evmAddress: string; /** @enum {string} */ - corridor: "AR" | "BR" | "CO" | "MX" | "US"; - currency: components["schemas"]["RampCurrency"]; - direction: components["schemas"]["RampDirection"]; - /** @description Maximum amount in the returned currency's human units. */ - max: string; - period: components["schemas"]["UserLimitPeriod"]; - /** @description Amount consumed during the period in the returned currency's human units. */ - used: string; - }; - UserLimitPeriod: { - /** - * Format: date-time - * @description Exclusive end of the reported period. - */ - endsAt: string; - /** Format: date-time */ - startsAt: string; - /** @constant */ - type: "calendar_month"; + identityStatus: "NOT-IDENTIFIED" | "CONFIRMED"; + /** @description The user's KYC level. */ + kycLevel: number; + subAccountId: string; }; - ValidatePixKeyResponse: { - /** @description Indicates if the PIX key is valid. */ - valid?: boolean; + GetWidgetUrlLocked: { + /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ + callbackUrl?: string; + /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ + externalSessionId?: string; + /** @description Pass the ID of an existing quote to make the widget lock in that particular quote without allowing to change it. */ + quoteId: string; + /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ + walletAddressLocked?: string; }; - /** @enum {string} */ - AlfredpayCountry: "AR" | "CO" | "MX" | "US"; - AlfredpayCreateCustomerRequest: { - country: components["schemas"]["AlfredpayCountry"]; + GetWidgetUrlRefresh: { + /** @description Your api key, if available. This is passed to all the quotes generated in this widget session. */ + apiKey?: string; + /** @description The widget will redirect to this callbackUrl after the user successfully created the transaction. */ + callbackUrl?: string; + countryCode?: components["schemas"]["CountryCode"]; + cryptoLocked?: components["schemas"]["OnChainToken"]; + /** @description A unique identifier for yourself to keep track of the widget session. Returned in the responses of webhooks, if registered. */ + externalSessionId: string; + fiat?: components["schemas"]["FiatToken"]; + inputAmount: string; + network: components["schemas"]["Networks"]; + /** @description The identifier of a partner. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + rampType: components["schemas"]["RampDirection"]; + /** @description Pass this parameter if you want to lock the wallet address for the user. It will not be editable in the widget. */ + walletAddressLocked?: string; }; - AlfredpayCreateCustomerResponse: { - /** Format: date-time */ - createdAt: string; + KYCDataUploadFileFiles: { + /** Format: url */ + CNHUploadUrl?: string; + /** Format: url */ + RGBackUploadUrl?: string; + /** Format: url */ + RGFrontUploadUrl?: string; + /** Format: url */ + selfieUploadUrl?: string; }; /** @enum {string} */ - AlfredpayCustomerType: "INDIVIDUAL" | "BUSINESS"; - AlfredpayKybDetailsResponse: { - relatedPersons: { - idRelatedPerson: string; - }[]; - submissionId: string; - }[]; - AlfredpayKybFileUploadRequest: { - /** @enum {string} */ - country: components["schemas"]["AlfredpayCountry"]; - /** Format: binary */ - file: string; - /** @enum {string} */ - fileType: "taxIdDocument" | "articlesIncorporation" | "proofAddress" | "shareholderRegistry" | "businessLicense" | "uploadAmlPolicy"; - submissionId: string; - }; - AlfredpayKybRelatedPerson: { - cpf?: string; - /** Format: date */ - dateOfBirth: string; - dni?: string; - /** Format: email */ - email: string; - firstName: string; - lastName: string; - nationalities: string[]; - pep?: boolean; - }; - AlfredpayKybRelatedPersonFileUploadRequest: { - /** @enum {string} */ - country: "CO" | "MX"; - /** Format: binary */ - file: string; - /** @enum {string} */ - fileType: "docFront" | "docBack"; - relatedPersonId: string; - }; - AlfredpayKycFileUploadRequest: { - /** @enum {string} */ - country: components["schemas"]["AlfredpayCountry"]; - /** Format: binary */ - file: string; - /** @enum {string} */ - fileType: "National ID Front" | "National ID Back" | "Selfie"; - submissionId: string; - }; - AlfredpayKycStatusResponse: { - alfred_pay_id: string; - country: components["schemas"]["AlfredpayCountry"]; - lastFailure?: string; - status: components["schemas"]["AlfredpayStatus"]; - /** Format: date-time */ - updated_at: string; - }; - AlfredpayRedirectLinkResponse: { - submissionId: string; - /** Format: uri */ - verification_url: string; - }; - AlfredpayRedirectNotificationRequest: { - country: components["schemas"]["AlfredpayCountry"]; - type?: components["schemas"]["AlfredpayCustomerType"]; - }; - AlfredpayRetryRequest: components["schemas"]["AlfredpayRedirectNotificationRequest"]; - AlfredpayRetryResponse: components["schemas"]["AlfredpayRedirectLinkResponse"] | components["schemas"]["SuccessResponse"]; - AlfredpaySendSubmissionRequest: { + KYCDocType: "RG" | "CNH"; + KybAttemptStatusResponse: { /** @enum {string} */ - country: components["schemas"]["AlfredpayCountry"]; - submissionId: string; - }; - /** @enum {string} */ - AlfredpayStatus: "CONSULTED" | "LINK_OPENED" | "USER_COMPLETED" | "VERIFYING" | "FAILED" | "SUCCESS" | "UPDATE_REQUIRED"; - AlfredpayStatusResponse: { - country: components["schemas"]["AlfredpayCountry"]; - /** Format: date-time */ - creationTime: string; - status: components["schemas"]["AlfredpayStatus"]; - }; - AveniaKybAttemptStatusResponse: { - failureReason?: string; + failureReason?: "face" | "name" | "birthdate" | "unknown" | "tax_id"; /** @enum {string} */ result?: "APPROVED" | "REJECTED"; - retryable?: boolean; /** @enum {string} */ status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; }; - AveniaKybDocumentRequest: { - documentType: components["schemas"]["AveniaDocumentType"]; - isDoubleSided?: boolean; - }; - AveniaKybDocumentResponse: { - document: { - documentType: components["schemas"]["AveniaDocumentType"]; - id: string; - ready: boolean; - uploadErrorBack?: string; - uploadErrorFront?: string; - uploadStatusBack?: string; - uploadStatusFront: string; - }; - }; - AveniaKybDocumentUploadResponse: { - id: string; - /** Format: uri */ - livenessUrl?: string; - /** Format: uri */ - uploadURLBack?: string; - /** Format: uri */ - uploadURLFront: string; - validateLivenessToken?: string; - }; - AveniaKybHostedResponse: { + KybLevel1Response: { attemptId: string; /** Format: uri */ authorizedRepresentativeUrl: string; /** Format: uri */ basicCompanyDataUrl: string; }; - AveniaKybLevel1Payload: { - businessActivityDescription: string; - certificateOfIncorporationDocumentId: string; - companyCity: string; - companyCountry: string; - companyLegalName: string; - companyRegistrationNumber: string; - companyState: string; - companyStreetLine1: string; - companyStreetLine2?: string; - companyStreetLine3?: string; - companyZipCode: string; - countrySubdivisionTaxResidence?: string; - countryTaxResidence: string; - /** Format: email */ - emailPixKey?: string; - /** @enum {string} */ - estimatedAnnualRevenueUsd: "less_than_100k" | "100k_to_1m" | "1m_to_10m" | "10m_to_50m" | "50m_to_100m" | "more_than_100m"; - estimatedMonthlyVolumeUsd: string; - /** @enum {string} */ - numberOfEmployees: "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001+"; - /** @enum {string} */ - reasonForAccountOpening: "charitable_donations" | "ecommerce_retail_payments" | "investment_purposes" | "other" | "payments_to_friends_or_family_abroad" | "payroll" | "personal_or_living_expenses" | "protect_wealth" | "purchase_goods_and_services" | "receive_payments_for_goods_and_services" | "tax_optimization" | "third_party_money_transmission" | "treasury_management"; - sandboxReject?: boolean; - /** Format: uri */ - socialMedia?: string; - /** @enum {string} */ - sourceOfFundsAndIncome: "business_loans" | "grants" | "inter_company_funds" | "investment_proceeds" | "legal_settlement" | "owners_capital" | "pension_retirement" | "sale_of_assets" | "sales_of_goods_and_services" | "third_party_funds" | "treasury_reserves"; - taxIdentificationDocumentId: string; - taxIdentificationNumberTin: string; - uboIds: string[]; - /** Format: uri */ - website?: string; - }; - /** @enum {string} */ - AveniaUboControlRole: "CEO" | "CFO" | "COO" | "CTO" | "President" | "Vice President" | "Director" | "Managing Director" | "Managing Partner" | "General Partner" | "Partner" | "Secretary" | "Treasurer" | "Chairman" | "Board Member" | "Authorized Signatory" | "General Counsel" | "Owner" | "Founder" | "Manager" | "Member" | "Comptroller" | "Chief Compliance Officer"; - AveniaUboPayload: { + KycLevel1Payload: { city: string; country: string; countryOfTaxId: string; - /** Format: date */ + /** @description ISO date (YYYY-MM-DD). */ dateOfBirth: string; - documentCountry: string; /** Format: email */ - email?: string; + email: string; fullName: string; - hasControl?: components["schemas"]["AveniaUboControlRole"]; - percentageOfOwnership: string; - phone?: string; state: string; - streetLine1: string; - streetLine2?: string; - streetLine3?: string; + streetAddress: string; + subAccountId: string; taxIdNumber: string; - uploadedIdentificationId: string; - uploadedSelfieId?: string; + uploadedDocumentId: string; + uploadedSelfieId: string; zipCode: string; }; - AveniaUboResponse: { + KycLevel1Response: { id: string; }; - OnboardingApiErrorResponse: { - error: string | { - code: string; - message: string; - status: number; - }; + ListApiCredentialsResponse: { + credentials: components["schemas"]["ApiCredential"][]; }; - OnboardingDocumentRequirement: { - acceptedMediaTypes?: string[]; - /** @enum {string} */ - collection?: "direct-upload" | "hosted"; - description?: string; - required: boolean; - requiredWhen?: string; - type: string; + ListManagedProfilesResponse: { + managedProfiles: components["schemas"]["ManagedProfile"][]; + pagination: components["schemas"]["ManagedProfilePagination"]; }; - OnboardingRequirementField: { - allowedValues?: string[]; + ManagedProfile: { + /** + * Format: email + * @description Normalized immutable provider contact email. It is not a Supabase login identity. + */ + contactEmail: string | null; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + creationSource: "manager" | "vortex"; + /** @enum {string} */ + customerType: "individual" | "business"; + /** Format: date-time */ + deletedAt: string | null; + /** @description Immutable manager-scoped subject identifier. */ + externalSubjectId: string; + /** Format: uuid */ + profileId: string; + /** @enum {string} */ + status: "active" | "deleted"; + /** Format: date-time */ + updatedAt: string; + }; + ManagedProfileErrorResponse: { + error: { + /** @description Machine-readable error code. Managed-profile lifecycle codes include `MANAGED_PROFILE_INVALID_INPUT`, `MANAGED_PROFILE_ACCESS_DENIED`, `MANAGED_PROFILE_NOT_FOUND`, `MANAGED_PROFILE_CONFLICT`, `MANAGED_PROFILE_MANAGER_NOT_FOUND`, and `MANAGED_PROFILE_MANAGER_INACTIVE`. Credential codes include `INVALID_CREDENTIAL_NAME`, `INVALID_CREDENTIAL_EXPIRY`, `CREDENTIAL_ACCESS_DENIED`, `CREDENTIAL_NOT_FOUND`, and `CREDENTIAL_LIMIT_REACHED`. Authentication middleware may return `AUTHENTICATION_REQUIRED`, `INVALID_SECRET_KEY`, `INVALID_API_KEY`, `INVALID_BEARER_TOKEN`, `INVALID_PUBLIC_KEY`, or `CREDENTIAL_MISMATCH`. */ + code: string; + message: string; + status: number; + }; + }; + ManagedProfilePagination: { + limit: number; + offset: number; + total: number; + }; + ManagedProfileResponse: { + managedProfile: components["schemas"]["ManagedProfile"]; + }; + ManagedSelectorErrorResponse: { + error: { + /** @description Machine-readable middleware code such as `INVALID_MANAGED_PROFILE_ID`, `MANAGED_PROFILE_CUSTOMER_TYPE_MISMATCH`, `AUTHENTICATION_REQUIRED`, `INVALID_SECRET_KEY`, `INVALID_API_KEY`, `INVALID_BEARER_TOKEN`, `CREDENTIAL_MISMATCH`, or `MANAGED_PROFILE_ACCESS_DENIED`. */ + code: string; + message: string; + status: number; + }; + }; + /** + * @description Supported blockchain networks. + * @enum {string} + */ + Networks: "assethub" | "arbitrum" | "avalanche" | "base" | "bsc" | "ethereum" | "polygon" | "moonbeam"; + OnboardingApiErrorResponse: { + error: string | { + code: string; + message: string; + status: number; + }; + }; + OnboardingDocumentRequirement: { + acceptedMediaTypes?: string[]; + /** @enum {string} */ + collection?: "direct-upload" | "hosted"; + description?: string; + required: boolean; + requiredWhen?: string; + type: string; + }; + OnboardingRequirementField: { + allowedValues?: string[]; description?: string; format?: string; path: string; @@ -2829,6 +2729,16 @@ export interface components { requirementsVersion: string; steps: components["schemas"]["OnboardingRequirementStep"][]; }; + OnboardingStatusErrorResponse: { + error: { + /** @constant */ + code: "INTERNAL_SERVER_ERROR"; + /** @constant */ + message: "Failed to read onboarding status"; + /** @constant */ + status: 500; + }; + }; OnboardingStatusResponse: { /** Format: uuid */ activeEntityId: string | null; @@ -2897,158 +2807,256 @@ export interface components { roles: string[]; selectionRequired: boolean; }; - RecordInitialKycAttemptRequest: { - quoteId?: string; - sessionId?: string; - taxId: string; + /** @enum {string} */ + OnChainToken: "USDC" | "USDT" | "ETH" | "USDC.E"; + /** @description Data related to the payment for the ramp transaction. */ + PaymentData: { + /** + * @description The amount for the payment. + * @example 0.05 + */ + amount?: string; + /** + * @description The target account for an anchor operation. + * @example GDSDQLBVDD5RZYKNDM2LAX5JDNNQOTSZOKECUYEXYMUZMAPXTMDUJCVF + */ + anchorTargetAccount?: string; + /** + * @description The memo content. + * @example 1204asjfnaksf10982e4 + */ + memo?: string; + /** + * @description Type of memo (e.g., text, id). + * @example text + */ + memoType?: string; }; - SelectActiveCustomerEntityRequest: { - /** @enum {string} */ - type: "individual" | "business"; + /** @description `PIX`, `SEPA`, `CBU` */ + PaymentMethod: string; + /** @description Represents a transaction that has been presigned. Based on UnsignedTx structure. */ + PresignedTx: { + /** @description Any additional metadata associated with the transaction. Can be an empty object. */ + meta?: { + [key: string]: unknown; + }; + /** + * Format: int64 + * @description Nonce for the transaction, if applicable. + */ + nonce?: number; + /** + * @description The phase this transaction belongs to within the ramp logic. + * @enum {string} + */ + phase?: "RampPhase" | "CleanupPhase"; + /** @description Address of the account that signed/will sign this transaction. */ + signer?: string; + /** + * @description The presigned transaction payload or relevant data. + * @example AAAAAKg... + */ + txData?: string; + } & { + [key: string]: unknown; }; - SelectActiveCustomerEntityResponse: { - activeEntityId: string; - /** @enum {string} */ - type: "individual" | "business"; + QuoteResponse: { + anchorFeeFiat: string; + anchorFeeUSD: string; + /** + * Format: date-time + * @description The timestamp when this quote expires. + */ + expiresAt?: string; + feeCurrency: components["schemas"]["RampCurrency"]; + from?: components["schemas"]["DestinationType"]; + /** + * Format: uuid + * @description Unique identifier for the quote. + */ + id?: string; + /** @description The input amount specified in the request. */ + inputAmount?: string; + inputCurrency?: components["schemas"]["RampCurrency"]; + networkFeeFiat: string; + networkFeeUSD: string; + /** @description The calculated output amount after fees and conversions. */ + outputAmount?: string; + outputCurrency?: components["schemas"]["RampCurrency"]; + partnerFeeFiat: string; + partnerFeeUSD: string; + processingFeeFiat: string; + processingFeeUSD: string; + /** @description The type of ramp process. */ + rampType?: components["schemas"]["RampDirection"]; + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + vortexFeeFiat: string; + vortexFeeUSD: string; }; - SubmitInformationResponse: { - submissionId: string; + /** + * @description Represents supported currencies for ramp operations, including fiat and on-chain tokens. + * @example USDC + * @enum {string} + */ + RampCurrency: "EUR" | "ARS" | "BRL" | "USD" | "MXN" | "COP" | "USDC" | "USDT" | "USDC.E"; + /** @enum {string} */ + RampDirection: "BUY" | "SELL"; + RampErrorLog: { + details?: string; + error: string; + phase: components["schemas"]["RampPhase"]; + recoverable?: boolean; + /** Format: date-time */ + timestamp: string; }; - SubmitKybInformationRequest: { - accountPurpose: string; - address: string; - businessActivities: string; - businessName: string; - city: string; - complianceScreeningDescription?: string; - conductsComplianceScreening?: boolean; - /** @enum {string} */ - country: "CO" | "MX"; - expectedMonthlyTransactions: number; - expectedMonthlyVolumeUsd: number; - isRegulatedBusiness: boolean; - operatesInSanctionedCountries: boolean; - relatedPersons: components["schemas"]["AlfredpayKybRelatedPerson"][]; - sourceOfFunds: string; - state: string; - taxId: string; - transmitsCustomerFunds: boolean; - walletAddresses: string; - /** Format: uri */ - website: string; - zipCode: string; - } & (unknown & unknown); - SubmitKycInformationRequest: { - address: string; - city: string; - /** @enum {string} */ - country: "AR" | "CO" | "MX"; - countryCode?: string; - cuit?: string; - /** Format: date */ - dateOfBirth: string; - dni: string; - /** Format: email */ - email?: string; - firstName: string; - lastName: string; - nationalities?: string[]; - pep?: boolean; - phoneNumber?: string; - state: string; - typeDocument?: string; - /** @enum {string} */ - typeDocumentAr?: "DNI"; - /** @enum {string} */ - typeDocumentCol?: "CC" | "CE"; - zipCode: string; - } & ({ - /** @constant */ - country?: "MX"; - } | { - /** @constant */ - country?: "CO"; - } | { - /** @constant */ - country?: "AR"; - }); - SuccessResponse: { - /** @constant */ - success: true; + RampInfoResponse: { + /** @description Sanitized eligibility keyed by corridor country code. No exact limits, PII, provider IDs, or failure reasons are returned. */ + corridors: { + [key: string]: { + canBuy: boolean; + canSell: boolean; + /** @enum {string} */ + kycStatus: "not_started" | "pending" | "approved" | "rejected"; + }; + }; }; - AlfredpayAddFiatAccountRequest: { - accountBankCode?: string; - accountName?: string; - accountNumber: string; - accountType?: string; - bankCity?: string; - bankCountry?: string; - bankPostalCode?: string; - bankState?: string; - bankStreet?: string; - beneficiaryCity?: string; - beneficiaryCountry?: string; - beneficiaryPostalCode?: string; - beneficiaryState?: string; - beneficiaryStreet?: string; - country: components["schemas"]["AlfredpayCountry"]; - documentNumber?: string; - documentType?: string; - isExternal?: boolean; - routingNumber?: string; - type: components["schemas"]["AlfredpayFiatAccountType"]; + /** + * @description The current phase of the ramp process. + * @enum {string} + */ + RampPhase: "initial" | "timedOut" | "stellarCreateAccount" | "squidrouterApprove" | "squidrouterSwap" | "fundEphemeral" | "nablaApprove" | "nablaSwap" | "moonbeamToPendulum" | "moonbeamToPendulumXcm" | "pendulumToMoonbeam" | "assethubToPendulum" | "pendulumToAssethub" | "spacewalkRedeem" | "stellarPayment" | "subsidizePreSwap" | "subsidizePostSwap" | "brlaTeleport" | "onHoldForComplianceCheck" | "brlaPayoutOnMoonbeam" | "failed"; + RampProcess: { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; }; - AlfredpayCountryAndCustomerTypeRequest: { - country: components["schemas"]["AlfredpayCountry"]; - type?: components["schemas"]["AlfredpayCustomerType"]; + RecordInitialKycAttemptRequest: { + quoteId?: string; + sessionId?: string; + taxId: string; }; - AlfredpayCountryRequest: { - country: components["schemas"]["AlfredpayCountry"]; + RegisterRampRequest: { + /** + * @description Optional additional data for the ramp process. + * + * For Brazil onramps, destinationAddress is required. + * + * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. + */ + additionalData?: { + /** @description Destination address, used for onramp. */ + destinationAddress?: string; + /** @description Auth token obtained from Monerium's API, for the current user. Only required for Monerium-related ramps. */ + moneriumAuthToken: string; + paymentData?: components["schemas"]["PaymentData"]; + /** @description PIX key for the destination account in an onramp. */ + pixDestination?: string; + /** @description Tax ID of the receiver for onramp. */ + receiverTaxId?: string; + /** @description Tax ID of the user. */ + taxId?: string; + /** @description Wallet address initiating the offramp. */ + walletAddress: string; + } & { + [key: string]: unknown; + }; + /** + * Format: uuid + * @description The unique identifier for the quote. + */ + quoteId: string; + /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ + signingAccounts: { + /** @description The account address. */ + address: string; + /** + * @description The type of the account. + * @enum {string} + */ + type: "EVM" | "Substrate"; + }[]; }; - AlfredpayCreateFiatAccountResponse: { - fiatAccountId: string; + SelectActiveCustomerEntityRequest: { + /** @enum {string} */ + type: "individual" | "business"; }; - AlfredpayErrorResponse: { - error: string; - fields?: { - field: string; - message: string; - }[]; + SelectActiveCustomerEntityResponse: { + activeEntityId: string; + /** @enum {string} */ + type: "individual" | "business"; }; - AlfredpayFiatAccount: { - accountName?: string; - accountNumber: string; - accountType: string; - /** Format: date-time */ - createdAt?: string; - customerId: string; - fiatAccountId: string; - routingNumber?: string; - type: components["schemas"]["AlfredpayFiatAccountType"]; - } & { - [key: string]: unknown; + /** @description `PENDING`, `FAILED`, `COMPLETED` */ + SimpleStatus: string; + StartKYC2Request: { + documentType: components["schemas"]["KYCDocType"]; + taxId: string; }; - /** @enum {string} */ - AlfredpayFiatAccountType: "ACH" | "ACH_BOL" | "ACH_CHL" | "ACH_DOM" | "B89" | "BANK_CN" | "BANK_USA" | "COELSA" | "PIX" | "SPEI"; - AlfredpayKybBusinessSummary: { - relatedPersons: { - idRelatedPerson: string; - }[]; - submissionId: string; + StartKYC2Response: { + uploadUrls?: components["schemas"]["KYCDataUploadFileFiles"]; }; - AlfredpayManagedBadRequestResponse: components["schemas"]["AlfredpayErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; - AlfredpayRelatedPersonFileUploadRequest: { - country: components["schemas"]["AlfredpayCountry"]; - /** Format: binary */ - file: string; - /** @enum {string} */ - fileType: "docFront" | "docBack"; - relatedPersonId: string; + StartRampRequest: { + rampId: string; }; - AlfredpaySubmissionResponse: { + SubmitInformationResponse: { submissionId: string; }; - AlfredpaySubmitKybInformationRequest: { + SubmitKybInformationRequest: { accountPurpose: string; address: string; businessActivities: string; @@ -3056,7 +3064,8 @@ export interface components { city: string; complianceScreeningDescription?: string; conductsComplianceScreening?: boolean; - country: components["schemas"]["AlfredpayCountry"]; + /** @enum {string} */ + country: "CO" | "MX"; expectedMonthlyTransactions: number; expectedMonthlyVolumeUsd: number; isRegulatedBusiness: boolean; @@ -3067,13 +3076,15 @@ export interface components { taxId: string; transmitsCustomerFunds: boolean; walletAddresses: string; + /** Format: uri */ website: string; zipCode: string; } & (unknown & unknown); - AlfredpaySubmitKycInformationRequest: { + SubmitKycInformationRequest: { address: string; city: string; - country: components["schemas"]["AlfredpayCountry"]; + /** @enum {string} */ + country: "AR" | "CO" | "MX"; countryCode?: string; cuit?: string; /** Format: date */ @@ -3093,112 +3104,101 @@ export interface components { /** @enum {string} */ typeDocumentCol?: "CC" | "CE"; zipCode: string; - } & unknown; - AlfredpaySuccessResponse: { + } & ({ + /** @constant */ + country?: "MX"; + } | { + /** @constant */ + country?: "CO"; + } | { + /** @constant */ + country?: "AR"; + }); + SuccessResponse: { /** @constant */ success: true; }; - AlfredpayValidationBadRequestResponse: components["schemas"]["AlfredpayErrorResponse"] | components["schemas"]["ApiValidationErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; - ApiCredentialManagedSelectorErrorResponse: components["schemas"]["ApiCredentialErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; - ApiValidationErrorResponse: { - code: number; - errors?: { - message: string; - }[]; - message: string; - }; - BrlaManagedBadRequestResponse: components["schemas"]["BrlaErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; - CreateManagedProfileRequest: { + /** @enum {string} */ + TaxIdType: "CPF" | "CNPJ"; + TriggerOfframpRequest: { /** - * Format: email - * @description Provider contact email. It is normalized to lowercase and remains permanently reserved within the manager, including after deletion. + * @description The amount to offramp. + * @example 100.50 */ - contactEmail: string; - /** @enum {string} */ - customerType: "individual" | "business"; - /** @description Immutable idempotency key for this subject within the authenticated manager. */ - externalSubjectId: string; + amount: string; + /** @description The recipient's PIX key. */ + pixKey: string; + /** @description The recipient's Tax ID for validation. */ + receiverTaxId: string; + /** @description The sender's Tax ID. */ + taxId: string; }; - ErrorManagedSelectorResponse: components["schemas"]["ErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; - FlatErrorResponse: { - error: string; + TriggerOfframpResponse: { + /** @description The ID of the triggered offramp transaction. */ + offrampId?: string; }; - FlatManagedSelectorErrorResponse: components["schemas"]["FlatErrorResponse"] | components["schemas"]["ManagedSelectorErrorResponse"]; - KybAttemptStatusResponse: { - /** @enum {string} */ - failureReason?: "face" | "name" | "birthdate" | "unknown" | "tax_id"; - /** @enum {string} */ - result?: "APPROVED" | "REJECTED"; + /** @description Represents an unsigned transaction that requires user signature. Actual properties will depend on the transaction type and network. */ + UnsignedTx: { + meta?: Record; + nonce?: number; /** @enum {string} */ - status: "PENDING" | "PROCESSING" | "COMPLETED" | "EXPIRED"; - }; - KybLevel1Response: { - attemptId: string; - /** Format: uri */ - authorizedRepresentativeUrl: string; - /** Format: uri */ - basicCompanyDataUrl: string; - }; - ListManagedProfilesResponse: { - managedProfiles: components["schemas"]["ManagedProfile"][]; - pagination: components["schemas"]["ManagedProfilePagination"]; - }; - ManagedProfile: { + phase?: "RampPhase" | "CleanupPhase"; + signer?: string; /** - * Format: email - * @description Normalized immutable provider contact email. It is not a Supabase login identity. + * @description The unsigned transaction payload or relevant data. + * @example AAAAAKu... */ - contactEmail: string | null; - /** Format: date-time */ - createdAt: string; - /** @enum {string} */ - creationSource: "manager" | "vortex"; - /** @enum {string} */ - customerType: "individual" | "business"; - /** Format: date-time */ - deletedAt: string | null; - /** @description Immutable manager-scoped subject identifier. */ - externalSubjectId: string; - /** Format: uuid */ - profileId: string; - /** @enum {string} */ - status: "active" | "deleted"; - /** Format: date-time */ - updatedAt: string; - }; - ManagedProfileErrorResponse: { - error: { - /** @description Machine-readable error code. Managed-profile lifecycle codes include `MANAGED_PROFILE_INVALID_INPUT`, `MANAGED_PROFILE_ACCESS_DENIED`, `MANAGED_PROFILE_NOT_FOUND`, `MANAGED_PROFILE_CONFLICT`, `MANAGED_PROFILE_MANAGER_NOT_FOUND`, and `MANAGED_PROFILE_MANAGER_INACTIVE`. Credential codes include `INVALID_CREDENTIAL_NAME`, `INVALID_CREDENTIAL_EXPIRY`, `CREDENTIAL_ACCESS_DENIED`, `CREDENTIAL_NOT_FOUND`, and `CREDENTIAL_LIMIT_REACHED`. Authentication middleware may return `AUTHENTICATION_REQUIRED`, `INVALID_SECRET_KEY`, `INVALID_API_KEY`, `INVALID_BEARER_TOKEN`, `INVALID_PUBLIC_KEY`, or `CREDENTIAL_MISMATCH`. */ - code: string; - message: string; - status: number; - }; + txData?: string; + } & { + [key: string]: unknown; }; - ManagedProfilePagination: { - limit: number; - offset: number; - total: number; + UpdateRampRequest: { + /** @description Optional additional data, like transaction hashes from external services. */ + additionalData?: ({ + /** @description Transaction hash for AssetHub to Pendulum transfer, if applicable. */ + assetHubToPendulumHash?: string | null; + /** @description Signed message to trigger a Monerium offramp. */ + moneriumOfframpSignature: string; + /** @description Transaction hash for Squid Router approval. Optional: omit when the wallet already holds a sufficient allowance and no approval transaction was submitted. */ + squidRouterApproveHash?: string | null; + /** @description Transaction hash for Squid Router swap, if applicable. */ + squidRouterSwapHash?: string | null; + } & { + [key: string]: unknown; + }) | null; + /** @description An array of transactions that have been pre-signed by the user. */ + presignedTxs: components["schemas"]["PresignedTx"][]; + /** + * @description The unique identifier of the ramp process to start. + * @example proc_12345 + */ + rampId: string; }; - ManagedProfileResponse: { - managedProfile: components["schemas"]["ManagedProfile"]; + UserLimit: { + /** @enum {string} */ + corridor: "AR" | "BR" | "CO" | "MX" | "US"; + currency: components["schemas"]["RampCurrency"]; + direction: components["schemas"]["RampDirection"]; + /** @description Maximum amount in the returned currency's human units. */ + max: string; + period: components["schemas"]["UserLimitPeriod"]; + /** @description Amount consumed during the period in the returned currency's human units. */ + used: string; }; - ManagedSelectorErrorResponse: { - error: { - /** @description Machine-readable middleware code such as `INVALID_MANAGED_PROFILE_ID`, `MANAGED_PROFILE_CUSTOMER_TYPE_MISMATCH`, `AUTHENTICATION_REQUIRED`, `INVALID_SECRET_KEY`, `INVALID_API_KEY`, `INVALID_BEARER_TOKEN`, `CREDENTIAL_MISMATCH`, or `MANAGED_PROFILE_ACCESS_DENIED`. */ - code: string; - message: string; - status: number; - }; + UserLimitPeriod: { + /** + * Format: date-time + * @description Exclusive end of the reported period. + */ + endsAt: string; + /** Format: date-time */ + startsAt: string; + /** @constant */ + type: "calendar_month"; }; - OnboardingStatusErrorResponse: { - error: { - /** @constant */ - code: "INTERNAL_SERVER_ERROR"; - /** @constant */ - message: "Failed to read onboarding status"; - /** @constant */ - status: 500; - }; + ValidatePixKeyResponse: { + /** @description Indicates if the PIX key is valid. */ + valid?: boolean; }; }; responses: { @@ -3262,320 +3262,365 @@ export interface components { } export type $defs = Record; export interface operations { - listApiCredentials: { + getAlfredpayStatus: { parameters: { - query?: never; - header?: never; + query: { + country: components["schemas"]["AlfredpayCountry"]; + /** @description Selects the individual or business customer. When omitted, the active customer entity is used for backward compatibility. */ + type?: components["schemas"]["AlfredpayCustomerType"]; + }; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Credentials, newest first, including revoked and expired lifecycle records. */ + /** @description Customer status returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ListApiCredentialsResponse"]; + "application/json": components["schemas"]["AlfredpayStatusResponse"]; }; }; - /** @description Missing or invalid Bearer token. */ - 401: { + /** @description Invalid or missing country, invalid selector UUID, or managed-profile customer-type mismatch. */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Internal server error. */ + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Status refresh failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - createApiCredential: { + createAlfredpayBusinessCustomer: { parameters: { query?: never; - header?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - "application/json": components["schemas"]["CreateApiCredentialRequest"]; + "application/json": components["schemas"]["AlfredpayCreateCustomerRequest"]; }; }; responses: { - /** @description Credential created. Persist `secretKey` immediately; it cannot be retrieved again. */ - 201: { + /** @description Business customer created. */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["CreateApiCredentialResponse"]; + "application/json": components["schemas"]["AlfredpayCreateCustomerResponse"]; }; }; - /** @description `INVALID_CREDENTIAL_EXPIRY` or `INVALID_CREDENTIAL_NAME`. */ + /** @description Invalid country, unavailable email, existing customer, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Missing or invalid Bearer token. */ - 401: { + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description An upstream customer exists with a conflicting country or type. */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description `CREDENTIAL_LIMIT_REACHED`: the profile already holds five active non-expired credentials. */ - 409: { + /** @description Customer creation failed. */ + 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal server error. */ - 500: { + /** @description Alfredpay returned an invalid existing-customer response. */ + 502: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - revokeApiCredential: { + createAlfredpayIndividualCustomer: { parameters: { query?: never; - header?: never; - path: { - /** @description Immutable credential ID to revoke. */ - credentialId: string; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpayCreateCustomerRequest"]; + }; + }; responses: { - /** @description Credential revoked; both values are immediately unusable. */ - 204: { + /** @description Customer created. */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["AlfredpayCreateCustomerResponse"]; + }; }; - /** @description Missing or invalid Bearer token. */ - 401: { + /** @description Invalid country, unavailable email, existing customer, invalid selector UUID, or managed-profile customer-type mismatch. */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description `CREDENTIAL_NOT_FOUND`: credential is missing, already revoked, partner-managed, or not owned by the profile. */ - 404: { + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description An upstream customer exists with a conflicting country or type. */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal server error. */ + /** @description Customer creation failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Alfredpay returned an invalid existing-customer response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - requestOTP: { + listAlfredpayFiatAccounts: { parameters: { - query?: never; - header?: never; + query: { + country: components["schemas"]["AlfredpayCountry"]; + }; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; path?: never; cookie?: never; }; - requestBody: { - content: { - /** - * @example { - * "email": "user@example.com" - * } - */ - "application/json": { - /** Format: email */ - email: string; - /** @description Optional locale for the email, e.g. `pt-BR`. */ - locale?: string; - }; - }; - }; + requestBody?: never; responses: { - /** @description OTP sent. */ + /** @description Fiat accounts returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - message: string; - success: boolean; - }; + "application/json": components["schemas"]["AlfredpayFiatAccount"][]; }; }; - /** @description Email missing or locale not a string. */ + /** @description Invalid country, credential without an effective user, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": { - error: string; - }; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Failed to send the OTP email. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Account lookup failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": { - error: string; - }; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - verifyOTP: { + createAlfredpayFiatAccount: { parameters: { query?: never; - header?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; path?: never; cookie?: never; }; requestBody: { content: { - /** - * @example { - * "email": "user@example.com", - * "token": "123456" - * } - */ - "application/json": { - /** Format: email */ - email: string; - /** @description The 6-digit code from the email. */ - token: string; - }; + "application/json": components["schemas"]["AlfredpayAddFiatAccountRequest"]; }; }; responses: { - /** @description Session created. */ + /** @description Fiat account created. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": { - access_token: string; - refresh_token: string; - success: boolean; - /** Format: uuid */ - user_id: string; - }; + "application/json": components["schemas"]["AlfredpayCreateFiatAccountResponse"]; + }; + }; + /** @description Invalid country, account details, credential without an effective user, invalid selector UUID, or managed-profile customer-type mismatch. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + }; + }; + 401: components["responses"]["ManagedSelectorUnauthorized"]; + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Missing fields, or the OTP is invalid or expired. */ - 400: { + /** @description Account creation failed. */ + 500: { headers: { [name: string]: unknown; }; content: { - "application/json": { - error: string; - }; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - createSubaccount: { + deleteAlfredpayFiatAccount: { parameters: { - query?: never; + query: { + country: components["schemas"]["AlfredpayCountry"]; + }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; - path?: never; - cookie?: never; - }; - requestBody?: { - content: { - "application/json": components["schemas"]["CreateSubaccountRequest"]; + path: { + fiatAccountId: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description Subaccount created or KYC retry initiated successfully. */ - 200: { + /** @description Fiat account deleted. */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["CreateSubaccountResponse"]; - }; + content?: never; }; - /** - * @description Bad Request. Possible reasons: - * - Missing required fields (cpf, cnpj, companyName, startDate) - * - Subaccount already created and KYC level > 0 - * - Other invalid request details - */ + /** @description Invalid country, provider rejection, credential without an effective user, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Internal Server Error. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Account deletion failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - fetchSubaccountKycStatus: { + findAlfredpayKybCustomerAndBusiness: { parameters: { query: { - /** @description The user's Tax ID. */ - taxId: string; + country: "CO" | "MX"; }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ @@ -3586,51 +3631,51 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Successfully retrieved KYC status. */ + /** @description KYB submission and related-person IDs returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetKycStatusResponse"]; + "application/json": components["schemas"]["AlfredpayKybDetailsResponse"]; }; }; - /** @description Missing taxId or subaccount not found (returned as 400 from code). */ + /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description No KYC process started. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay business customer not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal Server Error (e.g., no KYC events found when expected). */ + /** @description Lookup failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - brlaGetSelfieLivenessUrl: { + getAlfredpayKybRedirectLink: { parameters: { query: { - /** @description CPF or CNPJ. */ - taxId: string; + country: components["schemas"]["AlfredpayCountry"]; }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ @@ -3641,40 +3686,52 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Liveness URL returned. */ + /** @description KYB redirect link returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaGetSelfieLivenessUrlResponse"]; + "application/json": components["schemas"]["AlfredpayRedirectLinkResponse"]; }; }; - /** @description Missing taxId or ramp disabled. */ + /** @description Invalid country, KYB already verifying or complete, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Internal server error. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay business customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Redirect-link creation failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - brlaGetUploadUrls: { + getAlfredpayKycRedirectLink: { parameters: { - query?: never; + query: { + country: components["schemas"]["AlfredpayCountry"]; + }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; @@ -3682,48 +3739,54 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AveniaKYCDataUploadRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Upload URLs returned. */ + /** @description KYC redirect link returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaKYCDataUploadResponse"]; + "application/json": components["schemas"]["AlfredpayRedirectLinkResponse"]; }; }; - /** @description Missing/invalid documentType or taxId; or ramp disabled for this tax ID. */ + /** @description Invalid country, KYC already verifying or complete, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Internal server error. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Redirect-link creation failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - getBrlaUser: { + getAlfredpayKycStatus: { parameters: { - query?: { - /** @description The user's Tax ID. */ - taxId?: string; + query: { + country: components["schemas"]["AlfredpayCountry"]; + type?: components["schemas"]["AlfredpayCustomerType"]; }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ @@ -3734,58 +3797,50 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Successfully retrieved user information. */ + /** @description Verification status returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetUserResponse"]; + "application/json": components["schemas"]["AlfredpayKycStatusResponse"]; }; }; - /** - * @description Bad Request. Possible reasons: - * - Missing taxId query parameter - * - KYC invalid - */ + /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Subaccount not found. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Customer or verification attempt not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal Server Error. */ + /** @description Status lookup failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - getBrlaUserRemainingLimit: { + notifyAlfredpayKycRedirectFinished: { parameters: { - query: { - /** @description The user's Tax ID. */ - taxId?: string; - /** @description Ramp direction whose remaining limit should be returned. */ - direction: "BUY" | "SELL"; - }; + query?: never; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; @@ -3793,49 +3848,54 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpayRedirectNotificationRequest"]; + }; + }; responses: { - /** @description Successfully retrieved user's remaining limits. */ + /** @description Redirect state recorded. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetUserRemainingLimitResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Missing taxId query parameter or other invalid request. */ + /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Subaccount not found or limits not found. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal Server Error. */ + /** @description State update failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - brlaNewKyc: { + notifyAlfredpayKycRedirectOpened: { parameters: { query?: never; header?: { @@ -3847,83 +3907,109 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["KycLevel1Payload"]; + "application/json": components["schemas"]["AlfredpayRedirectNotificationRequest"]; }; }; responses: { - /** @description KYC submission accepted. */ + /** @description Redirect state recorded. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["KycLevel1Response"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Validation failure. */ + /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Internal server error. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description State update failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - brlaValidatePixKey: { + retryAlfredpayKyc: { parameters: { - query: { - /** @description Pix key to validate (CPF, CNPJ, email, phone, or random key). */ - pixKey: string; + query?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AlfredpayRetryRequest"]; }; - header?: never; - path?: never; - cookie?: never; }; - requestBody?: never; responses: { - /** @description Validation result. */ + /** @description Retry initialized. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaValidatePixKeyResponse"]; + "application/json": components["schemas"]["AlfredpayRetryResponse"]; }; }; - /** @description Missing or invalid pix key. */ + /** @description No failed submission is available, the selector UUID is invalid, or the managed-profile customer type mismatches. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Internal server error. */ + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Retry failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - getUserLimits: { + sendAlfredpayKybSubmission: { parameters: { query?: never; header?: { @@ -3935,56 +4021,53 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["GetUserLimitsRequest"]; + "application/json": components["schemas"]["AlfredpaySendSubmissionRequest"]; }; }; responses: { - /** @description Limits and consumed amounts for both directions of every requested corridor. */ + /** @description KYB submission sent. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetUserLimitsResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Invalid corridor list or no completed provider profile for a requested corridor. */ + /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FlatManagedSelectorErrorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Missing or invalid credentials. */ - 401: { + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay business customer not found. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description The credential is not linked to a user. */ - 403: { + /** @description Submission finalization failed. */ + 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FlatManagedSelectorErrorResponse"]; - }; - }; - /** @description Provider limits are unavailable or invalid. */ - 502: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; - content?: never; }; }; }; - createQuote: { + sendAlfredpayKycSubmission: { parameters: { query?: never; header?: { @@ -3994,146 +4077,55 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - /** - * @example { - * "from": "pix", - * "inputAmount": "33", - * "inputCurrency": "BRL", - * "outputCurrency": "USDC", - * "partnerId": "myPartnerId", - * "rampType": "BUY", - * "to": "polygon" - * } - */ - "application/json": { - /** @description Your api key, if available. */ - apiKey?: string; - countryCode?: components["schemas"]["CountryCode"]; - /** @description From destination */ - from: components["schemas"]["DestinationType"]; - /** - * @description The amount of currency to be input. - * @example 100.00 - */ - inputAmount: string; - /** @description The currency type for the input amount. */ - inputCurrency: components["schemas"]["RampCurrency"]; - network?: components["schemas"]["Networks"]; - /** @description The desired currency type for the output amount. */ - outputCurrency: components["schemas"]["RampCurrency"]; - /** @description Your partner ID, if available. */ - partnerId?: string; - paymentMethod?: components["schemas"]["PaymentMethod"]; - /** @description The type of ramp process (on-ramp or off-ramp). */ - rampType: components["schemas"]["RampDirection"]; - /** @description To destination */ - to: components["schemas"]["DestinationType"]; - }; + "application/json": components["schemas"]["AlfredpaySendSubmissionRequest"]; }; }; responses: { - /** @description Quote successfully created. */ - 201: { + /** @description KYC submission sent. */ + 200: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "expiresAt": "2025-05-16T12:30:00Z", - * "fee": "0.50", - * "from": "polygon", - * "id": "quote_7af7171e-aa42-49a2-80c2-9e18483bad38", - * "inputAmount": "33", - * "inputCurrency": "usdc", - * "outputAmount": "32500.50", - * "outputCurrency": "ars", - * "rampType": "sell", - * "to": "cbu" - * } - */ - "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - /** - * Format: date-time - * @description The timestamp when this quote expires. - */ - expiresAt?: string; - feeCurrency: components["schemas"]["RampCurrency"]; - from?: components["schemas"]["DestinationType"]; - /** - * Format: uuid - * @description Unique identifier for the quote. - */ - id?: string; - /** @description The input amount specified in the request. */ - inputAmount?: string; - inputCurrency?: components["schemas"]["RampCurrency"]; - networkFeeFiat: string; - networkFeeUSD: string; - /** @description The calculated output amount after fees and conversions. */ - outputAmount?: string; - outputCurrency?: components["schemas"]["RampCurrency"]; - partnerFeeFiat: string; - partnerFeeUSD: string; - processingFeeFiat: string; - processingFeeUSD: string; - /** @description The type of ramp process. */ - rampType?: components["schemas"]["RampDirection"]; - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - }; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** - * @description Bad Request. Possible reasons: - * - Missing required fields (rampType, from, to, inputAmount, inputCurrency, outputCurrency) - * - Invalid ramp type (must be "BUY" or "SELL") - */ + /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; - }; - /** @description Invalid authentication or managed-profile selection. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Partner authorization or managed-profile authorization failed. */ - 403: { + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal Server Error. Low-liquidity route failures use this status with a safe user-facing message; unexpected internal failures remain masked. */ + /** @description Submission finalization failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - createBestQuote: { + submitAlfredpayKybFile: { parameters: { query?: never; header?: { @@ -4143,108 +4135,55 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - /** - * @example { - * "from": "pix", - * "inputAmount": "30", - * "inputCurrency": "BRL", - * "outputCurrency": "USDC", - * "partnerId": "myPartnerId", - * "rampType": "BUY" - * } - */ - "application/json": components["schemas"]["CreateBestQuoteRequest"]; + "multipart/form-data": components["schemas"]["AlfredpayKybFileUploadRequest"]; }; }; responses: { - /** @description Quote successfully created. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - /** - * Format: date-time - * @description The timestamp when this quote expires. - */ - expiresAt?: string; - feeCurrency: components["schemas"]["RampCurrency"]; - from?: components["schemas"]["DestinationType"]; - /** - * Format: uuid - * @description Unique identifier for the quote. - */ - id?: string; - /** @description The input amount specified in the request. */ - inputAmount?: string; - inputCurrency?: components["schemas"]["RampCurrency"]; - networkFeeFiat: string; - networkFeeUSD: string; - /** @description The calculated output amount after fees and conversions. */ - outputAmount?: string; - outputCurrency?: components["schemas"]["RampCurrency"]; - partnerFeeFiat: string; - partnerFeeUSD: string; - processingFeeFiat: string; - processingFeeUSD: string; - /** @description The type of ramp process. */ - rampType?: components["schemas"]["RampDirection"]; - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - }; - }; - }; - /** - * @description Bad Request. Possible reasons: - * - Missing required fields (rampType, from, to, inputAmount, inputCurrency, outputCurrency) - * - Invalid ramp type (must be "BUY" or "SELL") - */ - 400: { + /** @description File uploaded. */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Invalid authentication or managed-profile selection. */ - 401: { + /** @description Invalid country, missing file, invalid selector UUID, or managed-profile customer-type mismatch. */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; - /** @description Partner authorization or managed-profile authorization failed. */ - 403: { + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay business customer not found. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal Server Error. Low-liquidity route failures use this status with a safe user-facing message when every eligible route cannot serve the requested amount; unexpected internal failures remain masked. */ + /** @description Upload failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - getRampInfo: { + submitAlfredpayKybInformation: { parameters: { query?: never; header?: { @@ -4254,107 +4193,122 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["SubmitKybInformationRequest"]; + }; + }; responses: { - /** @description Sanitized corridor eligibility. */ + /** @description KYB information accepted. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RampInfoResponse"]; + "application/json": components["schemas"]["SubmitInformationResponse"]; }; }; - /** @description Malformed key or wrong key type. */ + /** @description Invalid country, company data, questionnaire, selector UUID, or managed-profile customer type. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialManagedSelectorErrorResponse"]; + "application/json": components["schemas"]["AlfredpayValidationBadRequestResponse"]; }; }; - /** @description Missing, invalid, expired, or revoked API credential. */ - 401: { + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay business customer not found. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialManagedSelectorErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description `CREDENTIAL_MISMATCH`: presented public and secret values belong to different credentials. */ - 403: { + /** @description KYB is already in review or complete. */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ApiCredentialManagedSelectorErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Submission failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - getRampErrorLogs: { + submitAlfredpayKybRelatedPersonFile: { parameters: { query?: never; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; - path: { - /** @description Ramp ID. */ - id: string; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["AlfredpayKybRelatedPersonFileUploadRequest"]; + }; + }; responses: { - /** @description Error log array (empty if no errors). */ + /** @description File uploaded. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["GetRampErrorLogsResponse"]; + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description The managed-profile selector is invalid. */ + /** @description Invalid country, missing file, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; /** @description Authentication required. */ - 401: { + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay business customer not found. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Ramp does not belong to authenticated principal. */ - 403: { + /** @description Upload failed. */ + 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; - }; - /** @description Ramp not found. */ - 404: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; - content?: never; }; }; }; - registerRamp: { + submitAlfredpayKycFile: { parameters: { query?: never; header?: { @@ -4364,200 +4318,55 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - /** - * @example { - * "additionalData": { - * "pixDestination": "711.711.011-11", - * "receiverTaxId": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", - * "taxId": "711.711.011-11" - * }, - * "quoteId": "8e4bca04-aa22-4f86-9ce5-80aaef58ef83", - * "signingAccounts": [ - * { - * "address": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", - * "network": "moonbeam" - * }, - * { - * "address": "6ftBYTotU4mmCuvUqJvk6qEP7uCzzz771pTMoxcbHFb9rcPv", - * "network": "pendulum" - * } - * ] - * } - */ - "application/json": { - /** - * @description Optional additional data for the ramp process. - * - * For Brazil onramps, destinationAddress is required. - * - * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. - */ - additionalData?: { - /** @description Destination address, used for onramp. */ - destinationAddress?: string; - /** @description Auth token obtained from Monerium's API, for the current user. Only required for Monerium-related ramps. */ - moneriumAuthToken: string; - paymentData?: components["schemas"]["PaymentData"]; - /** @description PIX key for the destination account in an onramp. */ - pixDestination?: string; - /** @description Tax ID of the receiver for onramp. */ - receiverTaxId?: string; - sessionId?: string; - /** @description Tax ID of the user. */ - taxId?: string; - /** @description Wallet address initiating the offramp. */ - walletAddress: string; - } & { - [key: string]: unknown; - }; - /** - * Format: uuid - * @description The unique identifier for the quote. - */ - quoteId: string; - /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ - signingAccounts: { - /** @description The account address. */ - address: string; - /** - * @description The type of the account. - * @enum {string} - */ - type: "EVM" | "Substrate"; - }[]; - }; + "multipart/form-data": components["schemas"]["AlfredpayKycFileUploadRequest"]; }; }; responses: { - /** @description Ramp process successfully registered. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "brCode": "00020126...", - * "createdAt": "2024-05-16T10:00:00Z", - * "currentPhase": "pending_signature", - * "from": "stellar", - * "id": "proc_12345", - * "quoteId": "41a756dc-04e4-4e4b-b243-9c8f977c24d6", - * "to": "pix", - * "type": "off", - * "unsignedTxs": [ - * { - * "data": "AAAA...", - * "type": "stellar_payment" - * } - * ], - * "updatedAt": "2024-05-16T10:00:00Z" - * } - */ - "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - countryCode?: components["schemas"]["CountryCode"]; - /** - * Format: date-time - * @description Timestamp of when the ramp process was created. - */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; - inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; - }; + /** @description File uploaded. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuccessResponse"]; }; }; - /** @description Bad Request - Invalid input, missing required fields, or validation error. */ + /** @description Invalid country, missing file, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "message": "Missing required fields" - * } - */ - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; }; }; + /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Quote ownership or managed-profile authorization failed. */ - 403: { + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; - /** @description Internal Server Error. */ + /** @description Upload failed. */ 500: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "message": "An unexpected error occurred." - * } - */ - "application/json": components["schemas"]["ErrorResponse"]; + "application/json": components["schemas"]["AlfredpayErrorResponse"]; }; }; }; }; - startRamp: { + submitAlfredpayKycInformation: { parameters: { query?: never; header?: { @@ -4567,365 +4376,312 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { - /** - * @example { - * "rampId": "proc_12345" - * } - */ - "application/json": components["schemas"]["StartRampRequest"]; + "application/json": components["schemas"]["SubmitKycInformationRequest"]; }; }; responses: { - /** @description Ramp process successfully started or updated. */ + /** @description KYC information accepted. */ 200: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "createdAt": "2024-05-16T10:00:00Z", - * "currentPhase": "processing", - * "depositQrCode": "00020126...", - * "from": "stellar", - * "id": "proc_12345", - * "quoteId": "quote_7af7171e-aa42-49a2-80c2-9e18483bad38", - * "to": "pix", - * "type": "sell", - * "unsignedTxs": [], - * "updatedAt": "2024-05-16T12:30:00Z" - * } - */ - "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - countryCode?: components["schemas"]["CountryCode"]; - /** - * Format: date-time - * @description Timestamp of when the ramp process was created. - */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; - inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; - }; + "application/json": components["schemas"]["SubmitInformationResponse"]; + }; + }; + /** @description Invalid country, KYC fields, selector UUID, or managed-profile customer type. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayValidationBadRequestResponse"]; + }; + }; + /** @description Authentication required. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Managed profile or corridor is not authorized. */ + 403: components["responses"]["ManagedSelectorForbidden"]; + /** @description Alfredpay customer not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + /** @description Submission failed. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlfredpayErrorResponse"]; + }; + }; + }; + }; + listApiCredentials: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Credentials, newest first, including revoked and expired lifecycle records. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListApiCredentialsResponse"]; + }; + }; + /** @description Missing or invalid Bearer token. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + }; + }; + createApiCredential: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["CreateApiCredentialRequest"]; + }; + }; + responses: { + /** @description Credential created. Persist `secretKey` immediately; it cannot be retrieved again. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateApiCredentialResponse"]; + }; + }; + /** @description `INVALID_CREDENTIAL_EXPIRY` or `INVALID_CREDENTIAL_NAME`. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description Missing or invalid Bearer token. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + /** @description `CREDENTIAL_LIMIT_REACHED`: the profile already holds five active non-expired credentials. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; }; }; - /** - * @description Bad Request. Possible reasons: - * - Missing required fields (rampId, presignedTxs) - * - Invalid additional data format (if provided, must be an object) - */ - 400: { + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; + }; + }; + }; + }; + revokeApiCredential: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Immutable credential ID to revoke. */ + credentialId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Credential revoked; both values are immediately unusable. */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; + content?: never; }; - /** @description Authentication is required for an owned ramp or managed-profile selection. */ + /** @description Missing or invalid Bearer token. */ 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; }; }; - /** @description Ramp ownership or managed-profile authorization failed. */ - 403: { + /** @description `CREDENTIAL_NOT_FOUND`: credential is missing, already revoked, partner-managed, or not owned by the profile. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; }; }; - /** @description Internal Server Error. */ + /** @description Internal server error. */ 500: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "message": "An unexpected error occurred." - * } - */ - "application/json": Record; + "application/json": components["schemas"]["ApiCredentialErrorResponse"]; }; }; }; }; - updateRamp: { + requestOTP: { parameters: { query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; + header?: never; path?: never; cookie?: never; }; - requestBody?: { + requestBody: { content: { /** * @example { - * "additionalData": { - * "squidRouterApproveHash": "0x123...", - * "squidRouterSwapHash": "0x456..." - * }, - * "presignedTxs": [ - * { - * "meta": {}, - * "nonce": 1, - * "phase": "RampPhase", - * "signer": "GB2TP24WCY6BPGFX4SOGDHT7IGJRR7HCDQT2VL2MVCZJTJCGKMVGQGQB", - * "txData": "AAAAAKu..." - * } - * ], - * "rampId": "proc_12345" + * "email": "user@example.com" * } */ - "application/json": components["schemas"]["UpdateRampRequest"]; + "application/json": { + /** Format: email */ + email: string; + /** @description Optional locale for the email, e.g. `pt-BR`. */ + locale?: string; + }; }; }; responses: { - /** @description Ramp process successfully started or updated. */ + /** @description OTP sent. */ 200: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "createdAt": "2024-05-16T10:00:00Z", - * "currentPhase": "processing", - * "depositQrCode": "00020126...", - * "from": "stellar", - * "id": "proc_12345", - * "quoteId": "quote_7af7171e-aa42-49a2-80c2-9e18483bad38", - * "to": "pix", - * "type": "off", - * "unsignedTxs": [], - * "updatedAt": "2024-05-16T12:30:00Z" - * } - */ "application/json": { - anchorFeeFiat: string; - anchorFeeUSD: string; - countryCode?: components["schemas"]["CountryCode"]; - /** - * Format: date-time - * @description Timestamp of when the ramp process was created. - */ - createdAt?: string; - currentPhase?: components["schemas"]["RampPhase"]; - /** @description BR Code for PIX payment, if applicable. */ - depositQrCode?: string | null; - feeCurrency: components["schemas"]["RampCurrency"]; - /** @description The source network or payment method. */ - from?: components["schemas"]["DestinationType"]; - /** @description Unique identifier for the ramp process. */ - id?: string; - inputAmount: string; - inputCurrency: string; - network?: components["schemas"]["Networks"]; - networkFeeFiat: string; - networkFeeUSD: string; - outputAmount: string; - outputCurrency: string; - partnerFeeFiat: string; - partnerFeeUSD: string; - paymentMethod: components["schemas"]["PaymentMethod"]; - processingFeeFiat: string; - processingFeeUSD: string; - /** - * Format: uuid - * @description The quote ID associated with this ramp process. - */ - quoteId?: string; - /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ - sessionId?: string; - status?: components["schemas"]["SimpleStatus"]; - /** @description The destination network or payment method. */ - to?: components["schemas"]["DestinationType"]; - totalFeeFiat: string; - totalFeeUSD: string; - /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ - transactionExplorerLink?: string; - /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ - transactionHash?: string; - /** @description Type of ramp process. */ - type?: components["schemas"]["RampDirection"]; - /** @description Array of unsigned transactions that need to be signed by the user. */ - unsignedTxs?: components["schemas"]["UnsignedTx"][]; - /** - * Format: date-time - * @description Timestamp of the last update to the ramp process. - */ - updatedAt?: string; - vortexFeeFiat: string; - vortexFeeUSD: string; - /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ - walletAddress?: string; + message: string; + success: boolean; }; }; }; - /** - * @description Bad Request. Possible reasons: - * - Missing required fields (rampId, presignedTxs) - * - Invalid additional data format (if provided, must be an object) - */ + /** @description Email missing or locale not a string. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; - }; - /** @description Authentication is required for an owned ramp or managed-profile selection. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; - }; - }; - /** @description Ramp ownership or managed-profile authorization failed. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; + "application/json": { + error: string; + }; }; }; - /** @description Internal Server Error. */ + /** @description Failed to send the OTP email. */ 500: { headers: { [name: string]: unknown; }; content: { - /** - * @example { - * "message": "An unexpected error occurred." - * } - */ - "application/json": Record; + "application/json": { + error: string; + }; }; }; }; }; - getAlfredpayStatus: { + verifyOTP: { parameters: { - query: { - country: components["schemas"]["AlfredpayCountry"]; - /** @description Selects the individual or business customer. When omitted, the active customer entity is used for backward compatibility. */ - type?: components["schemas"]["AlfredpayCustomerType"]; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; + query?: never; + header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + /** + * @example { + * "email": "user@example.com", + * "token": "123456" + * } + */ + "application/json": { + /** Format: email */ + email: string; + /** @description The 6-digit code from the email. */ + token: string; + }; + }; + }; responses: { - /** @description Customer status returned. */ + /** @description Session created. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayStatusResponse"]; + "application/json": { + access_token: string; + refresh_token: string; + success: boolean; + /** Format: uuid */ + user_id: string; + }; }; }; - /** @description Invalid or missing country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing fields, or the OTP is invalid or expired. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; - }; - /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Status refresh failed. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": { + error: string; + }; }; }; }; }; - createAlfredpayBusinessCustomer: { + createSubaccount: { parameters: { query?: never; header?: { @@ -4935,66 +4691,54 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { + requestBody?: { content: { - "application/json": components["schemas"]["AlfredpayCreateCustomerRequest"]; + "application/json": components["schemas"]["CreateSubaccountRequest"]; }; }; responses: { - /** @description Business customer created. */ + /** @description Subaccount created or KYC retry initiated successfully. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayCreateCustomerResponse"]; + "application/json": components["schemas"]["CreateSubaccountResponse"]; }; }; - /** @description Invalid country, unavailable email, existing customer, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** + * @description Bad Request. Possible reasons: + * - Missing required fields (cpf, cnpj, companyName, startDate) + * - Subaccount already created and KYC level > 0 + * - Other invalid request details + */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description An upstream customer exists with a conflicting country or type. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Customer creation failed. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Internal Server Error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Alfredpay returned an invalid existing-customer response. */ - 502: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - createAlfredpayIndividualCustomer: { + fetchSubaccountKycStatus: { parameters: { - query?: never; + query: { + /** @description The user's Tax ID. */ + taxId: string; + }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; @@ -5002,67 +4746,53 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AlfredpayCreateCustomerRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Customer created. */ + /** @description Successfully retrieved KYC status. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayCreateCustomerResponse"]; + "application/json": components["schemas"]["GetKycStatusResponse"]; }; }; - /** @description Invalid country, unavailable email, existing customer, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing taxId or subaccount not found (returned as 400 from code). */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description An upstream customer exists with a conflicting country or type. */ - 409: { + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description No KYC process started. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; - /** @description Customer creation failed. */ + /** @description Internal Server Error (e.g., no KYC events found when expected). */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Alfredpay returned an invalid existing-customer response. */ - 502: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - findAlfredpayKybCustomerAndBusiness: { + brlaGetSelfieLivenessUrl: { parameters: { query: { - country: "CO" | "MX"; + /** @description CPF or CNPJ. */ + taxId: string; }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ @@ -5073,52 +4803,40 @@ export interface operations { }; requestBody?: never; responses: { - /** @description KYB submission and related-person IDs returned. */ + /** @description Liveness URL returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayKybDetailsResponse"]; + "application/json": components["schemas"]["BrlaGetSelfieLivenessUrlResponse"]; }; }; - /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing taxId or ramp disabled. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay business customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Lookup failed. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Internal server error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - getAlfredpayKybRedirectLink: { + brlaGetUploadUrls: { parameters: { - query: { - country: components["schemas"]["AlfredpayCountry"]; - }; + query?: never; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; @@ -5126,53 +4844,48 @@ export interface operations { path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["AveniaKYCDataUploadRequest"]; + }; + }; responses: { - /** @description KYB redirect link returned. */ + /** @description Upload URLs returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayRedirectLinkResponse"]; + "application/json": components["schemas"]["AveniaKYCDataUploadResponse"]; }; }; - /** @description Invalid country, KYB already verifying or complete, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing/invalid documentType or taxId; or ramp disabled for this tax ID. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay business customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Redirect-link creation failed. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Internal server error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - getAlfredpayKycRedirectLink: { + getBrlaUser: { parameters: { - query: { - country: components["schemas"]["AlfredpayCountry"]; + query?: { + /** @description The user's Tax ID. */ + taxId?: string; }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ @@ -5183,52 +4896,57 @@ export interface operations { }; requestBody?: never; responses: { - /** @description KYC redirect link returned. */ + /** @description Successfully retrieved user information. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayRedirectLinkResponse"]; + "application/json": components["schemas"]["GetUserResponse"]; }; }; - /** @description Invalid country, KYC already verifying or complete, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** + * @description Bad Request. Possible reasons: + * - Missing taxId query parameter + * - KYC invalid + */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Subaccount not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; - /** @description Redirect-link creation failed. */ + /** @description Internal Server Error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - getAlfredpayKycStatus: { + getBrlaUserRemainingLimit: { parameters: { query: { - country: components["schemas"]["AlfredpayCountry"]; - type?: components["schemas"]["AlfredpayCustomerType"]; + /** @description The user's Tax ID. */ + taxId?: string; + /** @description Ramp direction whose remaining limit should be returned. */ + direction: "BUY" | "SELL"; }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ @@ -5239,50 +4957,51 @@ export interface operations { }; requestBody?: never; responses: { - /** @description Verification status returned. */ + /** @description Successfully retrieved user's remaining limits. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayKycStatusResponse"]; + "application/json": components["schemas"]["GetUserRemainingLimitResponse"]; }; }; - /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing taxId query parameter or other invalid request. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Customer or verification attempt not found. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Subaccount not found or limits not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; - /** @description Status lookup failed. */ + /** @description Internal Server Error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - notifyAlfredpayKycRedirectFinished: { + getAveniaKybAttemptStatus: { parameters: { - query?: never; + query: { + attemptId: string; + }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; @@ -5290,228 +5009,257 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AlfredpayRedirectNotificationRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Redirect state recorded. */ + /** @description KYB attempt status returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["AveniaKybAttemptStatusResponse"]; }; }; - /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing attempt ID, invalid selector UUID, or authentication subject. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ + /** @description Attempt does not belong to the effective profile. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description KYB attempt or account not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; - /** @description State update failed. */ + /** @description The attempt is no longer the current bound KYB attempt. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Status refresh failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - notifyAlfredpayKycRedirectOpened: { + createAveniaKybDocument: { parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query: { + subAccountId: string; }; + header?: never; path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["AlfredpayRedirectNotificationRequest"]; + "application/json": components["schemas"]["AveniaKybDocumentRequest"]; }; }; responses: { - /** @description Redirect state recorded. */ - 200: { + /** @description Document upload targets created. */ + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["AveniaKybDocumentUploadResponse"]; }; }; - /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Invalid document request. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; + content?: never; }; /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { + 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Managed profile or corridor is not authorized. */ + 403: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description State update failed. */ - 500: { + /** @description Subaccount not found. */ + 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - retryAlfredpayKyc: { + getAveniaKybDocument: { parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query: { + subAccountId: string; }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AlfredpayRetryRequest"]; + header?: never; + path: { + documentId: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description Retry initialized. */ + /** @description Document status. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayRetryResponse"]; + "application/json": components["schemas"]["AveniaKybDocumentResponse"]; }; }; - /** @description No failed submission is available, the selector UUID is invalid, or the managed-profile customer type mismatches. */ + /** @description Invalid document identifier. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; + content?: never; }; /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { + 401: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Document does not belong to the effective profile. */ + 403: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Retry failed. */ - 500: { + /** @description Document not found. */ + 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - sendAlfredpayKybSubmission: { + submitAveniaKybLevel1Api: { parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query: { + subAccountId: string; }; + header?: never; path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["AlfredpaySendSubmissionRequest"]; + "application/json": components["schemas"]["AveniaKybLevel1Payload"]; }; }; responses: { - /** @description KYB submission sent. */ + /** @description KYB attempt submitted. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["KycLevel1Response"]; }; }; - /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Invalid submission or document state. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; + content?: never; }; /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay business customer not found. */ - 404: { + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Subaccount or referenced document not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Submission finalization failed. */ - 500: { + /** @description A different KYB submission is already in progress. */ + 409: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - sendAlfredpayKycSubmission: { + startAveniaKybLevel1Hosted: { parameters: { - query?: never; + query: { + subAccountId: string; + }; header?: { /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; @@ -5519,113 +5267,135 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["AlfredpaySendSubmissionRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description KYC submission sent. */ + /** @description Hosted KYB attempt and step URLs returned. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["AveniaKybHostedResponse"]; }; }; - /** @description Invalid country, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing subaccount, non-company account, invalid selector UUID, customer-type mismatch, or invalid request. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Subaccount not found. */ 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; - /** @description Submission finalization failed. */ + /** @description Company is approved or a non-resumable KYB attempt is in progress. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description KYB initialization failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; + }; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - submitAlfredpayKybFile: { + createAveniaKybUbo: { parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query: { + subAccountId: string; }; + header?: never; path?: never; cookie?: never; }; requestBody: { content: { - "multipart/form-data": components["schemas"]["AlfredpayKybFileUploadRequest"]; + "application/json": components["schemas"]["AveniaUboPayload"]; }; }; responses: { - /** @description File uploaded. */ - 200: { + /** @description UBO registered. */ + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["AveniaUboResponse"]; }; }; - /** @description Invalid country, missing file, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Invalid UBO or document state. */ 400: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; + content?: never; }; /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay business customer not found. */ - 404: { + 403: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Subaccount or referenced document not found. */ + 404: { + headers: { + [name: string]: unknown; }; + content?: never; }; - /** @description Upload failed. */ - 500: { + /** @description A referenced document is not ready. */ + 409: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + content?: never; + }; + /** @description Avenia is unavailable or returned an invalid response. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - submitAlfredpayKybInformation: { + recordInitialAveniaKycAttempt: { parameters: { query?: never; header?: { @@ -5637,62 +5407,44 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SubmitKybInformationRequest"]; + "application/json": components["schemas"]["RecordInitialKycAttemptRequest"]; }; }; responses: { - /** @description KYB information accepted. */ + /** @description Attempt marker recorded or already present. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SubmitInformationResponse"]; + "application/json": Record; }; }; - /** @description Invalid country, company data, questionnaire, selector UUID, or managed-profile customer type. */ + /** @description Missing tax ID, invalid selector UUID, or managed-profile customer-type mismatch. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayValidationBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; /** @description Managed profile or corridor is not authorized. */ 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay business customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description KYB is already in review or complete. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Submission failed. */ + /** @description Attempt recording failed. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - submitAlfredpayKybRelatedPersonFile: { + brlaNewKyc: { parameters: { query?: never; header?: { @@ -5704,111 +5456,83 @@ export interface operations { }; requestBody: { content: { - "multipart/form-data": components["schemas"]["AlfredpayKybRelatedPersonFileUploadRequest"]; + "application/json": components["schemas"]["KycLevel1Payload"]; }; }; responses: { - /** @description File uploaded. */ + /** @description KYC submission accepted. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["KycLevel1Response"]; }; }; - /** @description Invalid country, missing file, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Validation failure. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; }; }; - /** @description Authentication required. */ 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay business customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Upload failed. */ + 403: components["responses"]["BrlaManagedSelectorForbidden"]; + /** @description Internal server error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - submitAlfredpayKycFile: { + brlaValidatePixKey: { parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query: { + /** @description Pix key to validate (CPF, CNPJ, email, phone, or random key). */ + pixKey: string; }; + header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "multipart/form-data": components["schemas"]["AlfredpayKycFileUploadRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description File uploaded. */ + /** @description Validation result. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SuccessResponse"]; + "application/json": components["schemas"]["BrlaValidatePixKeyResponse"]; }; }; - /** @description Invalid country, missing file, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** @description Missing or invalid pix key. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; - }; - /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; - /** @description Upload failed. */ + /** @description Internal server error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["BrlaErrorResponse"]; }; }; }; }; - submitAlfredpayKycInformation: { + getUserLimits: { parameters: { query?: never; header?: { @@ -5820,495 +5544,561 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SubmitKycInformationRequest"]; + "application/json": components["schemas"]["GetUserLimitsRequest"]; }; }; responses: { - /** @description KYC information accepted. */ + /** @description Limits and consumed amounts for both directions of every requested corridor. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SubmitInformationResponse"]; + "application/json": components["schemas"]["GetUserLimitsResponse"]; }; }; - /** @description Invalid country, KYC fields, selector UUID, or managed-profile customer type. */ + /** @description Invalid corridor list or no completed provider profile for a requested corridor. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayValidationBadRequestResponse"]; + "application/json": components["schemas"]["FlatManagedSelectorErrorResponse"]; }; }; - /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { + /** @description Missing or invalid credentials. */ + 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; }; }; - /** @description Submission failed. */ - 500: { + /** @description The credential is not linked to a user. */ + 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["FlatManagedSelectorErrorResponse"]; + }; + }; + /** @description Provider limits are unavailable or invalid. */ + 502: { + headers: { + [name: string]: unknown; }; + content?: never; }; }; }; - getAveniaKybAttemptStatus: { + listManagedProfiles: { parameters: { - query: { - attemptId: string; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query?: { + /** @description Maximum records to return. */ + limit?: number; + /** @description Number of records to skip. */ + offset?: number; + /** @description Lifecycle records to include. */ + status?: "active" | "deleted" | "all"; }; + header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description KYB attempt status returned. */ + /** @description A page of owned managed profiles and offset pagination metadata. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaKybAttemptStatusResponse"]; + "application/json": components["schemas"]["ListManagedProfilesResponse"]; }; }; - /** @description Missing attempt ID, invalid selector UUID, or authentication subject. */ + /** @description `MANAGED_PROFILE_INVALID_INPUT`: invalid pagination or status filter. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Attempt does not belong to the effective profile. */ - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description KYB attempt or account not found. */ - 404: { + /** @description Missing, invalid, expired, or revoked manager authentication. */ + 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description The attempt is no longer the current bound KYB attempt. */ - 409: { + /** @description `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + 403: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Status refresh failed. */ - 500: { + /** @description `MANAGED_PROFILE_CONFLICT`: a retained child has an invalid customer-entity layout. */ + 409: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Avenia is unavailable or returned an invalid response. */ - 502: { + /** @description Internal server error. */ + 500: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; }; }; - createAveniaKybDocument: { + createManagedProfile: { parameters: { - query: { - subAccountId: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { - "application/json": components["schemas"]["AveniaKybDocumentRequest"]; + "application/json": components["schemas"]["CreateManagedProfileRequest"]; }; }; responses: { - /** @description Document upload targets created. */ + /** @description Idempotent retry returned the existing active managed profile. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedProfileResponse"]; + }; + }; + /** @description Managed profile created. */ 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaKybDocumentUploadResponse"]; + "application/json": components["schemas"]["ManagedProfileResponse"]; }; }; - /** @description Invalid document request. */ + /** @description `MANAGED_PROFILE_INVALID_INPUT`: required input is missing or invalid. */ 400: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Authentication required. */ + /** @description Missing, invalid, expired, or revoked manager authentication. */ 401: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Managed profile or corridor is not authorized. */ + /** @description `MANAGED_PROFILE_MANAGER_NOT_FOUND`, `MANAGED_PROFILE_MANAGER_INACTIVE`, or `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active managed-profile manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ 403: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Subaccount not found. */ - 404: { + /** @description `MANAGED_PROFILE_CONFLICT`: an immutable external subject or contact email is reserved with different profile data. */ + 409: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Avenia is unavailable or returned an invalid response. */ - 502: { + /** @description Internal server error. */ + 500: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; }; }; - getAveniaKybDocument: { - parameters: { - query: { - subAccountId: string; - }; + getManagedProfile: { + parameters: { + query?: never; header?: never; path: { - documentId: string; + /** @description Managed child profile ID. */ + profileId: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Document status. */ + /** @description Owned active or deleted managed profile. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaKybDocumentResponse"]; + "application/json": components["schemas"]["ManagedProfileResponse"]; }; }; - /** @description Invalid document identifier. */ + /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId is not a UUID. */ 400: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Authentication required. */ + /** @description Missing, invalid, expired, or revoked manager authentication. */ 401: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Document does not belong to the effective profile. */ + /** @description `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ 403: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Document not found. */ + /** @description `MANAGED_PROFILE_NOT_FOUND`: the child does not exist or is not owned by this manager. */ 404: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Avenia is unavailable or returned an invalid response. */ - 502: { + /** @description `MANAGED_PROFILE_CONFLICT`: the retained child has an invalid customer-entity layout. */ + 409: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; }; }; - submitAveniaKybLevel1Api: { + deleteManagedProfile: { parameters: { - query: { - subAccountId: string; - }; + query?: never; header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AveniaKybLevel1Payload"]; + path: { + /** @description Managed child profile ID. */ + profileId: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description KYB attempt submitted. */ - 200: { + /** @description Managed profile is logically deleted and its credentials are revoked. */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["KycLevel1Response"]; - }; + content?: never; }; - /** @description Invalid submission or document state. */ + /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId is not a UUID. */ 400: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Authentication required. */ + /** @description Missing, invalid, expired, or revoked manager authentication. */ 401: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Managed profile or corridor is not authorized. */ + /** @description `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ 403: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Subaccount or referenced document not found. */ + /** @description `MANAGED_PROFILE_NOT_FOUND`: the child does not exist or is not owned by this manager. */ 404: { headers: { [name: string]: unknown; }; - content?: never; - }; - /** @description A different KYB submission is already in progress. */ - 409: { - headers: { - [name: string]: unknown; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; - content?: never; }; - /** @description Avenia is unavailable or returned an invalid response. */ - 502: { + /** @description Internal server error. */ + 500: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; }; }; - startAveniaKybLevel1Hosted: { + listManagedProfileApiCredentials: { parameters: { - query: { - subAccountId: string; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + query?: never; + header?: never; + path: { + /** @description Managed child profile ID. */ + profileId: string; }; - path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description Hosted KYB attempt and step URLs returned. */ + /** @description Child credentials without secret values. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaKybHostedResponse"]; + "application/json": components["schemas"]["ListApiCredentialsResponse"]; }; }; - /** @description Missing subaccount, non-company account, invalid selector UUID, customer-type mismatch, or invalid request. */ + /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId is not a UUID. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["BrlaManagedSelectorForbidden"]; - /** @description Subaccount not found. */ - 404: { + /** @description Missing, invalid, expired, or revoked manager authentication. */ + 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Company is approved or a non-resumable KYB attempt is in progress. */ - 409: { + /** @description `CREDENTIAL_ACCESS_DENIED`: the manager is inactive. `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated credential belongs directly to a managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description KYB initialization failed. */ - 500: { + /** @description `CREDENTIAL_NOT_FOUND`: the active child does not exist or is not owned by this manager. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Avenia is unavailable or returned an invalid response. */ - 502: { + /** @description Internal server error. */ + 500: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; }; }; - createAveniaKybUbo: { + createManagedProfileApiCredential: { parameters: { - query: { - subAccountId: string; - }; + query?: never; header?: never; - path?: never; + path: { + /** @description Managed child profile ID. */ + profileId: string; + }; cookie?: never; }; - requestBody: { + requestBody?: { content: { - "application/json": components["schemas"]["AveniaUboPayload"]; + "application/json": components["schemas"]["CreateApiCredentialRequest"]; }; }; responses: { - /** @description UBO registered. */ + /** @description Credential created. This is the only response containing `secretKey`. */ 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AveniaUboResponse"]; + "application/json": components["schemas"]["CreateApiCredentialResponse"]; }; }; - /** @description Invalid UBO or document state. */ + /** @description Invalid profileId, credential name, or expiry. */ 400: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Authentication required. */ + /** @description Missing, invalid, expired, or revoked manager authentication. */ 401: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Managed profile or corridor is not authorized. */ + /** @description `CREDENTIAL_ACCESS_DENIED`: the manager is inactive. `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated credential belongs directly to a managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ 403: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description Subaccount or referenced document not found. */ + /** @description `CREDENTIAL_NOT_FOUND`: the active child does not exist or is not owned by this manager. */ 404: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - /** @description A referenced document is not ready. */ + /** @description `CREDENTIAL_LIMIT_REACHED`: the child already holds five active, non-expired credentials. */ 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; + }; + /** @description Internal server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; + }; + }; + }; + revokeManagedProfileApiCredential: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Managed child profile ID. */ + profileId: string; + /** @description Child credential ID. */ + credentialId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Credential is revoked; both values are unusable. Repeated revocation also returns this status. */ + 204: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Avenia is unavailable or returned an invalid response. */ - 502: { + /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId or credentialId is not a UUID. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; + }; + /** @description Missing, invalid, expired, or revoked manager authentication. */ + 401: { headers: { [name: string]: unknown; }; - content?: never; - }; - }; - }; - recordInitialAveniaKycAttempt: { - parameters: { - query?: never; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["RecordInitialKycAttemptRequest"]; + content: { + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + }; }; - }; - responses: { - /** @description Attempt marker recorded or already present. */ - 200: { + /** @description `CREDENTIAL_ACCESS_DENIED`: the manager is inactive. `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated credential belongs directly to a managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + 403: { headers: { [name: string]: unknown; }; content: { - "application/json": Record; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Missing tax ID, invalid selector UUID, or managed-profile customer-type mismatch. */ - 400: { + /** @description `CREDENTIAL_NOT_FOUND`: the active child or credential does not exist, or is not owned by this manager. */ + 404: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaManagedBadRequestResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; - /** @description Authentication required. */ - 401: components["responses"]["ManagedSelectorUnauthorized"]; - /** @description Managed profile or corridor is not authorized. */ - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Attempt recording failed. */ + /** @description Internal server error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["BrlaErrorResponse"]; + "application/json": components["schemas"]["ManagedProfileErrorResponse"]; }; }; }; @@ -6457,61 +6247,7 @@ export interface operations { }; }; }; - listAlfredpayFiatAccounts: { - parameters: { - query: { - country: components["schemas"]["AlfredpayCountry"]; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Fiat accounts returned. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayFiatAccount"][]; - }; - }; - /** @description Invalid country, credential without an effective user, invalid selector UUID, or managed-profile customer-type mismatch. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; - }; - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Account lookup failed. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - }; - }; - createAlfredpayFiatAccount: { + createQuote: { parameters: { query?: never; header?: { @@ -6521,612 +6257,876 @@ export interface operations { path?: never; cookie?: never; }; - requestBody: { + requestBody?: { content: { - "application/json": components["schemas"]["AlfredpayAddFiatAccountRequest"]; + /** + * @example { + * "from": "pix", + * "inputAmount": "33", + * "inputCurrency": "BRL", + * "outputCurrency": "USDC", + * "partnerId": "myPartnerId", + * "rampType": "BUY", + * "to": "polygon" + * } + */ + "application/json": { + /** @description Your api key, if available. */ + apiKey?: string; + countryCode?: components["schemas"]["CountryCode"]; + /** @description From destination */ + from: components["schemas"]["DestinationType"]; + /** + * @description The amount of currency to be input. + * @example 100.00 + */ + inputAmount: string; + /** @description The currency type for the input amount. */ + inputCurrency: components["schemas"]["RampCurrency"]; + network?: components["schemas"]["Networks"]; + /** @description The desired currency type for the output amount. */ + outputCurrency: components["schemas"]["RampCurrency"]; + /** @description Your partner ID, if available. */ + partnerId?: string; + paymentMethod?: components["schemas"]["PaymentMethod"]; + /** @description The type of ramp process (on-ramp or off-ramp). */ + rampType: components["schemas"]["RampDirection"]; + /** @description To destination */ + to: components["schemas"]["DestinationType"]; + }; }; }; responses: { - /** @description Fiat account created. */ - 200: { + /** @description Quote successfully created. */ + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayCreateFiatAccountResponse"]; + /** + * @example { + * "expiresAt": "2025-05-16T12:30:00Z", + * "fee": "0.50", + * "from": "polygon", + * "id": "quote_7af7171e-aa42-49a2-80c2-9e18483bad38", + * "inputAmount": "33", + * "inputCurrency": "usdc", + * "outputAmount": "32500.50", + * "outputCurrency": "ars", + * "rampType": "sell", + * "to": "cbu" + * } + */ + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + /** + * Format: date-time + * @description The timestamp when this quote expires. + */ + expiresAt?: string; + feeCurrency: components["schemas"]["RampCurrency"]; + from?: components["schemas"]["DestinationType"]; + /** + * Format: uuid + * @description Unique identifier for the quote. + */ + id?: string; + /** @description The input amount specified in the request. */ + inputAmount?: string; + inputCurrency?: components["schemas"]["RampCurrency"]; + networkFeeFiat: string; + networkFeeUSD: string; + /** @description The calculated output amount after fees and conversions. */ + outputAmount?: string; + outputCurrency?: components["schemas"]["RampCurrency"]; + partnerFeeFiat: string; + partnerFeeUSD: string; + processingFeeFiat: string; + processingFeeUSD: string; + /** @description The type of ramp process. */ + rampType?: components["schemas"]["RampDirection"]; + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + }; }; }; - /** @description Invalid country, account details, credential without an effective user, invalid selector UUID, or managed-profile customer-type mismatch. */ + /** + * @description Bad Request. Possible reasons: + * - Missing required fields (rampType, from, to, inputAmount, inputCurrency, outputCurrency) + * - Invalid ramp type (must be "BUY" or "SELL") + */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; - }; - }; - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - /** @description Account creation failed. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; - }; - }; - }; - }; - deleteAlfredpayFiatAccount: { - parameters: { - query: { - country: components["schemas"]["AlfredpayCountry"]; - }; - header?: { - /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ - "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; - }; - path: { - fiatAccountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Fiat account deleted. */ - 204: { - headers: { - [name: string]: unknown; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; - content?: never; }; - /** @description Invalid country, provider rejection, credential without an effective user, invalid selector UUID, or managed-profile customer-type mismatch. */ - 400: { + /** @description Invalid authentication or managed-profile selection. */ + 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayManagedBadRequestResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - 401: components["responses"]["ManagedSelectorUnauthorized"]; - 403: components["responses"]["ManagedSelectorForbidden"]; - /** @description Alfredpay customer not found. */ - 404: { + /** @description Partner authorization or managed-profile authorization failed. */ + 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Account deletion failed. */ + /** @description Internal Server Error. Low-liquidity route failures use this status with a safe user-facing message; unexpected internal failures remain masked. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AlfredpayErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; }; }; - listManagedProfiles: { + createBestQuote: { parameters: { - query?: { - /** @description Maximum records to return. */ - limit?: number; - /** @description Number of records to skip. */ - offset?: number; - /** @description Lifecycle records to include. */ - status?: "active" | "deleted" | "all"; + query?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; - header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody?: { + content: { + /** + * @example { + * "from": "pix", + * "inputAmount": "30", + * "inputCurrency": "BRL", + * "outputCurrency": "USDC", + * "partnerId": "myPartnerId", + * "rampType": "BUY" + * } + */ + "application/json": components["schemas"]["CreateBestQuoteRequest"]; + }; + }; responses: { - /** @description A page of owned managed profiles and offset pagination metadata. */ - 200: { + /** @description Quote successfully created. */ + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ListManagedProfilesResponse"]; + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + /** + * Format: date-time + * @description The timestamp when this quote expires. + */ + expiresAt?: string; + feeCurrency: components["schemas"]["RampCurrency"]; + from?: components["schemas"]["DestinationType"]; + /** + * Format: uuid + * @description Unique identifier for the quote. + */ + id?: string; + /** @description The input amount specified in the request. */ + inputAmount?: string; + inputCurrency?: components["schemas"]["RampCurrency"]; + networkFeeFiat: string; + networkFeeUSD: string; + /** @description The calculated output amount after fees and conversions. */ + outputAmount?: string; + outputCurrency?: components["schemas"]["RampCurrency"]; + partnerFeeFiat: string; + partnerFeeUSD: string; + processingFeeFiat: string; + processingFeeUSD: string; + /** @description The type of ramp process. */ + rampType?: components["schemas"]["RampDirection"]; + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + }; }; }; - /** @description `MANAGED_PROFILE_INVALID_INPUT`: invalid pagination or status filter. */ + /** + * @description Bad Request. Possible reasons: + * - Missing required fields (rampType, from, to, inputAmount, inputCurrency, outputCurrency) + * - Invalid ramp type (must be "BUY" or "SELL") + */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ + /** @description Invalid authentication or managed-profile selection. */ 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + /** @description Partner authorization or managed-profile authorization failed. */ 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `MANAGED_PROFILE_CONFLICT`: a retained child has an invalid customer-entity layout. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Internal server error. */ + /** @description Internal Server Error. Low-liquidity route failures use this status with a safe user-facing message when every eligible route cannot serve the requested amount; unexpected internal failures remain masked. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorResponse"]; }; }; }; }; - createManagedProfile: { + getRampInfo: { parameters: { query?: never; - header?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateManagedProfileRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Idempotent retry returned the existing active managed profile. */ + /** @description Sanitized corridor eligibility. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileResponse"]; - }; - }; - /** @description Managed profile created. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileResponse"]; + "application/json": components["schemas"]["RampInfoResponse"]; }; }; - /** @description `MANAGED_PROFILE_INVALID_INPUT`: required input is missing or invalid. */ + /** @description Malformed key or wrong key type. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ApiCredentialManagedSelectorErrorResponse"]; }; }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ + /** @description Missing, invalid, expired, or revoked API credential. */ 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ApiCredentialManagedSelectorErrorResponse"]; }; }; - /** @description `MANAGED_PROFILE_MANAGER_NOT_FOUND`, `MANAGED_PROFILE_MANAGER_INACTIVE`, or `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active managed-profile manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + /** @description `CREDENTIAL_MISMATCH`: presented public and secret values belong to different credentials. */ 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `MANAGED_PROFILE_CONFLICT`: an immutable external subject or contact email is reserved with different profile data. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description Internal server error. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ApiCredentialManagedSelectorErrorResponse"]; }; }; }; }; - getManagedProfile: { + getRampErrorLogs: { parameters: { query?: never; - header?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; + }; path: { - /** @description Managed child profile ID. */ - profileId: string; + /** @description Ramp ID. */ + id: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Owned active or deleted managed profile. */ + /** @description Error log array (empty if no errors). */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileResponse"]; + "application/json": components["schemas"]["GetRampErrorLogsResponse"]; }; }; - /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId is not a UUID. */ + /** @description The managed-profile selector is invalid. */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ManagedSelectorErrorResponse"]; }; }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ + /** @description Authentication required. */ 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + /** @description Ramp does not belong to authenticated principal. */ 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description `MANAGED_PROFILE_NOT_FOUND`: the child does not exist or is not owned by this manager. */ + /** @description Ramp not found. */ 404: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; + content?: never; }; - /** @description `MANAGED_PROFILE_CONFLICT`: the retained child has an invalid customer-entity layout. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; + }; + }; + registerRamp: { + parameters: { + query?: never; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; - /** @description Internal server error. */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + /** + * @example { + * "additionalData": { + * "pixDestination": "711.711.011-11", + * "receiverTaxId": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + * "taxId": "711.711.011-11" + * }, + * "quoteId": "8e4bca04-aa22-4f86-9ce5-80aaef58ef83", + * "signingAccounts": [ + * { + * "address": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + * "network": "moonbeam" + * }, + * { + * "address": "6ftBYTotU4mmCuvUqJvk6qEP7uCzzz771pTMoxcbHFb9rcPv", + * "network": "pendulum" + * } + * ] + * } + */ + "application/json": { + /** + * @description Optional additional data for the ramp process. + * + * For Brazil onramps, destinationAddress is required. + * + * For Brazil offramps, pixDestination is required. The user's taxId is derived from the authenticated account; receiverTaxId is optional and defaults to the user's own tax ID. + */ + additionalData?: { + /** @description Destination address, used for onramp. */ + destinationAddress?: string; + /** @description Auth token obtained from Monerium's API, for the current user. Only required for Monerium-related ramps. */ + moneriumAuthToken: string; + paymentData?: components["schemas"]["PaymentData"]; + /** @description PIX key for the destination account in an onramp. */ + pixDestination?: string; + /** @description Tax ID of the receiver for onramp. */ + receiverTaxId?: string; + sessionId?: string; + /** @description Tax ID of the user. */ + taxId?: string; + /** @description Wallet address initiating the offramp. */ + walletAddress: string; + } & { + [key: string]: unknown; + }; + /** + * Format: uuid + * @description The unique identifier for the quote. + */ + quoteId: string; + /** @description Array of accounts (public addresses) that will be used for signing transactions. Provide one Substrate ephemeral (Pendulum) and one EVM ephemeral; all EVM legs, including Moonbeam, use the EVM account. */ + signingAccounts: { + /** @description The account address. */ + address: string; + /** + * @description The type of the account. + * @enum {string} + */ + type: "EVM" | "Substrate"; + }[]; }; }; }; - }; - deleteManagedProfile: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Managed child profile ID. */ - profileId: string; - }; - cookie?: never; - }; - requestBody?: never; responses: { - /** @description Managed profile is logically deleted and its credentials are revoked. */ - 204: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId is not a UUID. */ - 400: { + /** @description Ramp process successfully registered. */ + 201: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + /** + * @example { + * "brCode": "00020126...", + * "createdAt": "2024-05-16T10:00:00Z", + * "currentPhase": "pending_signature", + * "from": "stellar", + * "id": "proc_12345", + * "quoteId": "41a756dc-04e4-4e4b-b243-9c8f977c24d6", + * "to": "pix", + * "type": "off", + * "unsignedTxs": [ + * { + * "data": "AAAA...", + * "type": "stellar_payment" + * } + * ], + * "updatedAt": "2024-05-16T10:00:00Z" + * } + */ + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; + }; }; }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ - 401: { + /** @description Bad Request - Invalid input, missing required fields, or validation error. */ + 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + /** + * @example { + * "message": "Missing required fields" + * } + */ + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated profile is not an active manager, or is a direct managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + 401: components["responses"]["ManagedSelectorUnauthorized"]; + /** @description Quote ownership or managed-profile authorization failed. */ 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `MANAGED_PROFILE_NOT_FOUND`: the child does not exist or is not owned by this manager. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Internal server error. */ + /** @description Internal Server Error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + /** + * @example { + * "message": "An unexpected error occurred." + * } + */ + "application/json": components["schemas"]["ErrorResponse"]; }; }; }; }; - listManagedProfileApiCredentials: { + startRamp: { parameters: { query?: never; - header?: never; - path: { - /** @description Managed child profile ID. */ - profileId: string; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody?: { + content: { + /** + * @example { + * "rampId": "proc_12345" + * } + */ + "application/json": components["schemas"]["StartRampRequest"]; + }; + }; responses: { - /** @description Child credentials without secret values. */ + /** @description Ramp process successfully started or updated. */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ListApiCredentialsResponse"]; + /** + * @example { + * "createdAt": "2024-05-16T10:00:00Z", + * "currentPhase": "processing", + * "depositQrCode": "00020126...", + * "from": "stellar", + * "id": "proc_12345", + * "quoteId": "quote_7af7171e-aa42-49a2-80c2-9e18483bad38", + * "to": "pix", + * "type": "sell", + * "unsignedTxs": [], + * "updatedAt": "2024-05-16T12:30:00Z" + * } + */ + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; + }; }; }; - /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId is not a UUID. */ + /** + * @description Bad Request. Possible reasons: + * - Missing required fields (rampId, presignedTxs) + * - Invalid additional data format (if provided, must be an object) + */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ + /** @description Authentication is required for an owned ramp or managed-profile selection. */ 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description `CREDENTIAL_ACCESS_DENIED`: the manager is inactive. `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated credential belongs directly to a managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + /** @description Ramp ownership or managed-profile authorization failed. */ 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `CREDENTIAL_NOT_FOUND`: the active child does not exist or is not owned by this manager. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Internal server error. */ + /** @description Internal Server Error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + /** + * @example { + * "message": "An unexpected error occurred." + * } + */ + "application/json": Record; }; }; }; }; - createManagedProfileApiCredential: { + updateRamp: { parameters: { query?: never; - header?: never; - path: { - /** @description Managed child profile ID. */ - profileId: string; + header?: { + /** @description Selects one active, directly managed child as the effective subject. Use the controlling manager's secret `X-API-Key`, or its Supabase Bearer session where that operation accepts Bearer authentication. Public keys and direct child credentials cannot use this selector; a direct child credential already acts as its own subject without the header. Invalid UUIDs return `400 INVALID_MANAGED_PROFILE_ID`, missing authentication returns `401 AUTHENTICATION_REQUIRED`, and unauthorized, deleted, malformed, or corridor-disallowed children return `403 MANAGED_PROFILE_ACCESS_DENIED`. */ + "X-Managed-Profile-Id"?: components["parameters"]["ManagedProfileId"]; }; + path?: never; cookie?: never; }; requestBody?: { content: { - "application/json": components["schemas"]["CreateApiCredentialRequest"]; + /** + * @example { + * "additionalData": { + * "squidRouterApproveHash": "0x123...", + * "squidRouterSwapHash": "0x456..." + * }, + * "presignedTxs": [ + * { + * "meta": {}, + * "nonce": 1, + * "phase": "RampPhase", + * "signer": "GB2TP24WCY6BPGFX4SOGDHT7IGJRR7HCDQT2VL2MVCZJTJCGKMVGQGQB", + * "txData": "AAAAAKu..." + * } + * ], + * "rampId": "proc_12345" + * } + */ + "application/json": components["schemas"]["UpdateRampRequest"]; }; }; responses: { - /** @description Credential created. This is the only response containing `secretKey`. */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreateApiCredentialResponse"]; - }; - }; - /** @description Invalid profileId, credential name, or expiry. */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `CREDENTIAL_ACCESS_DENIED`: the manager is inactive. `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated credential belongs directly to a managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `CREDENTIAL_NOT_FOUND`: the active child does not exist or is not owned by this manager. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `CREDENTIAL_LIMIT_REACHED`: the child already holds five active, non-expired credentials. */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description Internal server error. */ - 500: { + /** @description Ramp process successfully started or updated. */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - }; - }; - revokeManagedProfileApiCredential: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Managed child profile ID. */ - profileId: string; - /** @description Child credential ID. */ - credentialId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Credential is revoked; both values are unusable. Repeated revocation also returns this status. */ - 204: { - headers: { - [name: string]: unknown; + /** + * @example { + * "createdAt": "2024-05-16T10:00:00Z", + * "currentPhase": "processing", + * "depositQrCode": "00020126...", + * "from": "stellar", + * "id": "proc_12345", + * "quoteId": "quote_7af7171e-aa42-49a2-80c2-9e18483bad38", + * "to": "pix", + * "type": "off", + * "unsignedTxs": [], + * "updatedAt": "2024-05-16T12:30:00Z" + * } + */ + "application/json": { + anchorFeeFiat: string; + anchorFeeUSD: string; + countryCode?: components["schemas"]["CountryCode"]; + /** + * Format: date-time + * @description Timestamp of when the ramp process was created. + */ + createdAt?: string; + currentPhase?: components["schemas"]["RampPhase"]; + /** @description BR Code for PIX payment, if applicable. */ + depositQrCode?: string | null; + feeCurrency: components["schemas"]["RampCurrency"]; + /** @description The source network or payment method. */ + from?: components["schemas"]["DestinationType"]; + /** @description Unique identifier for the ramp process. */ + id?: string; + inputAmount: string; + inputCurrency: string; + network?: components["schemas"]["Networks"]; + networkFeeFiat: string; + networkFeeUSD: string; + outputAmount: string; + outputCurrency: string; + partnerFeeFiat: string; + partnerFeeUSD: string; + paymentMethod: components["schemas"]["PaymentMethod"]; + processingFeeFiat: string; + processingFeeUSD: string; + /** + * Format: uuid + * @description The quote ID associated with this ramp process. + */ + quoteId?: string; + /** @description The `externalSessionId` is an optional URL parameter that integrators can provide to track ramp transactions within their own systems. This identifier allows you to correlate Vortex transactions with your internal session or transaction tracking. `externalSessionId` url param is named `sessionId` in the Vortex API. */ + sessionId?: string; + status?: components["schemas"]["SimpleStatus"]; + /** @description The destination network or payment method. */ + to?: components["schemas"]["DestinationType"]; + totalFeeFiat: string; + totalFeeUSD: string; + /** @description (BUY-only) A link to a block explorer showing the details for the transaction hash. */ + transactionExplorerLink?: string; + /** @description (BUY-only) The hash of the transaction transferring the expected outputAmount to the wallet address. */ + transactionHash?: string; + /** @description Type of ramp process. */ + type?: components["schemas"]["RampDirection"]; + /** @description Array of unsigned transactions that need to be signed by the user. */ + unsignedTxs?: components["schemas"]["UnsignedTx"][]; + /** + * Format: date-time + * @description Timestamp of the last update to the ramp process. + */ + updatedAt?: string; + vortexFeeFiat: string; + vortexFeeUSD: string; + /** @description The address of the source account for SELL, or the address the destination account for BUY transactions. */ + walletAddress?: string; + }; }; - content?: never; }; - /** @description `MANAGED_PROFILE_INVALID_INPUT`: profileId or credentialId is not a UUID. */ + /** + * @description Bad Request. Possible reasons: + * - Missing required fields (rampId, presignedTxs) + * - Invalid additional data format (if provided, must be an object) + */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Missing, invalid, expired, or revoked manager authentication. */ + /** @description Authentication is required for an owned ramp or managed-profile selection. */ 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description `CREDENTIAL_ACCESS_DENIED`: the manager is inactive. `MANAGED_PROFILE_ACCESS_DENIED`: the authenticated credential belongs directly to a managed child. `CREDENTIAL_MISMATCH` is returned when public and secret headers identify different credentials. */ + /** @description Ramp ownership or managed-profile authorization failed. */ 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; - }; - }; - /** @description `CREDENTIAL_NOT_FOUND`: the active child or credential does not exist, or is not owned by this manager. */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + "application/json": components["schemas"]["ErrorManagedSelectorResponse"]; }; }; - /** @description Internal server error. */ + /** @description Internal Server Error. */ 500: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ManagedProfileErrorResponse"]; + /** + * @example { + * "message": "An unexpected error occurred." + * } + */ + "application/json": Record; }; }; };