# Appmax API > Appmax API documentation: integrate payments (credit card, Pix, boleto, Apple Pay), orders, customers, subscriptions and payment split. Covers the Appmax AppStore application install flow, OAuth2 authentication and webhooks. > Gerado automaticamente a partir de https://docs.appmax.com.br — não editar à mão. --- Source: https://docs.appmax.com.br/en/index.md # Appmax API ## Developer Documentation Integrate payments, orders, and customers into your application with the Appmax API. - [Quickstart](/en/quickstart) - [API Reference](/en/api-reference/introduction) - [Customers](/en/api-reference/customers/criar-atualizar): Create and update customers linked to your application with contact details, addresses, and documents. - [Orders](/en/api-reference/orders/criar-pedido): Create orders with physical or digital products, check status, and manage the full lifecycle. - [Payments](/en/api-reference/payments/cartao-credito): Process payments via credit card, Pix, boleto, or Apple Pay with built-in anti-fraud. - [Recurring](/en/api-reference/subscriptions/criar-assinatura): Create subscriptions with automatic recurring billing for subscription-based business models. - [Refunds](/en/api-reference/refunds/criar-estorno): Request full or partial refunds for payments processed through the platform. - [Webhooks](/en/guides/webhooks): Receive real-time notifications about order and payment status changes. ## App Store The Appmax App Store allows third-party developers to create applications that offer services to merchants, and merchants to add extra functionality to their own stores. Gateway, anti-fraud, and acquiring in one place to deliver high performance with simplicity. [Create your app →](/en/guides/criar-aplicativo) ![Developer integrating the API](/images/programmer_woman.webp) How it works 1 **Authenticate** Obtain a Bearer token with merchant credentials 2 **Create customer** Register the buyer's details 3 **Create order** Add products and link to the customer 4 **Pay** Process the payment with the chosen method Security - Access restricted by key pairs (client_id and client_secret). - Keys issued upon app installation — store them securely. - Temporary tokens keep credentials out of your requests. Next steps - [Quickstart](/en/quickstart): Quick guide to process your first payment. - [Full example](/en/guides/exemplo-integracao): Step-by-step guide with ready-to-copy code. - [Concepts](/en/guides/conceitos): Understand the fundamental platform concepts. --- Source: https://docs.appmax.com.br/en/guides/por-onde-comecar.md # Getting started New to Appmax? This guide helps you understand the integration model and choose the right path for your scenario. ## What is Appmax? Appmax is a payments platform with **gateway, anti-fraud, and acquiring** built in. Integration happens through an App Store: you create an application, merchants install it in their stores, and from there you process payments on their behalf. ## Appstore model vs direct API If you're coming from other gateways (PagSeguro, Mercado Pago, Stripe), the model may feel unfamiliar. Here's the difference: | Traditional gateway | Appmax | |---|---| | You receive credentials directly by email | Credentials are generated through the installation flow | | One API key per account | One pair of credentials **per merchant** who installs your app | | Integrate directly with the API | Create an app in the Appstore → merchant installs → generates credentials → integrate | > **This architecture exists so that **a single app** can serve **multiple merchants** with data isolation and per-store credentials.** > > ## Choose your scenario ### I want to integrate a custom checkout You're a developer building a custom checkout for a merchant (or for yourself). 1. [Create your application](/en/guides/criar-aplicativo) (private if it's for a single merchant) 2. [Install and obtain credentials](/en/guides/instalacao) 3. [Quickstart — first payment](/en/quickstart) 4. [Full integration example](/en/guides/exemplo-integracao) ### I want to build a platform for multiple merchants You're creating a platform (ERP, e-commerce, marketplace) that will be used by multiple merchants. 1. [Create your application](/en/guides/criar-aplicativo) (public) 2. [Installation — understand the 4-step flow](/en/guides/instalacao) 3. [Authentication — understand app vs merchant credentials](/en/guides/autenticacao) 4. [Publishing to production](/en/guides/publicacao-producao) ### I want to add post-purchase upsell 1. [Quickstart](/en/quickstart) 2. [Upsell API](/en/api-reference/orders/upsell) ## Concepts you'll encounter | Concept | What it is | Where to learn | |---------|-----------|----------------| | App vs Merchant credentials | Two credential pairs with different scopes | [Authentication](/en/guides/autenticacao) | | Health check / Validation URL | Endpoint on your system that Appmax calls during installation to receive the `external_id` (UUID) that binds the installation to the store | [Installation](/en/guides/instalacao) · [Validate your URL](/en/guides/validar-url) | | Webhook | Real-time notification of events (order paid, etc) | [Webhooks](/en/guides/webhooks) | | Sandbox vs Production | Two environments with different URLs | [Environments](/en/guides/ambientes) | | Values in cents | All monetary values are integers in cents | [Concepts](/en/guides/conceitos) | ## Need help? - [FAQ](/en/guides/faq) — answers to frequently asked questions - [AI integration](/en/guides/ia) — connect Claude, Cursor, or another MCP agent to accelerate --- Source: https://docs.appmax.com.br/en/quickstart.md # Quickstart ## Before you start Before following this guide, make sure you have: - Access to the [Appmax App Store](https://appstore.appmax.com.br) - An endpoint to receive [webhooks](/en/guides/webhooks) ## Flow overview The complete integration flow with the Appmax API follows these steps: #### 1. Create the app Create your app (public or private) in the [Appmax App Store](https://appstore.appmax.com.br). You will receive an `app_id`, `client_id`, and `client_secret` for the application. See more at [Create app](/en/guides/criar-aplicativo). #### 2. Install and authorize Obtain the app token, generate the authorization hash, and redirect the merchant to authorize the installation. After authorization, generate the merchant credentials. See more at [App installation](/en/guides/instalacao). #### 3. Authenticate with the API With the merchant credentials, obtain a Bearer token to make API calls. ```bash curl --location 'https://auth.appmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=MERCHANT_CLIENT_ID' \ --data-urlencode 'client_secret=MERCHANT_CLIENT_SECRET' ``` Expected response: ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...", "token_type": "Bearer", "expires_in": 3600 } ``` #### 4. Create a customer Register the buyer's details to obtain the `customer_id`. The `ip` field must be collected via [Appmax JS](/en/guides/appmax-js). ```bash curl --request POST \ --url https://api.appmax.com.br/v1/customers \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "first_name": "Junior", "last_name": "Almeida", "email": "junior.almeida@email.com", "phone": "51983655100", "ip": "127.0.0.1" }' ``` Expected response — save the returned `id` (this will be the `customer_id` in subsequent calls): ```json { "data": { "customer": { "id": 1 } } } ``` See more at [Create or update customer](/en/api-reference/customers/criar-atualizar). #### 5. Create an order With the `customer_id`, create the order to obtain the `order_id`. ```bash curl --request POST \ --url https://api.appmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": 1, "products": [ { "sku": "9000010", "name": "Recipe book", "quantity": 1, "unit_value": 12300, "type": "digital" } ] }' ``` Expected response — save the returned `id` (this will be the `order_id` for the payment): ```json { "data": { "order": { "id": 1, "status": "pendente" } } } ``` See more at [Create an order](/en/api-reference/orders/criar-pedido). #### 6. Process the payment Process the payment using one of the available methods: credit card, Pix, boleto, or Apple Pay. See more at [Credit card payment](/en/api-reference/payments/cartao-credito), [Pix](/en/api-reference/payments/pix), or [Boleto](/en/api-reference/payments/boleto). ## Next steps - [Business concepts](/en/guides/conceitos): Understand the fundamental Appmax platform concepts. - [Environments](/en/guides/ambientes): Learn about sandbox and production environments. --- Source: https://docs.appmax.com.br/en/guides/conceitos.md # Business concepts ## What is the Appmax API? The Appmax API allows you to create and manage customers, orders, payments, and other essential resources for running a store. It is designed so that developers can integrate efficiently, ensuring security and flexibility through the creation of an application. ## Application types ##### Private application Ideal for developers who want to integrate and process sales exclusively in their own environments. This type of app is not publicly available in the Appmax directory and can only be accessed through a link shared by the developer. It allows partners to create custom solutions without exposing them to the public, ensuring a more secure, exclusive, and optimized experience. ##### Public application An application that will be available to the entire Appmax customer base. With it, developers can offer their solutions broadly, allowing any merchant to use the app directly through the platform. | Characteristic | Public application | Private application | | -------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------ | | AppStore visibility | Visible to all users | Not listed; access only via exclusive link | | Access method | Available on the official Appmax App Store page | Access only through a sharing link | | Installation | Direct installation from the Appmax App Store | Installed through a link shared by the developer | ## AppStore benefits - Customization of the store experience. - Integration with external services. - Adding features directly to the admin panel. ## Security - API access is restricted by key pairs. - Keys are issued during app installation and must be stored securely. - The use of temporary tokens ensures that credentials are not exposed in requests. ## Mandatory technical requirements | Requirement | Description | | ---------------------- | ----------------------------------------------------------------------------- | | Authentication | Follow the authentication and authorization flow described in the documentation | | Appmax JS security | Implement Appmax.js on the front end to protect sensitive card data | | Customer creation | Register the buyer's data before creating orders | | Order creation | Register the purchase linked to a customer | | Payments | Process payments via credit card, Pix, boleto, or Apple Pay | | Installment calculation | Query the installments endpoint to ensure correct amounts | | Payment refunds | Implement refunds via API or dashboard | | Tracking code | Update orders with tracking codes to enable withdrawal | | Webhooks | Receive event notifications in real time | ## Optional features - **Recurring billing:** automatic periodic charges (beta). - **API payment link:** URL for quick payment without a full cart (beta). - **Upsell:** complementary sales strategy linked to existing orders. - **AI-powered sales recovery:** abandoned cart recovery using artificial intelligence (beta). --- Source: https://docs.appmax.com.br/en/guides/ambientes.md # Sandbox and production environments Appmax provides two distinct environments for integration. ## Sandbox (test environment) Use the sandbox to test your integration before going to production and before being approved in the homologation process. | Service | URL | | -------------- | ------------------------------------------------ | | Authentication | `https://auth.sandboxappmax.com.br` | | API | `https://api.sandboxappmax.com.br` | | Authorization | `https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH` | ## Production Use production to transact with real customers. | Service | URL | | -------------- | ------------------------------------------------ | | Authentication | `https://auth.appmax.com.br` | | API | `https://api.appmax.com.br` | | Authorization | `https://admin.appmax.com.br/appstore/integration/HASH` | ## Summary of differences > **- In sandbox, URLs use the `sandboxappmax` subdomain and the redirect goes to **BC Sandbox**.** > > - In production, URLs use the standard `appmax` domain and the redirect goes to **Appmax Admin**. To test credit card payments in the sandbox, use the [test cards](/en/api-reference/payments/cartao-credito#cartoes-de-teste). > **Sandbox instability** > > The sandbox environment may experience slowness or temporary downtime. If you get timeout (504) or 503 errors, wait a few minutes and try again. These issues **do not affect** the production environment. If it persists for more than 30 minutes, contact support. ## How to migrate to production Finished testing in sandbox? See the full go-live guide with technical checklist, homologation process, and what changes when migrating: - [Publishing to production](/en/guides/publicacao-producao) --- Source: https://docs.appmax.com.br/en/guides/status-pedidos.md # Order statuses Throughout the lifecycle of an order, it goes through different statuses. Below is the full list with a description of each status, representing specific stages in the order process. > **The statuses listed here are the values returned by the API. In the Appmax dashboard, they may have different labels to improve the merchant experience.** > > | Status | Dashboard label | Description | | ----------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pendente` | Payment pending | Orders that are not yet paid: card not authorized, Pix not paid, boleto not settled, or expired boletos that were never paid. | | `aprovado` | Payment approved | Payment confirmed. From this status onward, the amounts are made available in the merchant's account. | | `autorizado` | Anti-fraud analysis | Credit card order authorized by the issuing bank with available balance. Anti-fraud analysis begins before moving to approved. | | `cancelado` | Not authorized | Pix with expired QR Code, card not authorized by the bank or insufficient balance. Also includes orders created via the dashboard without a payment transaction.| | `estornado` | Refunded | Order with an approved refund request. The amount is debited from the merchant's account. | | `recusado_por_risco` | Declined by risk | Transactions considered high risk that do not pass anti-fraud analysis. The order is refunded and receives this status. | | `integrado` | Payment approved | Final status for an approved order, after integration validations. Ready for shipment to the buyer (physical products). | | `pendente_integracao` | Payment approved | Order is paid, but there is a pending issue with the integration (incorrect information in the registration or product). | | `pendente_integracao_em_analise` | Payment approved | Approved order that received a refund request before being integrated. Manual review is performed. | | `chargeback_em_tratativa` | Chargeback | The bank has flagged a chargeback. The order is refunded and receives this status. | | `chargeback_em_disputa` | Chargeback | Appmax has initiated a dispute to recover the chargeback. | | `chargeback_perdido` | Chargeback | There are no further ways to recover the chargeback. | | `chargeback_vencido` | Chargeback recovered | Chargeback dispute won. The merchant receives the credit in their balance. | --- Source: https://docs.appmax.com.br/en/guides/criar-aplicativo.md # Create an application This guide covers the application creation in the Appmax panel: the public vs private decision, what you need before you start, and the three form steps. For the reference of the **identifiers and URLs** the app exposes after creation (App UUID, App Numerical ID, Host, validation URL, webhook URL), see [App identifiers and URLs](/en/guides/identificadores-do-app). ## Public or private? The first decision is the application type. It cannot be changed later — choose based on the audience that will use it. | Aspect | Public | Private | |--------|--------|---------| | Visibility | Listed in the Appstore for any merchant | Visible only to merchants you invite | | Ideal for | Platforms, ERPs, integrators serving multiple merchants | Internal integration or single client | | Merchants | Unlimited — any merchant can install | Only merchants authorized by you | | Review | Required by the Appmax team before publishing | Not required | > **When to use public?** > > If you're building a platform that will be used by multiple merchants (e.g., ERP, gateway, e-commerce), choose **public**. This allows any merchant in the Appstore to install your app without an invitation. ## Requirements ### Business - **Active CNPJ** — required to create a developer account on the Appstore. ### Technical Before submitting the form, you need two public URLs on your system: | URL | When it's called | Reference | | --- | --- | --- | | **Validation URL** | Appmax makes a server-to-server `POST` during the last step of installation (`/app/client/generate`). Your URL must respond `HTTP 200` with `{ "external_id": "" }`. | [Installation flow — health check](/en/guides/instalacao#health-check) | | **Webhook URL** | Appmax sends events (order created, paid, refunded, etc.) during merchant operation. | [Webhooks](/en/guides/webhooks) | > **Test the validation URL before creating the app** > > The whole installation aborts with `500` if the validation URL doesn't answer correctly. Use the [interactive tool](/en/guides/validar-url) to verify the contract (HTTP 200 + unique `external_id` UUID per call) **before** submitting the form. ## Creation steps The form in the panel has three steps. ### 1. About the application Provide the basic details: - **Application name** — the name displayed to merchants (up to 30 characters). - **Support email** — address for users to reach out. It's also where Appmax sends communications about review status — use a monitored mailbox. - **Application description** — purpose, benefits, and services offered (up to 100 characters). - **Billing model** — choose between: - **Billing via external platform** — billing is handled on your own platform, with no Appmax involvement. - **Billing via Appmax** — a fixed monthly amount charged to the merchant, deducted from the partner balance. ### 2. Application images Upload the image that will serve as the application avatar. > **Required image specification** > > - Square format of **1200px x 1200px** > - **PNG or JPG** > - No rounded corners ### 3. Application settings Choose the **webhook events** your application will receive. The Appstore exposes 29 events across 4 categories: - **Order** — creation, payment, refund, chargeback, and variations (Pix, boleto, upsell). - **Customer** — creation, contact, and interest. - **Payment** — late authorization and non-authorization. - **Subscription** — creation, cancellation, recurring charge. Select only the events your integration actually processes — subscribing to events you ignore increases load without benefit and complicates auditing. The full list, payloads, and per-event examples are in [Webhooks](/en/guides/webhooks). > **You can change the event selection at any time from the panel, without having to recreate the application.** > > ## After submission When you complete the three steps, a modal opens with two options: - **Submit for review** — sends the application for review by the Appmax team. Required for **public** apps. - **Test** — the team contacts you to provide access to the staging environment. Click **"View Application"** and then **"Develop"** to see the identifiers and URLs generated — those values are the foundation for implementing the installation flow. See [App identifiers and URLs](/en/guides/identificadores-do-app) for the full per-field reference. ## Next steps - [App identifiers and URLs](/en/guides/identificadores-do-app) — App UUID vs Numerical ID, configured URLs. - [Validate installation URL](/en/guides/validar-url) — test the validation URL before homologation. - [Installation flow](/en/guides/instalacao) — implement `/app/authorize` → redirect → `/app/client/generate`. - [Publishing to production](/en/guides/publicacao-producao) — go-live checklist and homologation process. --- Source: https://docs.appmax.com.br/en/guides/identificadores-do-app.md # App identifiers and URLs After [creating the application](/en/guides/criar-aplicativo), the panel exposes a set of identifiers and URLs under **View Application → Develop**. This page is the per-field reference, and the point in the flow where each one is consumed. ## Identifiers Your application has **two different identifiers** in the panel. Mixing them up is the most common cause of `422 Unprocessable Entity` in the installation flow. | Identifier | Format | Where to use | | --- | --- | --- | | **App UUID** | `f9e8d7c6-b5a4-3210-fedc-ba0987654321` | In **every** API endpoint (including `POST /app/authorize`), except where a field is explicitly marked as "Numerical ID". | | **App Numerical ID** | `699` (integer) | Only in fields explicitly marked as "Numerical ID" (rare). | > **Use the UUID, not the Numerical ID** > > In `POST /app/authorize` and `POST /app/client/generate`, always send the **App UUID**. If you send the Numerical ID, you receive `422 Unprocessable Entity`. > > If you're getting `422` on `authorize`, first check which of the two IDs you're sending. Full diagnostics at [Installation troubleshooting](/en/guides/instalacao#troubleshooting). ## Configured URLs The panel shows four URLs tied to the application. Each is used at a different point of the lifecycle. ### Host Base URL of your system. Used by Appmax as the destination of merchant event webhooks. - **Caller:** Appmax - **Receiver:** your system - **When:** throughout merchant operation, whenever a configured event occurs Envelope details and the 29 available events in [Webhooks](/en/guides/webhooks). ### System URL Public URL of your system, **made available to the merchant** for access after installation. It is not used by Appmax in server-to-server calls — it's just the link displayed on the merchant panel. - **Caller:** merchant (manual click in the panel) - **Receiver:** your system - **When:** when the merchant wants to access your app's panel/settings ### Validation URL Endpoint on your system that Appmax calls during `POST /app/client/generate` to register the installation — this is the **health check**. - **Caller:** Appmax (server-to-server) - **Receiver:** your system - **When:** once per installation, during the last step of the flow **Contract:** | Direction | Payload | | --- | --- | | Appmax → your system (`POST`) | `{ app_id, client_id?, client_secret?, client_key?, external_key? }` — only `app_id` (Numerical ID, numeric) is guaranteed; all other fields are optional | | Your system → Appmax (`200 OK`) | `{ "external_id": "", "alias"?: "" }` | The `external_id` you return is **generated by your system** and becomes the current identifier of that installation — it's the same value later consumed by Appmax JS at checkout as the `external-id` header. Every new installation requires a new value from Appmax, and the previous one stops being valid. > **The installation fails if the validation URL doesn't answer correctly** > > If your validation URL isn't public, responds with a status other than `200`, or doesn't return a valid `external_id` UUID, `POST /app/client/generate` aborts with `500` and no merchant credentials are issued. Test it first at [Validate installation URL](/en/guides/validar-url). Full contract details, payload examples, and error handling in [Installation flow — health check](/en/guides/instalacao#health-check). `external_id` lifecycle (generation in the health check, usage in the front via CDN) in [`external-id`](/en/guides/external-id). ### Webhook URL Where Appmax sends notifications for events selected at step 3 of app creation (order created, paid, refunded, etc.). - **Caller:** Appmax - **Receiver:** your system - **When:** whenever a subscribed event occurs on the merchant's platform Envelope, event list, and per-type examples in [Webhooks](/en/guides/webhooks). ## Summary — which URL is called for what | URL | Direction | Triggered by | | --- | --- | --- | | Host | Appmax → you | Webhook event | | System URL | Merchant → you | Manual access via panel | | Validation URL | Appmax → you | Health check on `POST /app/client/generate` | | Webhook URL | Appmax → you | Event subscribed at app creation | ## Next steps - [Validate installation URL](/en/guides/validar-url) — interactive tool to check the validation URL contract. - [Installation flow](/en/guides/instalacao) — use the App UUID and validation URL in production. - [Webhooks](/en/guides/webhooks) — payloads and per-event examples delivered to Host / Webhook URL. - [`external-id`](/en/guides/external-id) — lifecycle of the identifier returned by the validation URL. --- Source: https://docs.appmax.com.br/en/guides/validar-url.md # Validate your validation URL Before submitting your application to homologation or triggering the first real installation, make sure your **validation URL** is answering Appmax's health check correctly. This tool fires a synthetic `POST` — with the exact payload Appmax uses on `POST /app/client/generate` — and runs every assertion of the contract. > **What is the health check?** > > During the last step of [app installation](/en/guides/instalacao#health-check), Appmax fires a server-to-server `POST` against the validation URL configured on the panel. Your URL must answer **HTTP 200 + JSON with an `external_id` UUID** for the installation to be completed. Full contract at [App installation](/en/guides/instalacao#health-check) and [`external-id`](/en/guides/external-id). > Ferramenta interativa disponível na versão web desta página. ## What is validated The tool fires **two requests in sequence** with distinct `external_key` values. That allows comparing the `external_id` returned in each one and catching handlers that respond with a hardcoded UUID — a common bug that silently breaks new installations. | Assertion | Criterion | | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | **URL reachable** | The URL responds within 8 seconds without DNS, connection-refused or TLS errors. | | **HTTP 200** | Status code is exactly `200`. When another code appears, the detail tries to classify the cause (anti-bot WAF, auth, 5xx). | | **Returns `external_id` as valid UUID** | Body parses as JSON and the `external_id` field is present as a string in the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (8-4-4-4-12 hex) format. | | **`external_id` changes per request (not hardcoded)** | The second call (with a different `external_key`) returns a **distinct** UUID. If they match, your handler is hardcoded — each installation needs a unique `external_id`. | > **Why these assertions?** > > The real contract is simple: Appmax needs a **200** with a UUID `external_id` that is **unique per installation**. The tool runs the scenario twice and shows both UUIDs side by side so you can compare. ## The payload sent The tool fires the same shape Appmax uses for the real health check: ```json { "app_id": "DEMO_APP_ID", "client_id": "DEMO_CLIENT_ID", "client_secret": "DEMO_CLIENT_SECRET", "client_key": "DEMO_EXTERNAL_KEY", "external_key": "DEMO_EXTERNAL_KEY" } ``` The values are obvious placeholders so they show up in your server log as a **test call**. In production, Appmax sends the real merchant `client_id`/`client_secret` — your handler needs to accept any payload in this shape. ## Expected response from your handler ```json { "external_id": "37bb0791-ee0b-457d-860c-186e32978bcd", "alias": "My Store" } ``` > **Persist the `external_id`** > > The UUID you return here isn't disposable — it becomes the current identifier of the installation and is required on every CDN call from the front-end. Every new installation requires a new value from Appmax, and the previous one stops being valid. See [`external-id`](/en/guides/external-id) for the full life cycle. ## Limitations of this tool - **No calls to private IPs**: the tool refuses URLs that resolve to `localhost`, `10.x`, `192.168.x`, `172.16-31.x` or link-local — use [ngrok](https://ngrok.com) / [beeceptor](https://beeceptor.com) to expose a local endpoint. - **No following redirects**: a `302` is treated as failure — Appmax's real health check also doesn't follow redirects. - **8s timeout**: if your URL doesn't respond within that window, you get `Timeout` (Appmax's production timeout is larger, but delays > 5s are considered an operational issue). - **HTTPS or HTTP**: both accepted here, but in production Appmax **only** calls HTTPS URLs — refuse HTTP on your final domain. ## When to use - Before submitting the app for homologation. - After updating the health-check handler. - When an installation fails with a `500` on `POST /app/client/generate` — most likely the validation URL didn't answer as expected. See [Troubleshooting](/en/guides/instalacao#troubleshooting). --- Source: https://docs.appmax.com.br/en/guides/implementar-url-validacao.md # Implement the validation URL > **Before you start — you are implementing, not calling** > > The **validation URL** is an endpoint **you create on your own server**. Appmax **calls that endpoint** during installation (`POST /app/client/generate`) — you **do not call anything from Appmax here**. > > If you are trying to find out which Appmax endpoint to invoke in order to "validate the URL", you are on the wrong track. What actually happens is: > > - Appmax sends a `POST` to your URL with a known payload. > - You respond with `HTTP 200` and a JSON body containing `external_id` (a UUID v4 generated by you). > > To **test** the endpoint once implemented, use the interactive tool in [Validate installation URL](/en/guides/validar-url). This guide gives you a ready-to-run handler in Go, Node.js and PHP. Copy it, adjust the route and register the public URL in the application panel. ## Contract Appmax makes **a single** server-to-server call against the validation URL registered in the panel, as part of processing [`POST /app/client/generate`](/en/guides/instalacao#health-check). Your URL must meet the contract below exactly. ### Request — what Appmax sends you | Item | Value | | ---- | ----- | | Method | `POST` | | Content-Type | `application/json` | | Body | JSON with the fields below — **only `app_id` is guaranteed**; the rest are optional | ```json { "app_id": 123, "client_id": "MERCHANT_CLIENT_ID", "client_secret": "MERCHANT_CLIENT_SECRET", "client_key": "EXTERNAL_KEY", "external_key": "EXTERNAL_KEY" } ``` | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `app_id` | integer | Yes | The application's **App Numerical ID** (numeric ID, e.g. `123`) — **not the UUID**. The only field always present. | | `client_id` | string | No | Client ID generated for the merchant in this installation. May not be sent. | | `client_secret` | string | No | Client Secret generated for the merchant in this installation. May not be sent. | | `client_key` | string | No | Same value as `external_key` (kept for backwards compatibility). May not be sent. | | `external_key` | string | No | Key provided by the merchant when the installation was created (`store_id`, `merchant_id`, etc.). May not be sent. | > **Your handler must only validate the presence of `app_id` — the remaining fields are **optional** and their absence must not be treated as an error. Remember that `app_id` arrives as the **Numerical ID** (numeric), not as a UUID.** > > ### Response — what you must return | Item | Value | | ---- | ----- | | HTTP status | `200` (exactly — `201`, `204` and `2xx` in general do **not** count) | | Content-Type | `application/json` | | Body | JSON with `external_id` (required) and `alias` (optional) | ```json { "external_id": "37bb0791-ee0b-457d-860c-186e32978bcd", "alias": "My Store" } ``` | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `external_id` | string (UUID v1-v5) | Yes | A UUID **generated by you**, **unique per installation**. Appmax stores this value and later returns it as the `external-id` header in front-end calls through the CDN. | | `alias` | string | No | Display name of the store in Appmax. If omitted, Appmax uses the default name. | > **Failures that abort the installation** > > If your URL responds with a status other than `200`, without parseable JSON, or without an `external_id` in a valid UUID format, the `POST /app/client/generate` step returns `500` and **no merchant credentials are issued**. Details in [Installation — health check](/en/guides/instalacao#health-check). ## Implementation The examples below are minimal handlers that fulfil the contract. In production, before responding `200`, persist the triple `external_key` → `client_id`/`client_secret` → `external_id` in your database (see the [Persistence](#persistence) section). ##### Go Minimal dependencies — just `net/http` from the stdlib plus `github.com/google/uuid` to generate a UUID v4. ```bash go mod init my-app go get github.com/google/uuid ``` ```go package main import ( "encoding/json" "log" "net/http" "github.com/google/uuid" ) type request struct { AppID int64 `json:"app_id"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` ClientKey string `json:"client_key"` ExternalKey string `json:"external_key"` } type response struct { ExternalID string `json:"external_id"` Alias string `json:"alias,omitempty"` } func validationHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", "POST") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req request if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } if req.AppID == 0 { http.Error(w, "invalid payload", http.StatusBadRequest) return } // In production: persist req.ClientID, req.ClientSecret and externalID // linked to req.ExternalKey in your database before responding. externalID := uuid.New().String() w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(response{ExternalID: externalID}) } func main() { mux := http.NewServeMux() mux.HandleFunc("/appmax/validate", validationHandler) log.Println("listening on :8080 — POST /appmax/validate") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatal(err) } } ``` ##### Node.js Minimal dependencies — Express plus `crypto.randomUUID` from the stdlib. ```bash npm init -y npm install express ``` ```js import express from 'express' import crypto from 'node:crypto' const app = express() app.use(express.json()) app.post('/appmax/validate', (req, res) => { const { app_id, client_id, client_secret, client_key, external_key } = req.body ?? {} if (!app_id) { return res.status(400).json({ error: 'invalid payload' }) } // In production: persist client_id, client_secret and externalId // linked to external_key in your database before responding. const externalId = crypto.randomUUID() res.status(200).json({ external_id: externalId }) }) app.listen(3000, () => { console.log('listening on :3000 — POST /appmax/validate') }) ``` ##### PHP No framework — `json_decode` straight from `php://input` and a UUID v4 generated with `random_bytes`. ```php $externalId], JSON_THROW_ON_ERROR); ``` ## Persistence The `external_id` is **not disposable**. It comes back as the `external-id` header in every front-end call through the CDN (tokenization, Apple Pay). **Generate a new UUID on every health check request** — Appmax rejects repeated values: if the `external_id` it receives already exists in the database, it is discarded and automatically replaced by the installation's `client_id`. Persist it the moment you generate it and, if the health check runs again for the same store, always keep the **latest** value returned and discard the previous one. The minimum persistence structure is: | Column | Source | Use | | ------ | ------ | --- | | `external_key` | received in the payload | Links the row to the platform's merchant. | | `client_id` | received in the payload | Credential used later in server-to-server calls. | | `client_secret` | received in the payload | Credential used later in server-to-server calls. | | `external_id` | **generated by you** | Returned in the response and reused as the `external-id` header on the front end. | | `alias` | optional | Display name. | For a complete service example (callback + health check + Postgres) that wires this end to end, see [Automate credential creation](/en/guides/automatizar-criacao-credenciais). For the `external_id` lifecycle after installation, see [`external-id`](/en/guides/external-id). ## Register the URL in the panel Once your handler is live at a public URL, open **View Application → Develop** and fill the **Validation URL** field with the full URL (including the endpoint path, e.g. `https://onboarding.myapp.com/appmax/validate`). Details of the other panel fields in [Application identifiers and URLs](/en/guides/identificadores-do-app). Before submitting for approval or starting a real installation, validate your handler in [Validate installation URL](/en/guides/validar-url) — the tool makes two calls with different `external_key` values and shows both UUIDs side by side, to make sure you are not returning a hardcoded value. ## Common limitations - **`localhost` does not work** — Appmax cannot reach private networks. In development, use [ngrok](https://ngrok.com), [beeceptor](https://beeceptor.com) or similar and register the generated public URL. - **HTTPS is mandatory in production** — in production Appmax only calls `https://` URLs. In sandbox the validation tool accepts HTTP, but avoid that setup. - **Redirects are not followed** — the health check does not follow `301`/`302`. If your URL redirects (e.g. trailing slash, canonical subdomain), respond at the final destination and register that URL in the panel. - **Short timeout** — respond in under 5 seconds. Heavy logic (third-party synchronization, welcome emails) must go to an asynchronous queue after the `200`. - **The HTTP status must be exactly `200`** — `201`, `202` and `204` fail the installation. - **`external_id` must be unique per installation** — do not return a hardcoded UUID. The validation tool detects this and marks it as a failure. ## See also - [App installation — health check](/en/guides/instalacao#health-check) — the full flow where the URL is called. - [Validate installation URL](/en/guides/validar-url) — interactive tool to test the contract. - [`external-id`](/en/guides/external-id) — lifecycle of the UUID you return here. - [Automate credential creation](/en/guides/automatizar-criacao-credenciais) — complete example with callback + Postgres persistence. - [Application identifiers and URLs](/en/guides/identificadores-do-app) — reference for the panel fields. --- Source: https://docs.appmax.com.br/en/guides/publicacao-producao.md # Publishing to production This guide covers the complete process to migrate your integration from sandbox to production — from the technical checklist to the homologation flow and activation. ## Overview Publishing to production involves three parts: 1. **Technical checklist** — validate that your integration is complete and resilient in sandbox. 2. **Homologation** — validation by the Appmax team that your integration is working correctly. 3. **Production activation** — app publication and issuance of production credentials. > **The homologation process is currently conducted **via email** by the Appmax team. This guide shows how to prepare your integration so that homologation is fast and without rework.** > > > **Only public apps go through homologation** > > **Private** apps don't require homologation — they're published directly after the technical checklist. The homologation flow described on this page applies only to **public** apps. ## Prerequisites Before requesting publication to production, confirm your sandbox integration meets every item below. ### Installation flow - [ ] The complete 4-step flow (`/oauth2/token` → `/app/authorize` → redirect → `/app/client/generate`) is implemented and working in sandbox. - [ ] Your **validation URL** (health check) is publicly reachable, returns **HTTP 200** and an `external_id` in valid UUID format. - [ ] Your validation URL responds in under **5 seconds** (Appmax timeout). - [ ] You store the `external_id` of each installation in your database. - [ ] You store merchant credentials (`client_id` and `client_secret`) securely per installation. ### Webhooks - [ ] Your webhook URL is publicly reachable and returns **HTTP 200** for all expected events. - [ ] You process webhooks **idempotently** (redeliveries are possible). - [ ] You respond to the webhook in under **5 seconds** — heavy processing must be async. - [ ] You handle the events relevant to your use case (payment, refund, cancellation, etc.). ### API operations - [ ] You correctly differentiate **app credentials** (installation) and **merchant credentials** (transactions on `/v1/*`). - [ ] You handle token expiration (1 hour) by requesting a new one when needed. - [ ] You respect the [rate limit](/guides/rate-limit) (burst 50/s, sustained 5/s). - [ ] You handle key HTTP status codes (401, 422, 429, 5xx) with proper retry and backoff. - [ ] Monetary values are always sent as **integers in cents** (never float). ### Security - [ ] URLs (host, validation, webhook) are using **HTTPS** with a valid certificate. - [ ] Credentials (`client_secret`) are **not** versioned in code. - [ ] Logs do **not** expose credentials, tokens, or sensitive customer data (CPF, card). ### Sandbox testing - [ ] Full installation tested end-to-end in sandbox. - [ ] At least one order created and paid in sandbox (use the [test cards](/en/api-reference/payments/cartao-credito#cartoes-de-teste)). - [ ] Webhooks received and processed in sandbox for the main events. - [ ] Error scenarios tested (declined card, invalid hash, expired token). ## Requesting publication With every item above validated, email **integracoes@appmax.com.br** requesting homologation and publication of your app. Including the information below speeds up the process. ### Information to include in the email ``` Subject: Request for production publication - [App name] Hi Appmax team, We have completed sandbox testing and would like to publish our application to production. **Application info** - App name: [...] - App UUID: [...] - App Numerical ID: [...] - Type: [public / private] - Category: [...] **Production URLs** - Host: https://[...] - Validation URL (health check): https://[...] - Webhook URL: https://[...] - Callback URL (url_callback): https://[...] **Technical checklist** - [x] Installation flow complete and tested in sandbox - [x] Validation URL responding HTTP 200 with external_id (UUID) in < 5s - [x] Webhook URL responding HTTP 200 in < 5s - [x] Integration idempotent and resilient to retries - [x] Payment, webhook, and error tests completed in sandbox **Use case** [Briefly describe what the app does and who the merchants using it are.] ``` > **The more information you provide upfront, the faster the turnaround. Avoid incremental submissions like "I'll send URLs later" — the team needs everything to start homologating.** > > ## Homologation The Appmax team will: 1. **Review** the submitted information and your app's behavior in sandbox. 2. **Test** the installation flow and transactions manually. 3. **Validate** that the validation URL, webhooks, and credentials are operating correctly. 4. **Publish** the app once everything is approved. During homologation, the team may request adjustments (e.g., improve error handling, adjust timeout). Keep the sandbox environment available until final approval. ### Test scenarios evaluated The list below summarizes the essential scenarios evaluated during homologation. Testing them in sandbox beforehand speeds up the process — other tests may be performed, but checking these first improves accuracy. **General validations** - [ ] App logo - [ ] App description - [ ] Support email - [ ] App installation **Purchase and refund — test with both an individual (CPF) and a business (CNPJ) customer** | Scenario | Card with interest | Card without interest | Pix | Boleto | | --- | :-: | :-: | :-: | :-: | | Purchase | ☐ | ☐ | ☐ | ☐ | | Full refund | ☐ | ☐ | ☐ | ☐ | | Partial refund | ☐ | ☐ | ☐ | ☐ | **Other scenarios** - [ ] Soft descriptor on card purchases - [ ] Tracking code integration - [ ] IP recorded on orders - [ ] Purchase with discount coupon - [ ] Purchase with shipping + interest - [ ] Purchase with shipping + discount coupon - [ ] Purchase with more than one different product in the cart - [ ] Purchase with multiple units of the same product - [ ] Order status updates > **If your app isn't focused on payment processing through Appmax, the scenarios above may be disregarded — the review follows other criteria.** > > ## What changes in production | Item | Sandbox | Production | | ---- | ------- | ---------- | | Authentication | `https://auth.sandboxappmax.com.br` | `https://auth.appmax.com.br` | | API | `https://api.sandboxappmax.com.br` | `https://api.appmax.com.br` | | Authorization redirect | `https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH` | `https://admin.appmax.com.br/appstore/integration/HASH` | | Credentials | Received at the start of testing | Issued after homologation | | Cards accepted | Only [test cards](/en/api-reference/payments/cartao-credito#cartoes-de-teste) | Real cards | | Pix/boleto | Simulated | Real, with effective charge | | Webhooks | Sent to the sandbox URL | Sent to the production URL | > **Update **every URL** of your integration when migrating to production — not just the API one. Authentication, redirect, and panel-configured URLs (host, validation, webhook) also change.** > > ## After publication After your app is published, we recommend: - [ ] Setting up **monitoring** on validation and webhook URLs (uptime, latency, errors). - [ ] Setting up **alerts** for health check failures and 5xx spikes. - [ ] Tracking the **webhook delivery rate** in the first days. - [ ] Keeping credentials in a **secret manager** (AWS Secrets Manager, Vault, etc.), not plain environment variables. - [ ] Preparing a runbook for **credential rotation** in case of leaks. ## Troubleshooting ### "I sent the homologation email and got no reply" Check: 1. That the email was sent to **integracoes@appmax.com.br**. 2. That it did not land in your inbox's spam. 3. That you included all checklist information (apps with incomplete data stay in queue). If there's still no reply, follow up **on the original email thread** (do not open a new thread) and include a summary of what has already been validated. For general questions during integration (outside the homologation process itself), use **desenvolvedores@appmax.com.br**. ### "I'm getting 401 in production with credentials that worked in sandbox" Sandbox credentials **do not work in production**. After homologation, you'll receive **new credentials** specific to production. Replace in every key in your system: - App `client_id` and `client_secret` - Merchant `client_id` and `client_secret` (will be regenerated on the first production installation) ### "My app was published, but the first real installation failed" Most common behaviors: - The production validation URL is not publicly reachable. - The production webhook URL returns error for real events (issue that didn't appear in sandbox due to lower volume). Check logs, firewall, and IP rules. See [Installation troubleshooting](/en/guides/instalacao#troubleshooting) for details. ### "I can no longer access the old credentials" Production credentials are issued **once** after homologation. Store securely. If you lose them: 1. Use the Appmax panel to request regeneration (may invalidate active integrations). 2. Contact **desenvolvedores@appmax.com.br** for guidance. ## Next steps - [Monitor rate limit](/en/guides/rate-limit) — set up alerts before hitting limits. - [Understand order statuses](/en/guides/status-pedidos) — to correlate events in production. - [Review webhooks](/en/guides/webhooks) — ensure every relevant event is being consumed. --- Source: https://docs.appmax.com.br/en/guides/instalacao.md # App installation ## Flow overview The diagram below shows every call involved in the installation, including the **server-to-server call** that Appmax makes to your validation URL during the health check. ```mermaid sequenceDiagram participant Dev as Your backend participant Auth as auth.appmax.com.br participant API as api.appmax.com.br participant Merchant as Merchant panel participant Valid as Your validation URL Note over Dev,Valid: 1. Obtain the app token Dev->>Auth: POST /oauth2/token
(APP credentials) Auth-->>Dev: access_token (app) Note over Dev,Valid: 2. Authorize the installation Dev->>API: POST /app/authorize
(app_id, external_key, url_callback) API-->>Dev: authorization hash Note over Dev,Valid: 3. Redirect the merchant Dev->>Merchant: redirect with HASH Merchant-->>Dev: callback on url_callback Note over Dev,Valid: 4. Generate credentials (+ health check) Dev->>API: POST /app/client/generate
(hash) activate API API->>Valid: POST (app_id, external_key) Valid-->>API: HTTP 200 + {external_id, alias?} Note over API,Valid: Server-to-server.
Your URL must be public
and return 200. API-->>Dev: client_id, client_secret (merchant) deactivate API Note over Dev,Valid: 5. Operate on the API Dev->>Auth: POST /oauth2/token
(MERCHANT credentials) Auth-->>Dev: access_token (merchant) Dev->>API: POST /v1/customers, /v1/orders, ... ``` > **Step 4 combines two actions in a single request:** > > 1. You call `POST /app/client/generate` with the authorized hash. > 2. **While processing that call**, Appmax makes a server-to-server POST to the validation URL you configured. > 3. Your validation URL must respond with HTTP 200 and an `external_id` (UUID) — only then will Appmax return the merchant credentials. > > The `external_id` you return here **is not throwaway** — it becomes the current identifier for that store and is used as the `external-id` header on every CDN call from the front-end. Every new installation requires a new value from Appmax, and the previous one stops being valid. See [`external-id`](/en/guides/external-id) to understand where this value is consumed later. ## Installation flow #### 1. Obtain the app token Before making any request, obtain the access token using the app credentials. **Endpoint:** `POST https://auth.appmax.com.br/oauth2/token` ```bash curl --location 'https://auth.appmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=CLIENT_ID' \ --data-urlencode 'client_secret=CLIENT_SECRET' ``` **Response:** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...", "token_type": "Bearer", "expires_in": 3600 } ``` #### 2. Authorize the installation With the app access token, generate an authorization hash to redirect the merchant. **Endpoint:** `POST https://api.appmax.com.br/app/authorize` ```bash curl --location 'https://api.appmax.com.br/app/authorize' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_TOKEN' \ --data '{ "app_id": "APP_ID", "external_key": "EXTERNAL_KEY", "url_callback": "URL_CALLBACK", "domain_name": "subdomain.yourdomain.com" }' ``` **Response:** ```json { "data": { "token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554" } } ``` Request parameters: - `app_id` (string, obrigatório): **App UUID** of the application. Use the UUID, not the Numerical ID (see warning below). - `external_key` (string, obrigatório): Key provided by the partner platform to identify the origin of the installation (e.g., `store_id`, `merchant_id`). - `url_callback` (string, obrigatório): URL where the user will be redirected after authorization. - `domain_name` (string): The store's domain (subdomain + domain, e.g. `mystore.myintegration.com`). To send more than one domain in the same installation, use `domain_names` (array) instead of `domain_name`. > **`domain_name` is essential if you're going to use Apple Pay** > > Not required for the installation itself, but it's **essential** if the store will process Apple Pay payments: it's from this domain, informed here, that Appmax registers the domain with Apple — part of enabling Apple Pay on the customer's device. Without it, the Apple Pay button won't work for that store, even if the rest of the integration is correct. You still need to publish the `.well-known` file on the domain — see [Domain configuration for Apple Pay](/en/api-reference/payments/apple-pay-dominio) and [Apple Pay payment](/en/api-reference/payments/apple-pay). > **App UUID vs App Numerical ID** > > Your application has **two identifiers** in the panel: > > - **App UUID** — e.g., `8f2c1d3e-5a4b-4c7d-9e1f-2a3b4c5d6e7f` > - **App Numerical ID** — e.g., `699` > > Use the **App UUID** in all endpoints of the official documentation (including this `POST /app/authorize`), except in `POST /app/client/generate` — or whenever a field is explicitly marked as "Numerical ID". > > Confusing the two is one of the most common causes of `422 Unprocessable Entity` at this stage. If you're receiving this error, **check which of the two IDs you're sending**. #### 3. Redirect the merchant Redirect the user to the authorization URL, replacing `HASH` with the generated token. | Environment | Redirect URL | | ----------- | ------------------------------------------------------------------------ | | Sandbox | `https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH` | | Production | `https://admin.appmax.com.br/appstore/integration/HASH` | > **The redirect is essential for the merchant to authorize the installation. Without this step, the merchant credentials will not be generated.** > > > **The merchant can pick a store that already exists** > > On this screen the merchant can select an **existing store** from their account instead of creating a new one. The API contract does not change, but the store bound to the installation may be one that already existed — including one where your app was installed before. See [Reusing an existing store on installation](/en/guides/reaproveitar-loja). #### 4. Generate merchant credentials (+ health check) After the merchant authorizes the installation, use the hash to generate the credentials. **Endpoint:** `POST https://api.appmax.com.br/app/client/generate` ```bash curl --location 'https://api.appmax.com.br/app/client/generate' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer YOUR_TOKEN' \ --data '{ "token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554" }' ``` **Response:** ```json { "data": { "client": { "client_id": "MERCHANT_CLIENT_ID", "client_secret": "MERCHANT_CLIENT_SECRET" } } } ``` > **The hash can only be used once. The generated credentials are valid indefinitely until the app is uninstalled.** > > ### Health check During this request, Appmax performs a **health check** to complete the installation. Appmax sends a `POST` to the **validation URL** specified in the "Validation URL" field when creating the application. **Payload sent:** ```json { "app_id": 123, "client_id": "MERCHANT_CLIENT_ID", "client_secret": "MERCHANT_CLIENT_SECRET", "client_key": "EXTERNAL_KEY", "external_key": "EXTERNAL_KEY" } ``` Payload fields: - `app_id` (integer, obrigatório): The application's **App Numerical ID** (numeric ID, e.g. `123`). Note: this is the **Numerical ID, not the UUID** — unlike `POST /app/authorize`. It is the **only required field** in the payload: it is always present in every health check call. - `client_id` (string): **Optional.** Client ID generated for the merchant (API credential). May not be sent — do not treat its absence as an error. - `client_secret` (string): **Optional.** Client Secret generated for the merchant (API credential). May not be sent — do not treat its absence as an error. - `client_key` (string): **Optional.** Same value as `external_key` (kept for backwards compatibility). May not be sent — do not treat its absence as an error. - `external_key` (string): **Optional.** Key provided by the merchant during installation for identification. May not be sent — do not treat its absence as an error. > **app_id: Numerical ID, and the only required field** > > In the health check payload, **only `app_id` is required** — all other fields are optional and may be absent. The `app_id` sent is the **App Numerical ID** (e.g. `123`), **not the UUID**. Your handler should only validate the presence of `app_id` and treat the other fields as optional. **Expected response — HTTP 200:** ```json { "external_id": "37bb0791-ee0b-457d-860c-186e32978bcd", "alias": "My Store" } ``` You are the one who **generates** this `external_id` — Appmax only **persists** it, bound to the store. The same UUID later comes back as the `external-id` header on every front-end call (tokenization, Apple Pay) — it is the identifier for that store on the CDN. **Generate a new UUID on every health check request**: repeated values are rejected by Appmax — if the `external_id` it receives already exists in the database, it is discarded and automatically replaced by the installation's `client_id`. Store it in your database the moment you generate it and, whenever a new health check happens, always keep the latest value and discard the previous one. Full reference at [`external-id`](/en/guides/external-id). | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `external_id` | string (UUID) | Yes | Unique installation ID in your system. **Must be a valid UUID** (v1 through v5) and **unique** across all installations. This same value is later sent as the `external-id` header on CDN calls from the front-end. | | `alias` | string | No | Site/store name. If provided, it will be used as the display name in Appmax. | > **The health check is **mandatory** to complete the installation. If your URL is not reachable, returns a status other than `200`, or fails to return a valid UUID `external_id` in the body, the installation is considered failed — `/app/client/generate` aborts with `500` and no credentials are issued. Test your URL at [Validate your validation URL](/en/guides/validar-url) **before** starting the installation flow.** > > > **The `external_id` **must be a valid UUID** (e.g., `37bb0791-ee0b-457d-860c-186e32978bcd`). For apps in the **Payments and Security** category, the `external_id` is strictly required — the installation will fail if it is not sent.** > > > **Store the `external_id` in your database to identify the binding between the merchant and your application. Each installation must generate a different `external_id` — duplicate values will be rejected.** > > > **Same value, from generation to front-end use** > > The `external_id` you return on the health check **is exactly the same** value that goes as the `external-id` header on every front-end call via CDN. **Generate a new UUID on every health check request** — repeated values are rejected by Appmax and automatically replaced by the installation's `client_id`. Store it the moment you generate it and, on every new health check, replace the stored value with the most recent one, discarding the previous. Usage details, `AppCheckout.init` parameter, and error diagnosis at [`external-id`](/en/guides/external-id). #### 5. Authenticate with the merchant credentials With the merchant credentials (`client_id` and `client_secret`) generated in the previous step, authenticate to obtain the **merchant access token**. This is the token that authorizes transactional operations (`/v1/customers`, `/v1/orders`, `/v1/payments/*`). **Endpoint:** `POST https://auth.appmax.com.br/oauth2/token` ```bash curl --location 'https://auth.appmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=MERCHANT_CLIENT_ID' \ --data-urlencode 'client_secret=MERCHANT_CLIENT_SECRET' ``` **Response:** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...", "token_type": "Bearer", "expires_in": 3600 } ``` Use the returned `access_token` as the `Authorization: Bearer YOUR_TOKEN` header on the API's transactional calls. > **This is the **same endpoint** used to authenticate the application (step 1) — the difference is sending the **merchant's** `client_id`/`client_secret`. Sending the app credentials here results in `401` on the `/v1/*` routes.** > > > **The token expires in **1 hour** and the API **does not use refresh tokens** — when it expires, just repeat this same request. The merchant credentials, on the other hand, are permanent (until uninstallation). Understand both credential types at [Authentication](/en/guides/autenticacao).** > > ## Summary 1. Obtain the app token using the app credentials. 2. Authorize the installation by generating a hash. 3. Redirect the merchant to authorize. 4. Generate credentials with `POST /app/client/generate` — **during this request, Appmax sends the health check** to your validation URL. Respond with HTTP 200 and the `external_id` to complete the installation. 5. **Authenticate with the merchant credentials** at `POST /oauth2/token` to obtain the access token and transact on the API (`/v1/*`). ## Troubleshooting The most common errors during installation and how to fix them. ### `422 Unprocessable Entity` on `POST /app/authorize` **Most likely cause:** you're sending the **App Numerical ID** instead of the **App UUID** in the `app_id` field. **Fix:** copy the **App UUID** from the panel (format `8f2c1d3e-5a4b-...`) and send it in that field. The Numerical ID (integer) is only used in `POST /app/client/generate`, never here. ### `422 Unprocessable Entity` on `POST /app/client/generate` **Possible causes:** 1. **Invalid or already-used hash** — each hash can only be used once. If you tried and it failed, you need to generate a new hash with `POST /app/authorize`. 2. **Merchant didn't go through the redirect** — you skipped step 3. Without the redirect and authorization in the panel, the hash doesn't become valid for generating credentials. 3. **Expired hash** — hashes have limited lifetime. Generate a new one and complete the flow immediately. **Fix:** follow the full flow in order: `authorize` → redirect → authorization in the panel → `client/generate`. ### `500 Internal Server Error` on `POST /app/client/generate` **Most likely cause:** the health check failed. Appmax tried to call your validation URL and: - The URL didn't respond (timeout, DNS, firewall, or wrong URL in the panel). - The URL responded with a status other than `200`. - The URL responded with `200` but **without a valid `external_id` in UUID format** in the JSON body. **Fix:** 1. Verify the validation URL is correct in the app panel. 2. Test it manually with `curl` — it must respond to POST with HTTP 200 and JSON containing `{"external_id": ""}`. 3. Check your server logs for the POST that Appmax sent. 4. Make sure you're **not using `localhost`** or a private URL — Appmax needs to reach the URL publicly. ### I received the POST on the validation URL, but `POST /app/client/generate` returns an error **Cause:** your validation URL received the payload but didn't respond correctly — most likely: - Responded with a status other than `200` (e.g., `204`, `301`, `500`). - Returned `200` but **without JSON containing `external_id`** in the body. - Returned an `external_id` **that is not a valid UUID** or **already used** in another installation. **Fix:** your validation URL handler must: 1. Return **HTTP 200** explicitly. 2. Include a JSON body with `{"external_id": ""}`. 3. Generate a unique `external_id` per installation (e.g., `uuid.v4()`). ### The validation URL is on `localhost` (development) Appmax cannot reach private URLs. During development: - Use a tunnel service like [ngrok](https://ngrok.com), [beeceptor](https://beeceptor.com), or similar. - Configure the public URL in the app panel. - When publishing to production, update to the final URL. ### App credentials (`client_id`/`client_secret`) don't work on `/v1/*` routes **Cause:** app credentials only work on `POST /app/authorize` and `POST /app/client/generate`. For transactional routes (`/v1/customers`, `/v1/orders`, etc.), use the **merchant credentials** generated in step 4. See [Authentication](/guides/autenticacao) to understand both credential types. ### Errors using `external_id` on the front-end (CDN) The errors on this page cover the **generation** of the `external_id` during the health check. If the installation completed successfully but the front-end (tokenization, Apple Pay) is returning `401 Missing Authorization token`, `404 Merchant not found`, `404 Client id not found`, or `External ID is required`, those are **usage** errors of the `external-id` header — the full diagnosis table is at [Common errors and diagnosis](/en/guides/external-id#common-errors-and-diagnosis). ### More help If your error is not listed above: - Check the [FAQ](/guides/faq). - Review [Rate Limit](/guides/rate-limit) if you're getting `429`. - Use the `appmax-docs` MCP server (`diagnose_error` tool) passing the status code and the endpoint for an automated diagnosis. --- Source: https://docs.appmax.com.br/en/guides/callback-instalacao.md # Installation callback (`url_callback`) This guide details what happens between `POST /app/authorize` and the merchant credentials generation in `POST /app/client/generate`, focusing on the `url_callback` parameter and the `token` delivered to it. Use this page when you need to: - Understand the exact format of the URL Appmax calls at the end of the authorization. - Point the callback to a dedicated microservice (onboarding, provisioning, credentials) instead of your dashboard front-end. For the end-to-end flow, see [App installation](/en/guides/instalacao). ## Flow overview ```mermaid sequenceDiagram participant App as Your backend participant API as api.appmax.com.br participant Panel as Appmax panel participant Merchant as Merchant browser participant Callback as Your url_callback
(microservice) Note over App,Callback: 1. Create the authorization hash App->>API: POST /app/authorize
(app_id, external_key, url_callback) API-->>App: { "data": { "token": "" } } Note over App,Callback: 2. Redirect the merchant to authorize App->>Merchant: redirect to /appstore/integration/ Merchant->>Panel: authenticates and approves the installation Note over App,Callback: 3. Appmax redirects to url_callback with the token Panel-->>Merchant: 302 Location: ?token= Merchant->>Callback: GET ?token= Note over App,Callback: 4. Microservice swaps the token for credentials Callback->>API: POST /app/client/generate
(Bearer APP token, body: { token }) activate API API->>API: validates hash, runs health check,
creates merchant client_id/client_secret API-->>Callback: { client_id, client_secret } deactivate API Callback->>Callback: persists credentials bound to the merchant Callback-->>Merchant: success page ``` The `url_callback` receives the `token` directly as a query string, in the merchant's browser redirect. There is no additional authenticated server-to-server handshake **before** the callback — which is exactly why this endpoint can live in a microservice independent from the integrator's dashboard. ## Request parameters for `POST /app/authorize` This endpoint requires an access token issued for the **app** credentials (see [Authentication](/en/guides/autenticacao#app-credentials)). ```bash curl --location 'https://api.appmax.com.br/app/authorize' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer APP_ACCESS_TOKEN' \ --data '{ "app_id": "8f2c1d3e-5a4b-4c7d-9e1f-2a3b4c5d6e7f", "external_key": "store_42", "url_callback": "https://onboarding.myapp.com/appmax/callback", "domain_names": ["mystore.com"] }' ``` - `app_id` (string, obrigatório): **App UUID** (do not use the Numerical ID). E.g., `8f2c1d3e-5a4b-4c7d-9e1f-2a3b4c5d6e7f`. - `external_key` (string, obrigatório): Key provided by the partner platform to identify the origin of the installation (e.g., `store_id`, `merchant_id`). It is echoed back in the health check. - `url_callback` (string, obrigatório): Absolute URL (scheme `https://`) where the merchant will be redirected after authorization. Appmax appends the `token` to this URL (see next section). - `domain_names` (string[]): List of authorized store domains. Use when the app operates on multiple domains. - `domain_name` (string): Singular alternative to `domain_names`. Kept for backwards compatibility. > **Required if you're going to use Apple Pay** > > Neither is required for the installation itself, but one of them is **essential** if the store will process Apple Pay payments — it's from the domain informed here that Appmax registers the domain with Apple. See [Domain configuration for Apple Pay](/en/api-reference/payments/apple-pay-dominio). **Response:** ```json { "data": { "token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554" } } ``` The `token` returned here is an **opaque single-use hash**, valid for **1 hour** (cache TTL). You use it both in the redirect and later in `POST /app/client/generate`. > **Token format** > > The `token` is not a JWT. It is an opaque identifier (SHA1) that references the installation data kept in Appmax's cache. Do not try to decode it — just pass the value through as received. ## Redirect to authorization Once you have the hash, redirect the merchant to Appmax's authorization URL: | Environment | Redirect URL | | ----------- | ------------------------------------------------------------------------ | | Sandbox | `https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH` | | Production | `https://admin.appmax.com.br/appstore/integration/HASH` | Replace `HASH` with the `token` returned by `/app/authorize`. The merchant signs in, reviews the permissions, and confirms the installation on the Appmax panel. ## Callback format Once the merchant authorizes the installation, Appmax issues an **HTTP 302** from the merchant's browser to your `url_callback`, appending the `token` as a query string. ### Parameter name The parameter is always called `token`. It is not `code`, `access_token`, or `authorization_code`. ### Concatenation rule Appmax concatenates the `token` honoring any existing query string: - If `url_callback` has **no** query string: `?token=` - If `url_callback` **already** has a query string: `&token=` ### Examples `url_callback` sent on `/app/authorize`: ``` https://onboarding.myapp.com/appmax/callback ``` URL the merchant hits: ``` https://onboarding.myapp.com/appmax/callback?token=12083w36219d223f33ecf48f2a7f5ccf143b0bc554 ``` `url_callback` with your own parameters (useful to carry context/state): ``` https://onboarding.myapp.com/appmax/callback?merchant_ref=42&state=xyz ``` URL the merchant hits: ``` https://onboarding.myapp.com/appmax/callback?merchant_ref=42&state=xyz&token=12083w36219d223f33ecf48f2a7f5ccf143b0bc554 ``` > **Callback method** > > The callback is a **GET** issued by the merchant's browser (302 redirect). It is not a POST and has no body. All context data must live in the query string of the `url_callback` you originally sent. ## Exchanging the token for credentials The `token` received in the callback **is not** a credential — it is a short-lived ticket. To obtain the merchant's definitive credentials, your backend must call `POST /app/client/generate` using the **app** access token (not the merchant's, which doesn't exist yet). ```bash curl --location 'https://api.appmax.com.br/app/client/generate' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer APP_ACCESS_TOKEN' \ --data '{ "token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554" }' ``` **Response:** ```json { "data": { "client": { "client_id": "MERCHANT_CLIENT_ID", "client_secret": "MERCHANT_CLIENT_SECRET" } } } ``` While processing this call, Appmax triggers the [health check](/en/guides/instalacao#health-check) on the validation URL registered in the app panel. If that URL does not respond with `HTTP 200` and a valid UUID `external_id`, credential generation fails. > **`external_id` is different from `token`** > > Do not confuse the two values that appear in this flow: > > - **`token`** — arrives in the redirect query string (`?token=...`), is **single-use**, and exists only to be exchanged for credentials. > - **`external_id`** — the UUID you return in the health check; stays **persisted** tied to the store and is used on every CDN call (header `external-id`) from then on. > > Persist the `external_id` in your database next to the `client_id` and `client_secret`. Full reference at [`external-id`](/en/guides/external-id). > **Single use** > > The token is consumed on the first successful call to `/app/client/generate` — Appmax removes it from the cache. Reusing it returns `Invalid token`. If it fails, start a new `/app/authorize`. ## Callback handler example Minimal Node.js (Express) handler showing the three responsibilities of the callback: extract the `token`, swap it for credentials, persist and acknowledge. ```javascript import express from 'express' import axios from 'axios' const app = express() const APP_CLIENT_ID = process.env.APPMAX_APP_CLIENT_ID const APP_CLIENT_SECRET = process.env.APPMAX_APP_CLIENT_SECRET const AUTH_URL = 'https://auth.appmax.com.br/oauth2/token' const API_URL = 'https://api.appmax.com.br' // Gets the APP token (an in-memory cache is recommended in production) async function getAppAccessToken () { const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: APP_CLIENT_ID, client_secret: APP_CLIENT_SECRET }) const { data } = await axios.post(AUTH_URL, body) return data.access_token } app.get('/appmax/callback', async (req, res) => { const { token, merchant_ref } = req.query if (!token) { return res.status(400).send('missing token') } try { const accessToken = await getAppAccessToken() const { data } = await axios.post( `${API_URL}/app/client/generate`, { token }, { headers: { Authorization: `Bearer ${accessToken}` } } ) const { client_id, client_secret } = data.data.client // Persist the merchant credentials bound to your merchant_ref await saveMerchantCredentials({ merchant_ref, client_id, client_secret }) return res.redirect('/onboarding/done') } catch (err) { console.error('appmax callback failed', err.response?.data ?? err.message) return res.status(502).send('failed to generate merchant credentials') } }) app.listen(3000) ``` Equivalent in plain PHP: ```php true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => getenv('APPMAX_APP_CLIENT_ID'), 'client_secret' => getenv('APPMAX_APP_CLIENT_SECRET'), ]), ]); $appToken = json_decode(curl_exec($auth), true)['access_token']; curl_close($auth); // 2. Swap the received hash for the merchant credentials $generate = curl_init('https://api.appmax.com.br/app/client/generate'); curl_setopt_array($generate, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $appToken, ], CURLOPT_POSTFIELDS => json_encode(['token' => $token]), ]); $response = json_decode(curl_exec($generate), true); curl_close($generate); $clientId = $response['data']['client']['client_id'] ?? null; $clientSecret = $response['data']['client']['client_secret'] ?? null; if (!$clientId || !$clientSecret) { http_response_code(502); exit('failed to generate merchant credentials'); } // 3. Persist the credentials bound to the merchant saveMerchantCredentials($merchantRef, $clientId, $clientSecret); header('Location: /onboarding/done'); ``` ## Why this enables microservice-based integrations Because the `token` reaches `url_callback` via a browser redirect, the URL can point to **any public HTTP service** — not necessarily the same host that kicked off `/app/authorize`. This unlocks a few common architectures: - **Dedicated onboarding microservice**: the integrator's dashboard starts the flow, but the callback points to an isolated service responsible solely for provisioning the merchant (creating a tenant, generating resources, storing credentials). The onboarding service does not need to know about the dashboard internals. - **Serverless function**: point `url_callback` at a Lambda/Cloud Function. It swaps the token for credentials and writes to a database or secret vault — no long-running server required. - **Domain separation**: the dashboard can live on `app.mydomain.com` while the callback points to `onboarding.mydomain.com` — each with its own deployment and security posture. The key point is that **there is no authenticated server-to-server handshake between Appmax and your `url_callback` before the callback arrives**. The token itself carries the authorization — whoever has the token and the app credentials can complete the flow. This simplifies the microservice design, which only needs to: 1. Expose a public HTTPS endpoint. 2. Have access to the app credentials (typically via a secret vault/environment variables). 3. Have access to the storage where merchant credentials will be persisted. ## Security - **Use HTTPS** for `url_callback`. Tokens in query strings over HTTP are visible to intermediaries and leak into logs. - **Validate context**: include your own identifier in the `url_callback` (e.g., `?merchant_ref=42`) and confirm it matches a legitimate installation attempt on your side before calling `/app/client/generate`. - **Treat the token as a short-lived secret**: 1h TTL in Appmax's cache, consumed on first successful exchange. Don't write the token to structured logs without masking. - **Protect the app credentials**: the app's `client_id`/`client_secret` live in the callback microservice. Use a secret manager (AWS Secrets Manager, Vault, Parameter Store) in production. - **Idempotency**: since the token is single-use, repeated calls to `/app/client/generate` with the same token return `Invalid token`. If the merchant refreshes the success page, design your handler to detect the already-provisioned state before trying to exchange the token again. - **Mandatory health check**: the validation URL registered in the app panel must be publicly reachable and return `HTTP 200` with a UUID `external_id` during `/app/client/generate`. Without it, the callback receives the token but the exchange fails. See [Health check](/en/guides/instalacao#health-check). ## Common errors | Symptom | Likely cause | Fix | | ------- | ------------ | --- | | Callback hit without `token` query param | `url_callback` already had a fragment (`#`) or a malformed URL | Send `url_callback` without a fragment; Appmax only manipulates the query string | | `/app/client/generate` returns `Invalid token` | Token already consumed, expired (>1h), or never authorized by the merchant | Start a new `/app/authorize` and run the flow again | | Callback arrives but the microservice cannot call `/app/client/generate` | Missing APP token in the microservice (app credentials) | Ensure the microservice has access to the app's `client_id`/`client_secret` | | `/app/client/generate` returns `500` | Health check failed on the validation URL | See [Installation troubleshooting](/en/guides/instalacao#troubleshooting) | ## See also - [App installation](/en/guides/instalacao) — full flow with health check - [Authentication](/en/guides/autenticacao) — difference between app and merchant credentials - [Create an app](/en/guides/criar-aplicativo) — registration of the validation URL used in the health check --- Source: https://docs.appmax.com.br/en/guides/reaproveitar-loja.md # Reusing an existing store on installation On the authorization screen the merchant reaches when installing your app — the redirect to `/appstore/integration/HASH`, [step 3 of the installation](/en/guides/instalacao#installation-flow) — they can **select a store that already exists** in their account, instead of always creating a new one. ## What changed Previously, every installation necessarily created a new store. Whenever the merchant installed more than one app, or reinstalled the same one, the account piled up duplicate stores — each with its own `external_id` and its own credentials, with no obvious relationship between them. Now the authorization screen offers a store selection field. The merchant chooses between reusing an existing store and creating a new one. > ****Nothing changes in the API contract.** The four installation steps, the validation URL contract, and the credentials/tokens process remain exactly the same. No new parameter is required from the partner app.** > > ## Store and site are the same thing This is the main source of confusion, so it's worth spelling out: | Where you see it | Term used | | --- | --- | | Merchant-facing screens (Appmax panel, authorization screen) | **store** ("loja") | | API and webhooks | **site** (`site_id`) | It is the **same entity** with two names. When this page says "existing store", the API equivalent is a `site` that already has a `site_id`. If a webhook arrives with a `site_id` you already know, it's because the installation was bound to a store that already existed. ## What the merchant sees The screen's behavior depends on what the merchant picks: | Scenario | "Store name" field | "Select Company" field | Domain | Store created? | | --- | --- | --- | --- | --- | | **Picked an existing store** | Not shown (the store already has a name) | Pre-filled and locked with that store's company | Pre-filled with the store's registered domain | No | | **Picked "Create new store"** | Shown, the merchant fills it in | The merchant chooses | The merchant fills it in | Yes | | **Merchant has no stores** | Shown, the merchant fills it in | The merchant chooses | The merchant fills it in | Yes | When the merchant does not have any store yet, the selection field **does not appear** — the flow is the previous one, unchanged. Only **eligible stores** show up in the list: active, belonging to the merchant themselves, and with a company in good standing at Appmax. A store that does not meet these criteria is simply not offered. ## What changes for your integration In the contract, nothing. The implication is conceptual: the store bound to the installation **may be one that already existed** — including one where your app was installed before. > **Revisit the "one installation = one new store" assumption** > > If your system creates local records assuming each installation corresponds to a freshly created store, that assumption no longer holds. Treat the installation as **idempotent per store**: when the health check arrives, check whether you already have a record for that store and **update** it instead of inserting a new row. > > Without that, you accumulate orphan records pointing at the same store, holding old `external_id` values that Appmax no longer recognizes. ## Reinstalling at the same store When the merchant reinstalls your app picking the same store: | What happens | Detail | | --- | --- | | **The app ↔ store binding is updated** | Not duplicated — there is still a single binding between your app and that store. | | **The `external_id` replaces the previous one** | The value your validation URL returns on the new health check becomes the store's. The old value **ceases to exist**. | | **A new `client_id` is generated** | Every installation generates new credentials. The store accumulates credentials over time — always use the **most recent** one. | > **Always persist the latest `external_id`** > > If your front-end keeps sending the old `external_id` in the `external-id` header, the requests answer `404 Merchant not found`. > > On every health check, **store the new value and discard the previous one**. And remember: you must [generate a new UUID on every health check request](/en/guides/instalacao#health-check) — repeated values are rejected by Appmax. Full life cycle at [`external-id`](/en/guides/external-id). The usual recommendation still applies: **read the `external_id` from your database** on every checkout render, keyed by the store. If you cache that value in session state or in an environment variable, invalidate the cache when a new health check arrives. ## Availability The feature is rolled out **gradually**. While it is not active for an account, the authorization screen keeps the previous behavior — every installation creates a new store — and **nothing changes for existing integrations**. In other words: you don't need to wait for the rollout to adjust your code. An integration that already treats the installation as idempotent per store works in both modes. ## See also - [Installation flow](/en/guides/instalacao) — the four steps, including the redirect where the selection happens. - [Installation callback](/en/guides/callback-instalacao) — redirect handler that receives the token. - [`external-id`](/en/guides/external-id) — identifier life cycle and when it changes. - [Validate your validation URL](/en/guides/validar-url) — tool to check the health check contract. - [Webhooks](/en/guides/webhooks) — where the store appears as `site_id`. --- Source: https://docs.appmax.com.br/en/guides/automatizar-criacao-credenciais.md # Automate credential creation with `url_callback` This tutorial shows, step by step, how to build a service that receives the Appmax callback after `/app/authorize` and **automatically generates merchant credentials** with no manual intervention. By the end you will have an endpoint that turns a just-authorized merchant into an integration ready to transact. If you want to understand the mechanism behind `url_callback` (exact format, contract, detailed security notes), see [Installation callback](/en/guides/callback-instalacao). This page is a **hands-on** — full code and operational decisions. ## What you will build ``` Merchant authorizes → Callback receives token → Service generates credentials → Persists → Ready to transact ``` A single HTTP endpoint solves the automation. No manual confirmation screen, no human in the middle. ## Prerequisites - App registered on the panel with its **App UUID** and **app credentials** (`client_id`/`client_secret`). See [Create an app](/en/guides/criar-aplicativo). - **Validation URL** registered on the panel (used by the health check during `/app/client/generate`). See [Installation](/en/guides/instalacao#health-check). - Public HTTPS endpoint for `url_callback` (in production; for dev use ngrok or similar). - A database to bind `merchant_ref` ↔ `client_id`/`client_secret`. - A vault or environment variables to keep the app credentials out of source code. > **Validation URL and url_callback are different things** > > - **Validation URL** — registered on the panel; receives the server-to-server health check during `/app/client/generate`. > - **`url_callback`** — sent on `/app/authorize`; receives the browser redirect with the `token`. > > This tutorial is about the second one. The first must already be working as a prerequisite. --- ## 1. Kick off the installation with `url_callback` From your backend (or integrator dashboard), call `/app/authorize` pointing `url_callback` at the automation service you are about to build. ```bash curl --location 'https://api.appmax.com.br/app/authorize' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer APP_ACCESS_TOKEN' \ --data '{ "app_id": "8f2c1d3e-5a4b-4c7d-9e1f-2a3b4c5d6e7f", "external_key": "store_42", "url_callback": "https://onboarding.myapp.com/appmax/callback?merchant_ref=42" }' ``` ```json { "data": { "token": "12083w36219d223f33ecf48f2a7f5ccf143b0bc554" } } ``` > **Carry your own identifier** > > Pass a `merchant_ref` (or any internal identifier) in the `url_callback` query string. It comes back untouched on the callback and lets you bind the received token to the right merchant in your database. With the hash in hand, redirect the merchant: | Environment | URL | | ----------- | ------------------------------------------------------------------------ | | Sandbox | `https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH` | | Production | `https://admin.appmax.com.br/appstore/integration/HASH` | After the merchant authorizes, Appmax redirects to `https://onboarding.myapp.com/appmax/callback?merchant_ref=42&token=12083w36219d223f33ecf48f2a7f5ccf143b0bc554`. --- ## 2. Implement the callback handler The handler has three responsibilities, in this order: 1. Read `token` and `merchant_ref` from the query string. 2. Swap the `token` for merchant credentials by calling `/app/client/generate`. 3. Persist the credentials and confirm success to the merchant. Full example in **Go 1.26**, using only `net/http` from the stdlib (with the default `http.ServeMux`) plus `pgx/v5` for Postgres. The four files below make a service ready to run. ##### main.go ```go package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/jackc/pgx/v5/pgxpool" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { logger.Error("db.connect", "err", err) os.Exit(1) } defer pool.Close() repo := NewRepository(pool) appmax := NewAppmaxClient(AppmaxConfig{ AuthURL: getenv("APPMAX_AUTH_URL", "https://auth.appmax.com.br/oauth2/token"), APIURL: getenv("APPMAX_API_URL", "https://api.appmax.com.br"), ClientID: os.Getenv("APPMAX_APP_CLIENT_ID"), ClientSecret: os.Getenv("APPMAX_APP_CLIENT_SECRET"), }) mux := http.NewServeMux() mux.HandleFunc("GET /appmax/callback", handleCallback(repo, appmax)) srv := &http.Server{ Addr: ":3000", Handler: mux, ReadHeaderTimeout: 5 * time.Second, } go func() { logger.Info("server.start", "addr", srv.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("server.error", "err", err) stop() } }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = srv.Shutdown(shutdownCtx) } func handleCallback(repo *Repository, appmax *AppmaxClient) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") merchantRef := r.URL.Query().Get("merchant_ref") if token == "" || merchantRef == "" { http.Error(w, "missing token or merchant_ref", http.StatusBadRequest) return } ctx := r.Context() log := slog.With("merchant_ref", merchantRef) // Idempotency: if we already provisioned this merchant, skip the swap. if existing, err := repo.Find(ctx, merchantRef); err != nil { log.Error("repo.find", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } else if existing != nil { http.Redirect(w, r, "/onboarding/done", http.StatusFound) return } creds, err := appmax.GenerateClient(ctx, token) if err != nil { switch { case errors.Is(err, ErrInvalidToken): log.Warn("appmax.invalid_token") http.Error(w, "token invalid or already consumed", http.StatusConflict) case errors.Is(err, ErrHealthCheckFailed): log.Error("appmax.health_check_failed") http.Error(w, "health check failed — restart the flow", http.StatusBadGateway) default: log.Error("appmax.generate", "err", err) http.Error(w, "failed to generate merchant credentials", http.StatusBadGateway) } return } if err := repo.Save(ctx, merchantRef, creds); err != nil { // At this point Appmax's token is ALREADY consumed. There is no way to redo // without a new /app/authorize — log loudly and return 5xx. log.Error("repo.save", "err", err) http.Error(w, "failed to persist credentials", http.StatusInternalServerError) return } log.Info("onboarding.ok") http.Redirect(w, r, "/onboarding/done", http.StatusFound) } } func getenv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } ``` ##### appmax_client.go ```go package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" ) // Typed errors the handler can branch on via errors.Is. var ( ErrInvalidToken = errors.New("appmax: invalid or already consumed token") ErrHealthCheckFailed = errors.New("appmax: health check failed") ) type AppmaxConfig struct { AuthURL string APIURL string ClientID string ClientSecret string } type AppmaxClient struct { cfg AppmaxConfig http *http.Client mu sync.Mutex appToken string appExpiry time.Time } func NewAppmaxClient(cfg AppmaxConfig) *AppmaxClient { return &AppmaxClient{ cfg: cfg, http: &http.Client{Timeout: 15 * time.Second}, } } // MerchantCredentials is the pair returned by /app/client/generate. type MerchantCredentials struct { ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` } // appAccessToken fetches (or reuses) the APP Bearer via OAuth2 client_credentials. // Renews 60s before actual expiration to avoid a race at the boundary. func (c *AppmaxClient) appAccessToken(ctx context.Context) (string, error) { c.mu.Lock() defer c.mu.Unlock() if c.appToken != "" && time.Now().Before(c.appExpiry) { return c.appToken, nil } form := url.Values{ "grant_type": {"client_credentials"}, "client_id": {c.cfg.ClientID}, "client_secret": {c.cfg.ClientSecret}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.AuthURL, strings.NewReader(form.Encode())) if err != nil { return "", err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := c.http.Do(req) if err != nil { return "", fmt.Errorf("auth request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("auth status %d: %s", resp.StatusCode, body) } var out struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", fmt.Errorf("auth decode: %w", err) } c.appToken = out.AccessToken c.appExpiry = time.Now().Add(time.Duration(out.ExpiresIn-60) * time.Second) return c.appToken, nil } // GenerateClient swaps the token received on url_callback for the merchant's // definitive credentials. The real API body accepts only {"token": ...} — // app_id and external_key were persisted by Appmax during /app/authorize. func (c *AppmaxClient) GenerateClient(ctx context.Context, token string) (*MerchantCredentials, error) { bearer, err := c.appAccessToken(ctx) if err != nil { return nil, err } body, _ := json.Marshal(map[string]string{"token": token}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.APIURL+"/app/client/generate", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+bearer) resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("generate request: %w", err) } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) switch resp.StatusCode { case http.StatusOK: var out struct { Data struct { Client MerchantCredentials `json:"client"` } `json:"data"` } if err := json.Unmarshal(raw, &out); err != nil { return nil, fmt.Errorf("generate decode: %w", err) } return &out.Data.Client, nil case http.StatusUnprocessableEntity: // Invalid, expired, or already consumed hash. return nil, fmt.Errorf("%w: %s", ErrInvalidToken, raw) case http.StatusInternalServerError: // Usually means the health check against the validation URL failed. return nil, fmt.Errorf("%w: %s", ErrHealthCheckFailed, raw) default: return nil, fmt.Errorf("generate status %d: %s", resp.StatusCode, raw) } } ``` ##### repository.go ```go package main import ( "context" "errors" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type Repository struct { pool *pgxpool.Pool } func NewRepository(pool *pgxpool.Pool) *Repository { return &Repository{pool: pool} } type StoredCredentials struct { MerchantRef string ClientID string ClientSecret string CreatedAt time.Time } // Find returns (nil, nil) when the merchant has not been provisioned yet. func (r *Repository) Find(ctx context.Context, merchantRef string) (*StoredCredentials, error) { const q = `SELECT merchant_ref, client_id, client_secret, created_at FROM merchant_credentials WHERE merchant_ref = $1` row := r.pool.QueryRow(ctx, q, merchantRef) var c StoredCredentials if err := row.Scan(&c.MerchantRef, &c.ClientID, &c.ClientSecret, &c.CreatedAt); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, nil } return nil, err } return &c, nil } // Save is idempotent — ON CONFLICT keeps the first successful provisioning. func (r *Repository) Save(ctx context.Context, merchantRef string, creds *MerchantCredentials) error { const q = `INSERT INTO merchant_credentials (merchant_ref, client_id, client_secret, created_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT (merchant_ref) DO NOTHING` _, err := r.pool.Exec(ctx, q, merchantRef, creds.ClientID, creds.ClientSecret) return err } ``` ##### schema.sql ```sql CREATE TABLE merchant_credentials ( merchant_ref TEXT PRIMARY KEY, client_id TEXT NOT NULL, client_secret TEXT NOT NULL, external_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- In production, consider encrypting client_secret at rest (pgcrypto, KMS, -- envelope encryption) or storing it in a vault and keeping only a reference here. ``` > **Why persist `external_id`** > > This UUID is the **same value** your validation URL returns on the health check of `/app/client/generate`. You will need it every time you render the merchant's checkout — it is the third parameter of `AppmaxScripts.init(...)` and the `external-id` header on CDN calls. Without persisting it, you lose access to the installation identifier. Full reference at [`external-id`](/en/guides/external-id). To run: ```bash go mod init onboarding && go mod tidy export DATABASE_URL=postgres://user:pass@localhost:5432/onboarding export APPMAX_APP_CLIENT_ID=... export APPMAX_APP_CLIENT_SECRET=... go run . ``` > **`client_secret` is a sensitive credential** > > In production, encrypt `client_secret` at rest (e.g., `pgcrypto`, KMS, envelope encryption) or store it in a secret vault (AWS Secrets Manager, Vault) and keep only the reference in the table. --- ## 3. Use the merchant credentials With credentials persisted, your app transacts on behalf of the merchant by authenticating at `/oauth2/token` with the **merchant** credentials (not the app's). The returned token has transactional scope. ```go // merchant_token.go — fetches a MERCHANT Bearer from the repository package main import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" ) func MerchantAccessToken(ctx context.Context, repo *Repository, merchantRef string) (string, error) { creds, err := repo.Find(ctx, merchantRef) if err != nil { return "", err } if creds == nil { return "", fmt.Errorf("merchant %q not provisioned", merchantRef) } form := url.Values{ "grant_type": {"client_credentials"}, "client_id": {creds.ClientID}, "client_secret": {creds.ClientSecret}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://auth.appmax.com.br/oauth2/token", strings.NewReader(form.Encode())) if err != nil { return "", err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() var out struct { AccessToken string `json:"access_token"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return "", err } return out.AccessToken, nil } ``` From that point on, any transactional call (customers, orders, payments) uses this token. See [Authentication](/en/guides/autenticacao) and [Full integration example](/en/guides/exemplo-integracao). ```go token, err := MerchantAccessToken(ctx, repo, "42") if err != nil { return err } req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.appmax.com.br/v1/customers", bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) ``` --- ## Production tips ### Idempotency The `token` you receive in the callback is **single-use** — Appmax wipes it from the cache the moment `/app/client/generate` returns successfully. Two consequences: - **Retry with the same token won't work**. If the merchant refreshes the page or the callback is replayed, the second `/app/client/generate` returns `Invalid token`. - **To make the handler idempotent**, query your database by `merchant_ref` before trying the swap. If credentials exist, treat as success and redirect — that's what the example above does. ### Error handling | Situation | What to do | | ---------------------------------------------------- | ---------- | | `/app/client/generate` returns `Invalid token` | Token already consumed or expired (>1h). Ask the merchant to restart the installation — unless your database already holds credentials, in which case just finish. | | `/app/client/generate` returns `500` (health check failed) | Your validation URL didn't respond `HTTP 200` with a UUID `external_id`. The token **was** consumed — you must redo `/app/authorize`. See [Troubleshooting](/en/guides/instalacao#troubleshooting). | | Timeout calling `/app/client/generate` | Don't retry with the same token. Check your database for credentials — if none arrived, restart the flow. | | Database persistence failure | You already hold `client_id`/`client_secret` in memory — persist before responding. If the insert fails, return `5xx` and don't confirm to the merchant. The flow will need to restart. | ### Observability - Emit metrics per stage: `authorize_started`, `callback_received`, `credentials_generated`, `credentials_persisted`, `onboarding_failed`. Tags: `app_id`, no PII. - Log `merchant_ref`, Appmax HTTP status and error message. **Never log** `token`, `client_secret` or the app Bearer without masking. - Alert when the failure rate crosses a threshold (e.g., >5% in 15 min) — this almost always means the health check is down. ### Security - **HTTPS is mandatory** on `url_callback`. A token in a query string over HTTP leaks. - **Validate `merchant_ref`** — before calling `/app/client/generate`, confirm there is a legitimate installation attempt on your side for that ref. Helps mitigate someone replaying a leaked callback URL. - **App credentials** live in the onboarding service only. Use a secret vault in production. - **Short timeout** on the call to `/app/client/generate` (10–15s). Slow failures freeze the merchant's callback. - **Don't regenerate** the APP token across processes without a coordinated cache — `expires_in` is 1h, so many instances hammering `/oauth2/token` is wasteful, not insecure. ### Where to run the service The handler is stateless enough to run anywhere: - **Container (ECS, Cloud Run, Kubernetes)** when you already have the infra. - **Serverless function (Lambda + API Gateway, Cloud Functions)** to pay only per use. The in-memory APP token cache still helps within an invocation; use a shared cache (ElastiCache/Redis) if you want to save calls to `/oauth2/token`. - **Integrator monolith** — just add the route. Works, you just lose the domain separation. ## Next steps - [Installation callback](/en/guides/callback-instalacao) — full technical reference of the `url_callback` contract. - [App installation](/en/guides/instalacao) — the 4-step flow, including the health check. - [Authentication](/en/guides/autenticacao) — app vs merchant credentials. - [Full integration example](/en/guides/exemplo-integracao) — your first transaction after credentials are provisioned. --- Source: https://docs.appmax.com.br/en/guides/autenticacao.md # Authentication and authorization ## Which credentials should I use? **I need to install the app on a store** → use **APP** credentials (from the developer dashboard). Endpoints: `/app/authorize`, `/app/client/generate`. **I need to create a customer, order or payment** → use **MERCHANT** credentials (returned at the end of the installation). Endpoints: `/v1/customers`, `/v1/orders`, `/v1/payments/*`. **I'm not sure** → do you have a merchant who installed your app? If yes, use merchant credentials. If not, follow the [installation flow](/en/guides/instalacao) first to obtain them. ## Understanding the credentials The Appmax API uses **two pairs of credentials** (`client_id` and `client_secret`). Both have the same format but serve completely different purposes. Confusing the two is the most common cause of integration errors. ### App credentials | Field | Description | | --------------- | ------------------------------------------------------------------ | | `client_id` | Your application's identifier in the Appstore | | `client_secret` | Application secret key | | Obtained from | Developer dashboard, when creating the application | | Scope | Installation flow only (`/app/authorize`, `/app/client/generate`) | | Validity | Permanent (as long as the app exists) | > **App credentials **do not allow** creating customers, orders, or payments. If you receive a `401` error when calling the transactional API, you are likely using the wrong credentials.** > > ### Merchant credentials | Field | Description | | --------------- | ------------------------------------------------------------------ | | `client_id` | Unique identifier for the app installation on that store | | `client_secret` | Installation secret key | | Obtained from | Returned at the end of the installation flow (`/app/client/generate`) | | Scope | Transactional operations: customers, orders, payments, refunds | | Validity | Permanent (until the app is uninstalled by the merchant) | > **For each merchant that installs your app, you receive a different pair of credentials. Store them securely, associated with the corresponding merchant.** > > ### Quick comparison | Aspect | App credentials | Merchant credentials | | ----------------------- | ------------------------------------- | -------------------------------------- | | When generated | When creating the app in the dashboard | At the end of the installation flow | | How many exist | 1 pair per app | 1 pair per merchant that installed the app | | What they allow | Initiate installation, generate credentials | Create customers, orders, payments | | Authentication endpoint | `POST /oauth2/token` | `POST /oauth2/token` (same endpoint) | | Do they expire | No | No (until uninstallation) | | Generated token expires in | 1 hour | 1 hour | > **Both use the **same endpoint** (`https://auth.appmax.com.br/oauth2/token`) with the **same request format**. The only difference is which `client_id` and `client_secret` you send. The returned token will have different permissions depending on the credential used.** > > ### Visual flow ```mermaid sequenceDiagram participant Dev as Your App participant Auth as auth.appmax.com.br participant API as api.appmax.com.br participant M as Merchant participant HC as Your Validation URL rect rgb(227, 242, 253) Note over Dev,HC: Phase 1 — Installation (APP credentials) Dev->>Auth: POST /oauth2/token (app client_id + secret) Auth-->>Dev: App Token (1h) Dev->>API: POST /app/authorize (token) API-->>Dev: hash (single use) Dev->>M: Redirect with hash M->>API: Merchant confirms installation Dev->>API: POST /app/client/generate (hash) activate API API->>HC: POST health check (app_id, external_key) HC-->>API: HTTP 200 + external_id (UUID) Note over API,HC: Server-to-server. Your URL must
be public and return 200. API-->>Dev: Merchant client_id + client_secret deactivate API end rect rgb(243, 229, 245) Note over Dev,HC: Phase 2 — Operations (MERCHANT credentials) Dev->>Auth: POST /oauth2/token (merchant client_id + secret) Auth-->>Dev: Merchant Token (1h) Dev->>API: /v1/customers, /v1/orders, /v1/payments/* API-->>Dev: Response end ``` ## Common credential errors | Error | Likely cause | Solution | | ----- | ------------ | -------- | | `401` when creating customer/order | Using **app** credentials instead of **merchant** credentials | Use the credentials returned by `/app/client/generate` | | `401` when calling `/app/authorize` | Using **merchant** credentials instead of **app** credentials | Use the credentials from the developer dashboard | | `500` when calling `/app/client/generate` | Incomplete installation flow (missing redirect) | Follow all 4 steps of the installation flow in order | | `401` expired token | JWT token older than 1 hour | Generate a new token with the same credentials | | `403` on `/oauth2/token` | Wrong endpoint (using the API URL instead of auth) | Use `https://auth.appmax.com.br/oauth2/token` | ## Why don't we use refresh tokens? The API adopts an authentication model without refresh tokens. This decision is based on the server-to-server communication architecture. > **Detailed reasons** > > 1. **Server-to-server nature:** communication occurs directly between servers in controlled, secure environments. This reduces the need for additional token renewal mechanisms. > > 2. **Security and simplicity:** short-lived tokens (1 hour) limit the usage window. In server-to-server environments where credentials are stored securely, this approach simplifies management. > > 3. **Reduced complexity:** eliminates secure refresh token storage, token rotation, and renewal logic. > > 4. **Compliance with best practices:** in server-to-server integrations, it is common to use short-lived access tokens with key-based authentication. ## Obtaining the token #### 1. Authentication Send the **merchant** credentials (for transactional operations) or the **app** credentials (for the installation flow). #### 2. Short-lived token After authentication, a 1-hour access token is issued and must be used in all subsequent requests. #### 3. Token renewal When the token expires, obtain a new one through the same initial authentication process. ### Example with merchant credentials ```bash curl --location 'https://auth.appmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=MERCHANT_CLIENT_ID' \ --data-urlencode 'client_secret=MERCHANT_CLIENT_SECRET' ``` ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...", "token_type": "Bearer", "expires_in": 3600 } ``` ### Example with app credentials ```bash curl --location 'https://auth.appmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=APP_CLIENT_ID' \ --data-urlencode 'client_secret=APP_CLIENT_SECRET' ``` > **The merchant's `client_id` and `client_secret` are never changed. New ones can only be generated by performing new installations, and existing ones can only be deactivated by uninstalling the app.** > > --- Source: https://docs.appmax.com.br/en/guides/external-id.md # `external-id` Unique identifier for your app installation at a store. It ties every call originating from the end customer's browser (CDN, tokenization, Apple Pay) to the correct merchant on Appmax. This page is the central reference. Other pages link here instead of repeating the content. ## `external-id` vs `external_id` convention The same identifier appears with two spellings throughout the docs — it is the **same value**, in different formats: | Spelling | Where it appears | | ------------- | ----------------------------------------------------------------------------------------------- | | `external_id` | JSON body of the health check (`{ "external_id": "..." }`), field stored in your database. | | `external-id` | Name of the **HTTP header** sent on CDN calls to the payment gateway. | This follows standard HTTP conventions (headers in `kebab-case`, JSON in `snake_case`). Mentally treat it as the same value — the string is identical between formats. ## What it is The `external-id` is an **identifier**, not a secret. It identifies the pair **(installed app + store)** to the platform. When your front-end calls the payment gateway, Appmax uses the `external-id` to identify the store and run the operation in the correct context. Key points: - **It is not a credential**. It does not authenticate the call. Authentication of the gateway route is handled by Appmax (internal gateway), not by the integrator. - **You are the one who generates it**. The value originates on your side during installation — Appmax only persists and validates it. - **It is not sensitive enough to require a vault**, but treat it as store configuration: persisted in your database, linked to the merchant, read on every checkout page render. The lifecycle of the `external_id` is: ```mermaid sequenceDiagram autonumber participant Merchant as Merchant store / Frontend participant Integrator as Integrator (you) participant Appmax as Appmax Note over Integrator,Appmax: App installation Merchant->>Integrator: Clicks "install" Integrator->>Appmax: POST /app/authorize (url_callback) Appmax-->>Integrator: redirect to url_callback with token (single use) Integrator->>Appmax: POST /app/client/generate (token) Appmax->>Integrator: server-to-server health check Note right of Integrator: You generate a UUID v4
= external_id Integrator-->>Appmax: HTTP 200 + { external_id: "" } Note over Appmax: Persists external_id
bound to installation Appmax-->>Integrator: client_id + client_secret Note over Integrator: You store in your database:
external_id + client_id + client_secret Note over Merchant,Appmax: Checkout usage Merchant->>Integrator: Opens checkout page Integrator-->>Merchant: HTML + AppmaxScripts.init(..., externalId, ...) Note over Merchant: Form data-appmax-checkout
triggers tokenization via CDN Merchant->>Appmax: appmax.js uses externalId internally Appmax-->>Merchant: onSuccess({ ip, token }) ``` **The `external_id` is generated by the integrator during the health check.** Appmax only persists and validates — it does not invent this value. The same UUID that originates on your side in step 6 comes back in step 11 as the CDN header. ## Where it comes from The `external_id` is defined **during the app's installation at a store**. The flow: 1. You start the installation by calling [`POST /app/authorize`](/en/guides/instalacao#installation-flow). 2. The merchant authorizes in the Appmax panel and is redirected to your `url_callback` with a `token`. 3. You exchange the `token` for credentials by calling [`POST /app/client/generate`](/en/guides/callback-instalacao). 4. While processing that call, Appmax performs a **server-to-server health check** against the validation URL configured in your app panel. 5. **Your validation URL responds with `HTTP 200` and a JSON body containing `external_id` (UUID).** 6. Appmax internally persists the binding between that `external_id` and the store. The `external_id` you return in this health check is the same value you must send as the `external-id` header on every subsequent CDN call for that store. Format details (UUID v1-v5, uniqueness, validation) in [Health check](/en/guides/instalacao#health-check). > **Persist it at generation time** > > When your callback handler generates the `external_id` to answer the health check, **store that value in your database** linked to the merchant. You will need it on every checkout render for that merchant. See [Automating credential creation](/en/guides/automatizar-criacao-credenciais). ## Where it is used You consume `external_id` through the public methods of [Appmax JS](/en/guides/appmax-js) — the script wraps the HTTP transport to Appmax and propagates the identifier wherever needed. Contexts where it shows up: ### 1. Script initialization — `AppmaxScripts.init` At the merchant's checkout, `externalId` is the **third parameter** of `init`: ```javascript window.AppmaxScripts.init(onSuccess, onError, externalId, onUpdate, onAuthorize); ``` Without it, card tokenization and Apple Pay initialization fail with `External ID is required` before any HTTP call is made. ### 2. Card form submit — `data-appmax-checkout` Any `` in the DOM triggers card tokenization through the CDN on submit. The script reuses the `externalId` provided in `init` — you don't need to repeat it or touch the HTTP headers. ```html
...
``` The card token arrives back in your code via the `onSuccess({ ip, token })` callback. ### 3. Apple Pay — `onAuthorize` When the user taps the Apple Pay button, the script opens Safari's PaymentSheet, runs Apple's merchant validation **(using `externalId` under the hood)** and delivers the `appleToken` via the `onAuthorize` callback you set on `init`. You don't call any endpoint yourself — just take the token and send it to `POST /v1/payments/apple-pay` from your backend. > You typically don't write any HTTP call to `scripts.appmax.com.br` manually — `appmax.js` does it for you. The [Tokenization](/en/api-reference/payments/cartao-credito#tokenizacao) and [Apple Pay merchant session](/en/api-reference/payments/apple-pay-merchant-session) pages exist for advanced cases (custom implementation without the script) and for debugging. ## When NOT to use `external-id` `external-id` belongs exclusively to calls originating from the **browser via Appmax JS**. Everywhere else — especially on your backend — it has no role. **Do not send `external-id` on server-to-server calls** made by your backend to `api.appmax.com.br` (creating a customer, creating an order, payment, recurring billing, split, etc.). Those calls use `Authorization: Bearer` with the **merchant** token obtained via `/oauth2/token` — the store context comes from the JWT itself. Summary: | Call origin | How to authenticate | Where does `external_id` go? | | ------------------------------------------------------------------------------------------ | -------------------------------------------- | ------------------------------------ | | Backend → `api.appmax.com.br` (transactional) | `Authorization: Bearer ` | Not sent — context comes from JWT. | | Browser → Appmax JS (`AppmaxScripts.init`, `data-appmax-checkout` form, Apple Pay) | Configured once in `init` | The script propagates internally; you don't touch headers or body. | ## When `external_id` changes The `external_id` is stable **while the installation is active**. However: - **Reinstalling the app at the same store** generates a **new** `external_id`. This holds both when the merchant **removes and reinstalls** your application and when they **reinstall without removing**, picking the same store on the authorization screen (see [Reusing an existing store on installation](/en/guides/reaproveitar-loja)). In both cases the old value stops working and the front-end must be updated with the new one. - **Different stores of the same merchant** have different `external_id` values. Each (installed app, store) pair has its own — they cannot be shared across stores. **Recommendation:** treat the `external_id` as live data. Do not hardcode it in code or in environment variables — read it from your database on every checkout render, keyed by the merchant. ## Common errors and diagnosis | Response | Where it appears | Likely cause | How to fix | | ------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `401 Missing Authorization token` | HTTP response from the gateway | The call reached the gateway **without** the `external-id` header and **without** `Authorization`. | Confirm the CDN script was initialized with the correct `externalId` before the form submit. | | `404 Merchant not found` | HTTP response from the gateway | The value sent in `external-id` does not match any active installation. | Verify the front-end is using this store's `external_id`, not another's. Confirm the installation is active. | | `404 Client id not found` | HTTP response from the gateway | The `external_id` exists, but the store's credential binding is incomplete. | Open a support ticket. This usually indicates a half-finished installation. | | `External ID is required` | Script error in the browser | `AppmaxScripts.init(...)` was called without the third parameter (`externalId` missing or empty). | Make sure your template renders the store's `externalId` before calling `init`. | ## Best practices - **Persist** the `external_id` in your database alongside the merchant's `client_id` and `client_secret`. See the example in [Automating credential creation](/en/guides/automatizar-criacao-credenciais). - **Never hardcode** the value. There is no "default" or "test" value — each installation has its own. - **Read it from the database** on every checkout render. If you cache in session state, invalidate when the merchant reinstalls the app. - **Do not expose** the `external_id` unnecessarily in public logs or third-party trackers. It is not a secret, but it is installation data — keep basic data hygiene. - **Handle front-end script errors**: if `init` rejects with `External ID is required`, log the error and show the user a "checkout temporarily unavailable, please try again" message — do not let the submit silently break. ## See also - [App installation](/en/guides/instalacao) — full flow where the `external_id` is generated. - [Installation callback](/en/guides/callback-instalacao) — redirect handler that receives the token. - [Automating credential creation](/en/guides/automatizar-criacao-credenciais) — tutorial that persists credentials and the `external_id`. - [Appmax JS](/en/guides/appmax-js) — CDN script that consumes `externalId` in `init`. - [Apple Pay payment](/en/api-reference/payments/apple-pay) — using `externalId` in the Apple Pay session. --- Source: https://docs.appmax.com.br/en/guides/appmax-js.md # Appmax JS ## What is Appmax JS `appmax.js` is a JavaScript library developed by Appmax for secure integration on checkout pages. By including the script, you don't need to worry about changes to the store's appearance, since it operates discreetly and efficiently. > **IP collection is always mandatory** > > IP collection via Appmax JS is **mandatory** for all integrations — even if your architecture is within PCI-DSS scope. There is no API alternative for this step alone. > **Server-side tokenization requires PCI-DSS** > > Card tokenization, on the other hand, can be done in two ways: through Appmax JS itself (recommended — the script isolates sensitive data from your servers) or directly through your API, if your architecture is within PCI-DSS scope. When tokenizing from your backend, your server touches the card number and CVV in the clear — this is only allowed within PCI-DSS scope. If you're not sure, use the Appmax JS path. See [Credit card tokenization](/en/api-reference/payments/cartao-credito#tokenizacao). ## How it works Due to **PCI DSS** (Payment Card Industry Data Security Standard) guidelines, it is crucial to protect sensitive data. `appmax.js` was designed to: - Prevent sensitive card data from passing through your servers. - Collect the customer's IP address for payment flow security. ## How to use #### 1. Include the CDN script ```html ``` #### 2. Initialize AppmaxScripts After loading the script, initialize it with the parameters below: ```javascript window.AppmaxScripts.init(onSuccess, onError, externalId, onUpdate, onAuthorize); ``` | Parameter | Required | Description | | -------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `onSuccess` | Yes | Success callback. Receives `{ ip, token? }` after IP collection or tokenization. | | `onError` | Yes | Error callback. Receives the error thrown by the script. | | `externalId` | Yes for tokenization and Apple Pay | App installation identifier for the store. See [`external-id`](/en/guides/external-id). | | `onUpdate` | Yes for Apple Pay | Callback fired when the PaymentSheet opens and on every change to it. Must return the current cart, with numeric values in BRL — see [the return contract](/en/api-reference/payments/apple-pay-appmax-js#what-onupdate-must-return). | | `onAuthorize` | Yes for Apple Pay | Callback fired when the payment is authorized. Receives the Apple Token. **Reject the Promise to signal failure** — see [how to signal failure](/en/api-reference/payments/apple-pay-appmax-js#how-to-signal-failure-in-onauthorize). | > **Implementing the Apple Pay button?** > > `onUpdate` and `onAuthorize` are only half the story — the button also requires specific DOM selectors and its own load order. See the dedicated walkthrough at [Implementing the Apple Pay button with the Appmax JS](/en/api-reference/payments/apple-pay-appmax-js). > **`externalId` is required for tokenization **through Appmax JS**** > > Without `externalId`, submitting any form with `data-appmax-checkout` fails with `External ID is required` before the HTTP call is made. The value is the same `external_id` defined during the [app installation at the store](/en/guides/instalacao#health-check) — persist it in your database and render it in the checkout template. See [`external-id`](/en/guides/external-id) for the full reference. > > If your architecture is within PCI-DSS scope and you'd rather tokenize directly through your API (without the script), use the backend path with the **merchant's access token** (`Authorization: Bearer`) instead of `externalId` — see [Credit card tokenization](/en/api-reference/payments/cartao-credito#tokenizacao). > **`init()` can throw a synchronous exception for Apple Pay** > > If you pass `onUpdate` and `onAuthorize` (Apple Pay flow) without a valid `externalId`, `init` throws `Error("External ID is required for Apple Pay use.")` **synchronously**, inside the call itself — not via `onError`. In React, this blows up inside `useEffect` and can bring down the whole tree if there's no `try/catch` around the call. If you only need IP collection (no tokenization, no Apple Pay), `externalId`, `onUpdate`, and `onAuthorize` can be omitted: ```javascript window.AppmaxScripts.init(onSuccess, onError); ``` ## DOM contract: `init` is not reactive `AppmaxScripts.init(...)` runs `querySelector` **once**, at the moment it's called, and doesn't observe DOM changes after that. On a traditional page (server-rendered HTML, nothing changes after load) this is transparent. In React, Vue, or any SPA, it needs attention: - **The IP trigger and the Apple Pay button need to exist in the DOM *before* `init` runs.** If the element shows up later — behind a route, a checkout step, a `v-if`/conditional — the SDK never finds it. There's no error, no log: the click or the collection simply don't happen. - **`init()` is not idempotent.** Each call registers new listeners, without removing the previous ones. With React StrictMode (which mounts effects twice in development) or any component that re-renders and re-runs the initialization effect, this silently accumulates handlers. Call `init` once per page load, not on every re-render. ## Available features ### Customer IP collection Collection happens when the SDK finds **one of the triggers** below in the DOM — no need to submit anything or wait for user interaction. Without one of the two present at `init` time, `onSuccess` and `onError` simply don't fire (see "DOM contract" above — it's a silent failure, no error at all). | Trigger | When to use | | --- | --- | | `form[data-appmax-customer]` | Traditional page (MPA), native form. | | `.appmax-ip` (any element) | **Recommended for SPAs** — doesn't require a `` in the tree and avoids the hidden-input injection described in the warning below. | ```html
``` Or, with no form at all — the element just needs to exist, not be visible: ```html ``` > **The SDK injects a hidden `` inside `form[data-appmax-customer]`** > > When it finds that form, the SDK inserts an `` into it via direct DOM manipulation — outside your framework's control. In an SPA, that node can be dropped on the next re-render with no warning. If you're in an SPA, prefer the `.appmax-ip` trigger above, which doesn't have this problem. For frameworks like Vue.js, the trigger (`.appmax-ip` in the example below) needs to be rendered **before** `init` — see "DOM contract" above. Retrieve the IP in the success callback: ```html ``` ```javascript const customer = ref({ first_name: '', last_name: '', email: '', phone: '', ip: '' }) const success = (data) => { customer.value.ip = data.ip || 'IP not found.' } const error = (error) => { console.error('Error:', error) } onMounted(() => { const script = document.createElement('script') script.src = 'https://scripts.appmax.com.br/appmax.min.js' script.onload = () => { if (window.AppmaxScripts) { window.AppmaxScripts.init(success, error) } } document.head.appendChild(script) }) ``` ### Payment tokenization Tokenization occurs when a form is submitted with the `data-appmax-checkout` attribute. Sensitive card data is converted into a secure token. > **Requires `externalId` at initialization** > > To tokenize through the script, initialize with the store's `externalId`: `AppmaxScripts.init(onSuccess, onError, externalId)`. Without it, the form submit fails with `External ID is required`. See [`external-id`](/en/guides/external-id). The `token` arrives back via the `onSuccess({ ip, token })` callback — you don't touch HTTP or headers. If you need to tokenize manually (without the script), the underlying endpoint contract lives at [Credit card tokenization](/en/api-reference/payments/cartao-credito#tokenizacao) — including the alternative backend path (merchant Bearer), allowed only within PCI-DSS scope. ```html
``` Fields are identified by the `appmax-form-element` attribute: | Attribute | Description | | ------------------- | -------------------------------- | | `number` | Credit card number | | `holder_name` | Cardholder name | | `expiration_month` | Expiration month | | `expiration_year` | Expiration year | | `cvv` | Security code (CVV) | ## Interactive test Use the playground below to test the CDN features directly in the browser — no installation required. > Ferramenta interativa disponível na versão web desta página. --- Source: https://docs.appmax.com.br/en/guides/webhooks.md # Webhooks ## Overview Appstore webhooks allow your application to receive real-time notifications about events that occur on the Appmax platform. When an event happens (order approved, customer created, subscription cancelled, etc.), Appmax sends a `POST` request to the URL you configured when creating the app. The payload is sent as JSON with a standard envelope that includes event metadata and the resource-specific data. There are **4 event types** (`order`, `customer`, `payment`, `subscription`) totaling **40 available events**. > **Appstore webhooks are dispatched in real time as soon as the event occurs on the platform.** > > ## Payload Structure (Envelope) All webhooks share the same envelope. The `data` field varies depending on the `event_type`. ```json { "event": "order_approved", "event_type": "order", "site_id": "uuid-do-site", "app_id": "uuid-do-app", "client_key": "chave-externa", "external_key": "chave-externa", "data": { }, "partner_merchant": { "merchant_email": "merchant@example.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` | Field | Type | Required | Description | | -------------------- | ----------------- | -------- | ---------------------------------------------------------------------- | | `event` | string | Yes | Event identifier (e.g., `order_approved`) | | `event_type` | string | Yes | Event type: `order`, `customer`, `payment`, or `subscription` | | `site_id` | string | Yes | UUID of the site where the event occurred | | `app_id` | string | Yes | UUID of the app receiving the webhook | | `client_key` | string \| null | No | Key configured by the merchant for external identification | | `external_key` | string \| null | No | External key associated with the resource | | `data` | object | Yes | Resource data -- varies depending on the `event_type` | | `partner_merchant` | object | Yes | Merchant data: `merchant_email`, `merchant_document_number`, `merchant_phone` | ## Event Table ### Customer | Description | `event` | `event_type` | | --------------------- | ---------------------- | ------------ | | Customer created | `customer_created` | `customer` | | Customer interested | `customer_interested` | `customer` | | Customer contacted | `customer_contacted` | `customer` | ### Order | Description | `event` | `event_type` | | ---------------------------------------- | --------------------------------- | ------------ | | Order authorized | `order_authorized` | `order` | | Order approved | `order_approved` | `order` | | Boleto created | `order_billet_created` | `order` | | Order paid | `order_paid` | `order` | | Order pending integration | `order_pending_integration` | `order` | | Order refunded | `order_refund` | `order` | | Partial refund | `order_partial_refund` | `order` | | Upsell paid | `order_up_sold` | `order` | | Pix generated | `order_pix_created` | `order` | | Pix paid | `order_paid_by_pix` | `order` | | Pix expired | `order_pix_expired` | `order` | | Order integrated | `order_integrated` | `order` | | Boleto overdue | `order_billet_overdue` | `order` | | Order authorized with delay | `order_authorized_with_delay` | `order` | | Chargeback in treatment | `order_chargeback_in_treatment` | `order` | | Chargeback won (merchant favor) | `order_charge_back_gain` | `order` | | Refused by risk | `order_refused_by_risk` | `order` | | Payment split | `split_orders` | `order` | ### Payment | Description | `event` | `event_type` | | ---------------------------------- | -------------------------------- | ------------ | | Payment authorized with delay | `payment_authorized_with_delay` | `payment` | | Payment not authorized | `payment_not_authorized` | `payment` | ### Subscription | Description | `event` | `event_type` | | --------------------------------- | --------------------------------------- | -------------- | | Subscription created | `subscription_created` | `subscription` | | Subscription cancelled | `subscription_cancelation` | `subscription` | | Subscription paused | `subscription_paused` | `subscription` | | Subscription resumed | `subscription_resumed` | `subscription` | | Recurring charge succeeded | `subscription_charge_success` | `subscription` | | Recurring charge failed | `subscription_charge_failed` | `subscription` | | Product added | `subscription_product_added` | `subscription` | | Product removed | `subscription_product_removed` | `subscription` | | Product quantity changed | `subscription_product_quantity_changed` | `subscription` | | Frequency changed | `subscription_frequency_changed` | `subscription` | | Cycle skipped | `subscription_cycle_skipped` | `subscription` | | Cycle unskipped | `subscription_cycle_unskipped` | `subscription` | | Billing day changed | `subscription_billing_day_changed` | `subscription` | | Next billing date changed | `subscription_next_billing_day_changed` | `subscription` | | Address updated | `subscription_address_updated` | `subscription` | | Payment method updated | `subscription_payment_method_updated` | `subscription` | | Subscription delayed (legacy) | `subscription_delayed` | `subscription` | > **The essential events for integration are order (`order_*`) and payment (`payment_*`) events.** > > > **Every event depends on the **matching permission** granted to the app. An app without the `subscription-product-added` permission, for example, does not receive `subscription_product_added` -- even when the event happens in the store. Review the app permissions in the Appstore before investigating an event that "never arrives".** > > `subscription_delayed` is a legacy event, kept only for backward compatibility: the current subscription engine does not emit it. Use `subscription_charge_failed` to detect a failed recurring charge. ## Payloads by Event Type ### Order Events The `data` field for `order` type events contains the following fields: | Field | Type | Description | | ------------------------ | ----------------- | ------------------------------------------------ | | `order_id` | int | Order ID | | `status` | string | Current order status | | `total` | int | Total amount in cents (12300 = R$ 123.00) | | `freight_value` | int | Shipping amount in cents | | `merchant_total` | int | Merchant net amount in cents | | `merchant_affiliate_total` | int | Merchant affiliate amount in cents | | `discount` | int | Discount amount in cents | | `interest` | int | Interest amount in cents | | `upsell_order_id` | int \| null | Associated upsell order ID | | `payment_link_id` | int \| null | Payment link ID | | `paid_at` | string \| null | Payment date/time | | `integrated_at` | string \| null | Integration date/time | | `refund_at` | string \| null | Refund date/time | | `created_at` | string | Order creation date/time | | `products` | array | List of products in the order | | `payment_info` | object | Payment information (varies by method) | | `client_key` | string \| null | External identification key | | `external_key` | string \| null | External key | | `cashback_used` | int \| null | Cashback used in cents | | `cashback_reserved` | int \| null | Cashback reserved in cents | | `cashback_status` | string \| null | Cashback status | | `notification_type` | string | Notification type | **`products[]` fields:** | Field | Type | Description | | ---------- | ------ | ---------------------- | | `sku` | string | Product SKU | | `name` | string | Product name | | `price` | int | Unit price in cents | | `quantity` | int | Quantity | **`payment_info` fields** (conditional by payment method): For **Pix**: | Field | Type | Description | | ------------------------ | ------ | ------------------------------- | | `pix.end_to_end_id` | string | Pix transaction end-to-end ID | | `pix.pix_creation_date` | string | Pix creation date | | `pix.pix_expiration_date` | string | Pix expiration date | | `pix.pix_emv` | string | EMV code (copy and paste) | | `pix.pix_ref` | string | Pix reference | | `pix.pix_qrcode` | string | QR Code image URL | | `pix.pix_payment_link` | string | Pix payment link | For **Boleto**: | Field | Type | Description | | ----------------------------- | ------ | ------------------------ | | `boleto.boleto_overdue_date` | string | Due date | | `boleto.boleto_url` | string | Boleto URL | | `boleto.boleto_digitable_line` | string | Digitable line | For **Credit Card / Apple Pay**: | Field | Type | Description | | -------------------------------- | ------ | ---------------------- | | `credit_card.installments` | int | Number of installments | | `credit_card.card_brand` | string | Card brand | | `credit_card.nsu` | string | Transaction NSU | | `credit_card.authorization_code` | string | Authorization code | | `credit_card.captured_at` | string | Capture date/time | #### Example: Order approved with credit card (`order_approved`) ```json { "event": "order_approved", "event_type": "order", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": "merchant-key-123", "external_key": "ext-order-456", "data": { "order_id": 3531, "status": "aprovado", "total": 25990, "freight_value": 1500, "merchant_total": 23400, "merchant_affiliate_total": 0, "discount": 0, "interest": 0, "upsell_order_id": null, "payment_link_id": null, "paid_at": "2025-03-15 14:30:00", "integrated_at": null, "refund_at": null, "created_at": "2025-03-15 14:28:00", "products": [ { "sku": "PROD-001", "name": "Curso de Marketing Digital", "price": 25990, "quantity": 1 } ], "payment_info": { "credit_card": { "installments": 3, "card_brand": "visa", "nsu": "0012345678", "authorization_code": "AUTH9876", "captured_at": "2025-03-15 14:30:00" } }, "client_key": "merchant-key-123", "external_key": "ext-order-456", "cashback_used": null, "cashback_reserved": null, "cashback_status": null, "notification_type": "order_approved" }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` #### Example: Pix paid (`order_paid_by_pix`) ```json { "event": "order_paid_by_pix", "event_type": "order", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": null, "external_key": null, "data": { "order_id": 4201, "status": "aprovado", "total": 9900, "freight_value": 0, "merchant_total": 8910, "merchant_affiliate_total": 0, "discount": 0, "interest": 0, "upsell_order_id": null, "payment_link_id": 789, "paid_at": "2025-03-15 15:10:00", "integrated_at": null, "refund_at": null, "created_at": "2025-03-15 15:05:00", "products": [ { "sku": "EBOOK-042", "name": "E-book Receitas Fit", "price": 9900, "quantity": 1 } ], "payment_info": { "pix": { "end_to_end_id": "E123456782025031515100001", "pix_creation_date": "2025-03-15 15:05:00", "pix_expiration_date": "2025-03-15 15:35:00", "pix_emv": "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890-abcd-ef12345678905204000053039865802BR5925APPMAX PAGAMENTOS LTDA6009SAO PAULO62070503***63041D3D", "pix_ref": "PIX-REF-4201", "pix_qrcode": "https://api.appmax.com.br/pix/qrcode/4201.png", "pix_payment_link": "https://pay.appmax.com.br/pix/4201" } }, "client_key": null, "external_key": null, "cashback_used": null, "cashback_reserved": null, "cashback_status": null, "notification_type": "order_paid_by_pix" }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` #### Example: Boleto created (`order_billet_created`) ```json { "event": "order_billet_created", "event_type": "order", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": "merchant-key-123", "external_key": null, "data": { "order_id": 4305, "status": "aguardando_pagamento", "total": 34900, "freight_value": 2000, "merchant_total": 31410, "merchant_affiliate_total": 0, "discount": 500, "interest": 0, "upsell_order_id": null, "payment_link_id": null, "paid_at": null, "integrated_at": null, "refund_at": null, "created_at": "2025-03-16 09:00:00", "products": [ { "sku": "KIT-PREMIUM", "name": "Kit Premium de Suplementos", "price": 16700, "quantity": 2 } ], "payment_info": { "boleto": { "boleto_overdue_date": "2025-03-19 23:59:59", "boleto_url": "https://api.appmax.com.br/boleto/4305.pdf", "boleto_digitable_line": "23793.38128 60000.000003 00000.000400 1 84340000034900" } }, "client_key": "merchant-key-123", "external_key": null, "cashback_used": 500, "cashback_reserved": 1000, "cashback_status": "applied", "notification_type": "order_billet_created" }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "98765432000188", "merchant_phone": "21988887777" } } ``` ### Customer Events The `data` field for `customer` type events contains the following fields: | Field | Type | Description | | ---------------------------------- | ----------------- | ---------------------------- | | `customer_id` | int | Customer ID | | `customer_data` | object | Customer personal data | | `customer_data.firstname` | string | First name | | `customer_data.lastname` | string | Last name | | `customer_data.email` | string | Email | | `customer_data.telephone` | string | Phone number | | `customer_data.document_number` | string | CPF or CNPJ | | `customer_data.custom_txt` | string \| null | Custom field | | `customer_address` | object | Customer address | | `customer_address.postcode` | string | Postal code (CEP) | | `customer_address.street` | string | Street name | | `customer_address.street_number` | string | Street number | | `customer_address.street_complement` | string \| null | Address complement | | `customer_address.street_district` | string | Neighborhood | | `customer_address.city` | string | City | | `customer_address.state` | string | State (UF) | | `created_at` | string | Creation date/time | | `updated_at` | string | Last update date/time | | `client_key` | string \| null | External identification key | | `external_key` | string \| null | External key | #### Example: Customer created (`customer_created`) ```json { "event": "customer_created", "event_type": "customer", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": "merchant-key-123", "external_key": null, "data": { "customer_id": 2023, "customer_data": { "firstname": "Junior", "lastname": "Almeida", "email": "junior.almeida@email.com", "telephone": "51983655100", "document_number": "12345678900", "custom_txt": null }, "customer_address": { "postcode": "90010-000", "street": "Rua dos Andradas", "street_number": "1234", "street_complement": "Sala 501", "street_district": "Centro Histórico", "city": "Porto Alegre", "state": "RS" }, "created_at": "2025-03-15 14:25:00", "updated_at": "2025-03-15 14:25:00", "client_key": "merchant-key-123", "external_key": null }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` ### Payment Events The `data` field for `payment` type events contains the following fields: | Field | Type | Description | | ---------------- | ----------------- | -------------------------------------------------------------- | | `customer_id` | int | Customer ID | | `order_id` | int | Order ID | | `payment_type` | string | Payment method (e.g., `credit_card`, `pix`, `boleto`) | | `payment_total` | int | Payment amount in cents | | `cashback_used` | int \| null | Cashback used in cents | #### Example: Payment not authorized (`payment_not_authorized`) ```json { "event": "payment_not_authorized", "event_type": "payment", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": null, "external_key": null, "data": { "customer_id": 2023, "order_id": 3532, "payment_type": "credit_card", "payment_total": 15900, "cashback_used": null }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` ### Subscription Events Subscription events are produced by the Appmax recurring-billing engine and delivered to your app with the same envelope as every other event. The `data` field of **every** subscription event is the union of two blocks: 1. **Base fields** -- present in all subscription events (`null` when they do not apply to that event). 2. **Event-specific fields** -- vary by `event`, described in the table further below. #### Base fields | Field | Type | Description | | ----------------- | -------------- | ---------------------------------------------------------------------------------------------- | | `subscription_id` | int | Subscription ID. The stable identifier across the whole lifecycle | | `order_id` | int | ID of the order that originated the subscription (on creation) or of the charge that fired it | | `customer_id` | int | ID of the customer who owns the subscription | | `total` | int | Order total in cents (12300 = R$ 123.00) | | `interval` | string \| null | Recurrence unit: `week`, `month` or `year` | | `interval_count` | int \| null | Number of units between charges (`interval=month` + `interval_count=2` = every 2 months) | | `status` | string \| null | Subscription state at the moment of the event (see table below) | | `cashback_used` | float \| null | Cashback applied to the order, when any | | `client_key` | string \| null | Merchant external key (repeated from the envelope) | | `external_key` | string \| null | Same value as `client_key` | > **`interval`, `interval_count` and `status` are only filled in on the events where they make sense. On change events (product, cycle, billing day) they arrive as `null` -- the subscription itself did not change state.** > > #### Event-specific fields | `event` | Additional fields in `data` | | --------------------------------------- | ------------------------------------------------------------------------------ | | `subscription_created` | `status` = `active`, `interval`, `interval_count`, `next_charge_at` (ISO-8601) | | `subscription_cancelation` | `status` = `canceled` | | `subscription_paused` | `status` = `paused` | | `subscription_resumed` | `status`, `next_billing_date` | | `subscription_charge_success` | `status` = `success`, `uuid` (subscription UUID) | | `subscription_charge_failed` | `status` = `failed`, `uuid` (subscription UUID) | | `subscription_product_added` | `products` -- full product list **after** the change | | `subscription_product_removed` | `products` -- full product list **after** the change | | `subscription_product_quantity_changed` | `products` -- full product list **after** the change | | `subscription_frequency_changed` | `frequency` (`week`/`month`/`year`), `interval_count`, `next_charge_at` (ISO-8601) | | `subscription_cycle_skipped` | `next_billing_day` (ISO-8601) | | `subscription_cycle_unskipped` | `next_billing_day` (ISO-8601) | | `subscription_billing_day_changed` | `billing_day` | | `subscription_next_billing_day_changed` | `next_billing_day` (ISO-8601) | | `subscription_payment_method_updated` | `payment_method` (`credit_card` or `pix`) | | `subscription_address_updated` | Fields of the updated shipping address | > **Product events (`subscription_product_added`, `subscription_product_removed`, `subscription_product_quantity_changed`) always send the **full consolidated list** of products after the change -- not just the item that changed. Replace your local copy with the list you receive instead of applying a delta.** > > Each `products` item has this shape: ```json { "name": "Produto A - Mensal", "price": 100.0, "quantity": 1, "variant_id": "va-month" } ``` #### Example: Subscription created (`subscription_created`) ```json { "event": "subscription_created", "event_type": "subscription", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": "merchant-key-123", "external_key": "merchant-key-123", "data": { "subscription_id": 501, "order_id": 3532, "customer_id": 2023, "total": 4990, "interval": "month", "interval_count": 1, "status": "active", "next_charge_at": "2025-04-15T14:30:00-03:00", "cashback_used": null, "client_key": "merchant-key-123", "external_key": "merchant-key-123" }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` #### Example: Recurring charge succeeded (`subscription_charge_success`) ```json { "event": "subscription_charge_success", "event_type": "subscription", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": "merchant-key-123", "external_key": "merchant-key-123", "data": { "subscription_id": 501, "order_id": 3987, "customer_id": 2023, "total": 4990, "interval": null, "interval_count": null, "status": "success", "uuid": "6f1e9a5c-8d24-4a1b-9f30-2c7b5e0a1d44", "cashback_used": null, "client_key": "merchant-key-123", "external_key": "merchant-key-123" }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` > **Every billing cycle creates a **new order**. `order_id` changes on each charge; `subscription_id` stays the same. Use `subscription_id` to tie charges back to a subscription and `order_id` to reconcile with the order events (`order_*`) of that charge.** > > #### Example: Product added (`subscription_product_added`) ```json { "event": "subscription_product_added", "event_type": "subscription", "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "client_key": "merchant-key-123", "external_key": "merchant-key-123", "data": { "subscription_id": 501, "order_id": 3532, "customer_id": 2023, "total": 4990, "interval": null, "interval_count": null, "status": null, "products": [ { "name": "Produto A - Mensal", "price": 100.0, "quantity": 1, "variant_id": "va-month" }, { "name": "Produto B - Mensal", "price": 100.0, "quantity": 1, "variant_id": "vb-month" } ], "cashback_used": null, "client_key": "merchant-key-123", "external_key": "merchant-key-123" }, "partner_merchant": { "merchant_email": "loja@exemplo.com", "merchant_document_number": "12345678000199", "merchant_phone": "11999999999" } } ``` #### Idempotency for subscription events The subscription envelope does not carry a unique event identifier. To deduplicate, use the combination **`event` + `subscription_id` + `order_id`**. Careful: charge events legitimately repeat on every cycle -- the pair `subscription_charge_success` + `subscription_id` is **not** unique over time. It is `order_id`, new on every charge, that tells one cycle from another. Change events (product, frequency, cycle), on the other hand, can repeat the same `order_id`; for those, also factor in the payload content or the reception timestamp. ## Event Temporal Flow The diagrams below illustrate the typical sequence of events for each payment method. **Credit Card:** ``` customer_created -> order_authorized -> order_approved -> order_paid -> order_integrated ``` **Pix:** ``` customer_created -> order_pix_created -> [timeout: order_pix_expired] -> order_paid_by_pix -> order_approved -> order_integrated ``` **Boleto:** ``` customer_created -> order_billet_created -> [overdue: order_billet_overdue] -> order_paid -> order_approved -> order_integrated ``` **Refund / Chargeback:** ``` [approved order] -> order_refund (full) -> order_partial_refund (partial) -> order_chargeback_in_treatment -> order_charge_back_gain ``` **Subscription -- lifecycle:** ``` subscription_created -> subscription_charge_success (every cycle, a new order_id) -> subscription_charge_failed (charge declined) -> subscription_cancelation (end of the subscription) subscription_paused <-> subscription_resumed ``` **Subscription -- changes (any time while the subscription is active):** ``` products -> subscription_product_added / subscription_product_removed subscription_product_quantity_changed recurrence -> subscription_frequency_changed subscription_billing_day_changed / subscription_next_billing_day_changed subscription_cycle_skipped <-> subscription_cycle_unskipped account -> subscription_payment_method_updated / subscription_address_updated ``` > **A recurring charge also fires the order events (`order_*`) of the order created for that cycle. If your app subscribes to both types, expect to receive `subscription_charge_success` **and** `order_approved` for the same `order_id`.** > > > **Event ordering is not guaranteed. Network delays, retries, and asynchronous processing can alter the sequence. Always check the current state of the resource before making decisions based on events.** > > ## Retry Policy When the endpoint fails to receive a webhook, Appmax initiates a retry cycle: ``` Attempt 1 (original) -- after event delay | failure Attempt 2 -- +30 minutes | failure Attempt 3 -- +2 hours | failure Attempt 4 -- +4 hours | failure Webhook discarded (no notification) ``` - **HTTP Timeout:** 5 seconds - **Success codes:** 200, 201, 202, 203, 204, 205, 206, 207, 208, 226 - **Failure:** any other HTTP code or timeout triggers a retry - **Maximum:** 4 attempts (1 original + 3 retries) > **After 4 unsuccessful attempts, the webhook is permanently discarded. There is no notification to the developer. Monitor your endpoint actively.** > > ## HTTP Headers Every webhook request is sent with the following headers: | Header | Value | | -------------- | ------------------ | | `Content-Type` | `application/json` | | `User-Agent` | `GuzzleHttp/7` | > **Appmax does not send a signature header (HMAC) or authentication token in webhooks. We recommend validating the origin through other means (see Best Practices).** > > ## How to Receive Webhooks Your endpoint must: 1. Accept `POST` requests with `Content-Type: application/json` 2. Respond with **HTTP 200** within **5 seconds** 3. Process the event asynchronously (do not block the response) > **If the endpoint does not respond 200 within 5 seconds, Appmax initiates the retry cycle. Process the event in the background and respond immediately.** > > ## Code Examples ##### Go ```go package main import ( "encoding/json" "fmt" "log" "sync" "github.com/gin-gonic/gin" ) var ( processed sync.Map ) func main() { r := gin.Default() r.POST("/webhooks/appmax", func(c *gin.Context) { var payload struct { Event string `json:"event"` EventType string `json:"event_type"` Data json.RawMessage `json:"data"` } if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(200, gin.H{"received": true}) // respond 200 even on error return } // Respond 200 immediately to avoid the 5s timeout c.JSON(200, gin.H{"received": true}) // Extract ID for idempotency var data struct { OrderID int `json:"order_id"` CustomerID int `json:"customer_id"` } json.Unmarshal(payload.Data, &data) id := data.OrderID if id == 0 { id = data.CustomerID } key := fmt.Sprintf("%d-%s", id, payload.Event) if _, loaded := processed.LoadOrStore(key, true); loaded { return // duplicate } // Process in a goroutine -- in production, send to a queue go func() { log.Printf("Processing: %s (%s)", payload.Event, payload.EventType) // Your logic here }() }) r.Run(":3000") } ``` ##### Node.js ```javascript const express = require('express'); const app = express(); app.use(express.json()); // Map to track already processed events (in production, use a database) const processed = new Set(); app.post('/webhooks/appmax', (req, res) => { // Respond 200 immediately to avoid the 5s timeout res.status(200).json({ received: true }); const { event, event_type, data } = req.body; const idempotencyKey = `${data.order_id || data.customer_id}-${event}`; if (processed.has(idempotencyKey)) { console.log(`Duplicate event ignored: ${idempotencyKey}`); return; } processed.add(idempotencyKey); console.log(`Processing: ${event} (${event_type})`); // Process event asynchronously // In production, send to a queue (Bull, RabbitMQ, etc.) }); app.listen(3000, () => console.log('Webhook listener on port 3000')); ``` ##### Python ```python from flask import Flask, request, jsonify import threading app = Flask(__name__) processed = set() @app.route('/webhooks/appmax', methods=['POST']) def webhook(): payload = request.get_json() event = payload.get('event') data = payload.get('data', {}) key = f"{data.get('order_id') or data.get('customer_id')}-{event}" if key in processed: return jsonify(received=True), 200 processed.add(key) # Process in background to respond quickly threading.Thread(target=process_event, args=(payload,)).start() return jsonify(received=True), 200 def process_event(payload): print(f"Processing: {payload['event']}") # Your logic here if __name__ == '__main__': app.run(port=3000) ``` ##### PHP (Laravel) ```php // routes/api.php Route::post('/webhooks/appmax', [WebhookController::class, 'handle']); // app/Http/Controllers/WebhookController.php class WebhookController extends Controller { public function handle(Request $request) { $payload = $request->all(); // Dispatch to async job and respond 200 immediately ProcessWebhook::dispatch($payload); return response()->json(['received' => true]); } } // app/Jobs/ProcessWebhook.php class ProcessWebhook implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(private array $payload) {} public function handle() { $event = $this->payload['event']; $data = $this->payload['data']; $key = ($data['order_id'] ?? $data['customer_id']) . '-' . $event; // Check idempotency if (Cache::has("webhook:{$key}")) { return; } Cache::put("webhook:{$key}", true, now()->addHours(24)); // Process event Log::info("Webhook received: {$event}", $this->payload); } } ``` ## Best Practices 1. **Respond 200 before processing.** The timeout is 5 seconds. Synchronous processing causes unnecessary retries. Respond immediately and process in the background (queue, thread, async job). 2. **Implement idempotency.** Use `order_id` + `event` (or `customer_id` + `event`; for subscriptions, `subscription_id` + `order_id` + `event`) as a unique key. Retries legitimately resend the same event, and your system needs to handle duplicates without side effects. 3. **Store the raw payload.** Save the complete JSON to a database or log before processing. This makes debugging easier and allows manual reprocessing without depending on resending. 4. **Do not rely on event ordering.** Network delays and retries can alter the sequence. Always check the current state of the resource (via API, if needed) before making decisions based on an event. 5. **Use HTTPS.** Protect data in transit. Appmax sends webhooks to both HTTP and HTTPS URLs, but customer and payment data are included in the payload. 6. **Handle duplicates.** Retries legitimately resend the same event. Ensure that processing the same event twice does not cause side effects (double charging, sending duplicate emails, etc.). 7. **Validate the origin.** Since there is no HMAC header, consider filtering by source IP, validating the payload structure against the expected schema, or confirming the event via the Appmax API. ## Webhooks: Appstore vs Dashboard There are **two types of webhooks** in the Appmax platform. Don't confuse them: | Aspect | Appstore Webhooks | Dashboard Webhooks | |--------|-------------------|-------------------| | Configured by | App developer, when creating the app | Merchant, in the store's admin panel | | Scope | All merchants who install the app | Only that specific merchant's store | | Destination URL | App host URL (set in the Appstore) | URL defined by the merchant in the panel | | Events | 40 events documented on this page | Subset of events (varies by configuration) | | Payload credentials | `app_id`, `site_id`, `external_key` | Different format, no `app_id` | | When to use | Appstore integrations (this guide) | Direct merchant integrations | > **If you're integrating via the Appstore (you created an app, merchants install it), use the webhooks documented on this page. Dashboard webhooks are for merchants who configure notifications directly, without an intermediary app.** > > ## Errors and Troubleshooting | Scenario | What happens | How to resolve | | -------- | ------------ | -------------- | | Endpoint returns non-2xx HTTP status | Retry initiated (up to 4 attempts) | Return 200, 201, or 202 | | Endpoint does not respond within 5s | Timeout, retry initiated | Process async and respond 200 immediately | | Endpoint returns 502 | Invalid URL or server is down | Verify the registered URL and server availability | | Endpoint returns 401/403 | Authentication failure, retry initiated | Remove authentication from the endpoint or add a whitelist | | Retries exhausted (4 attempts) | Webhook permanently discarded | Monitor your endpoint actively and request resending from support | | Webhook takes long to arrive | Event may be in the retry queue | Check if the endpoint responded 200 on previous attempts | | Webhook does not arrive (Yampi order) | Webhook suppressed | Intentional behavior for orders originating from Yampi | | `customer_interested` does not fire | Customer already has an order | Event only fires for leads with no associated order | ## Testing and Debugging ### webhook.site A free service to inspect received webhooks. Create a temporary URL at [webhook.site](https://webhook.site), configure it as the app's webhook URL, and view the received payloads in real time. ### ngrok To test webhooks directly in your local environment: ```bash ngrok http 3000 ``` Use the HTTPS URL generated by ngrok as the app's webhook URL. Requests will be forwarded to `localhost:3000`, enabling end-to-end debugging with breakpoints. ### General tips - Check your server logs to confirm that requests are arriving - Inspect the request headers to confirm `Content-Type: application/json` - Validate that the received JSON is well-formed before processing - Confirm that the app has the necessary permissions to receive the desired events --- Source: https://docs.appmax.com.br/en/guides/rate-limit.md # Rate Limit ## Overview The Appmax API enforces request limits at two levels to ensure stability and availability for all integrators. Your request must pass both levels to be processed. ## Control levels ### Level 1 — Per-credential limit Applied per merchant `client_id`. Controls request rate and total volume. | Metric | Limit | Description | | ------ | ----- | ----------- | | **Burst** | 50 requests | Maximum simultaneous requests (instant peak) | | **Rate** | 5 requests/second | Sustained request rate | | **Monthly quota** | 100,000 requests/month | Total requests per month (resets on the 1st) | ### Level 2 — Per-route limit Applied per **email + source IP**. Controls individual route usage. | Route type | Limit | Window | | ---------- | ----- | ------ | | Transactional routes (default) | 60 requests | 1 minute | | Sensitive operations (login, credentials) | 5 requests | 1 minute | > **In practice, you can burst up to 50 requests instantly, then maintain a sustained rate of 5 requests per second without being throttled. If the limit is exceeded, the request is rejected with `429`.** > > ### Monthly quota In addition to the per-second rate limit, there is a monthly limit of **100,000 requests** per `client_id`: - The counter is incremented on each request - Automatically resets on the first day of each month > **When the monthly quota is exceeded, all requests are blocked with `429` until the next month.** > > > **These limits exist to ensure platform security and stability, but they are fully flexible. If your integration needs higher limits, don't hesitate to reach out to our team — we'll adjust them to fit your needs.** > > ## Rate limit response When the limit is exceeded, the API returns **HTTP 429** status with informational headers: ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 Retry-After: 45 ``` ```json { "message": "Too many requests", "retryAfter": "45(s)" } ``` | Header | Description | | ------ | ----------- | | `X-RateLimit-Limit` | Maximum number of requests allowed in the window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `Retry-After` | Seconds until you can retry | ## Best practices 1. **Implement retry with exponential backoff.** When receiving `429`, wait for the time indicated in the `Retry-After` header. If no header is present, use exponential backoff (1s, 2s, 4s, 8s...). 2. **Use queues for batch operations.** If you need to create many orders or customers, queue the requests and process them respecting the 5 req/s rate. 3. **Cache the Bearer token.** The token lasts 1 hour. Reuse it instead of generating a new one per request — authentication also consumes quota. 4. **Monitor rate limit headers.** Use `X-RateLimit-Remaining` to adjust your speed before hitting the limit. 5. **Group operations when possible.** Prefer creating customer + order in quick sequence rather than multiple distributed calls. ## Retry with backoff example ##### Go ```go func requestWithRetry(client *http.Client, req *http.Request) (*http.Response, error) { maxRetries := 3 for attempt := 0; attempt <= maxRetries; attempt++ { resp, err := client.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusTooManyRequests { return resp, nil } resp.Body.Close() retryAfter := resp.Header.Get("Retry-After") wait, _ := strconv.Atoi(retryAfter) if wait == 0 { wait = 1 << attempt // backoff: 1s, 2s, 4s } log.Printf("Rate limited, retrying in %ds (attempt %d/%d)", wait, attempt+1, maxRetries) time.Sleep(time.Duration(wait) * time.Second) } return nil, fmt.Errorf("rate limit exceeded after %d retries", maxRetries) } ``` ##### Node.js ```javascript async function requestWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fetch(url, options); if (response.status !== 429) { return response; } const retryAfter = response.headers.get('Retry-After'); const wait = retryAfter ? parseInt(retryAfter) : Math.pow(2, attempt); console.log(`Rate limited, retrying in ${wait}s (attempt ${attempt + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, wait * 1000)); } throw new Error(`Rate limit exceeded after ${maxRetries} retries`); } ``` ##### Python ```python import time import requests def request_with_retry(method, url, max_retries=3, **kwargs): for attempt in range(max_retries + 1): response = requests.request(method, url, **kwargs) if response.status_code != 429: return response retry_after = response.headers.get('Retry-After') wait = int(retry_after) if retry_after else 2 ** attempt print(f"Rate limited, retrying in {wait}s (attempt {attempt + 1}/{max_retries})") time.sleep(wait) raise Exception(f"Rate limit exceeded after {max_retries} retries") ``` ##### PHP ```php function requestWithRetry(string $method, string $url, array $options, int $maxRetries = 3): Response { $client = new \GuzzleHttp\Client(); for ($attempt = 0; $attempt <= $maxRetries; $attempt++) { $response = $client->request($method, $url, $options + [ 'http_errors' => false, ]); if ($response->getStatusCode() !== 429) { return $response; } $retryAfter = $response->getHeader('Retry-After')[0] ?? null; $wait = $retryAfter ? (int) $retryAfter : pow(2, $attempt); Log::warning("Rate limited, retrying in {$wait}s (attempt " . ($attempt + 1) . "/{$maxRetries})"); sleep($wait); } throw new \Exception("Rate limit exceeded after {$maxRetries} retries"); } ``` --- Source: https://docs.appmax.com.br/en/guides/calculo-parcelas.md # Installment calculation ## Installment methods Appmax supports two installment calculation methods, configurable from 1 to 12 installments per merchant. ### PP - Simple per installment The interest rate is applied directly to the value of each installment. The additional cost is added to the base installment value, providing a clear view of the final amount to be paid each month. This is the most commonly used method. ### AM - Financing The interest rate is calculated monthly on the total outstanding balance. The installment amounts vary, as interest is applied to the total balance each month. Configured in specific situations, but equally relevant. > **It is essential to query the installments endpoint to ensure that order values are processed consistently across both systems, using the same rate. The system does not calculate interest automatically.** > > ## Query installments via API Use the `POST /v1/payments/installments` endpoint to retrieve the values with the installment rates configured in Appmax. ```bash curl --request POST \ --url https://api.appmax.com.br/v1/payments/installments \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "installments": 10, "total_value": 10000, "settings": true }' ``` > **The endpoint returns the total value with interest applied for each method. Your integration must perform the division to obtain the exact value of each installment.** > > See the full documentation at [Installment calculation via API](/en/api-reference/payments/parcelas). --- Source: https://docs.appmax.com.br/en/guides/recuperacao-vendas-ia.md # AI-powered sales recovery > **Beta feature** > > AI-powered sales recovery lets you register an **abandoned cart** — a customer who started checkout but didn't complete the payment — so Appmax can automatically attempt to recover that sale using artificial intelligence. There's no separate endpoint for this: the abandoned cart is created through the same [create or update customer](/en/api-reference/customers/criar-atualizar) endpoint, by sending the additional `cart_link` field with the cart's URL. > **Prerequisite** > > To create a customer, you need to have collected the IP using the [Appmax JS](/en/guides/appmax-js) script. ## Request ```bash curl --request POST \ --url https://api.appmax.com.br/v1/customers \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Content-Type: application/json' \ --data '{ "first_name": "Junior", "last_name": "Almeida", "email": "junior.almeida@email.com", "phone": "51983655100", "document_number": "25226493029", "cart_link": "https://subdomain.domain.com/cart/123-345-678", "address": { "postcode": "91520270", "street": "Rua Francisco Carneiro da Rocha", "number": "582", "complement": "Casa", "district": "Moinhos de Ventos", "city": "Porto Alegre", "state": "RS" }, "ip": "127.0.0.1", "products": [ { "sku": "9000010", "name": "Recipe book", "quantity": 1, "unit_value": 12300, "type": "digital" } ], "tracking": { "utm_source": "google", "utm_campaign": "test" } }' ``` ### Body fields | Field | Type | Required | Description | | ------------------ | ------ | :------: | ---------------------------------- | | `first_name` | string | ✅ | Customer's first name | | `last_name` | string | ✅ | Customer's last name | | `email` | string | ✅ | Valid email | | `phone` | string | ✅ | Phone number with area code | | `ip` | string | ✅ | Origin IP | | `products` | array | ✅ | List of linked products | | `cart_link` | string | ✅ | Abandoned cart URL | | `document_number` | string | ❌ | CPF or CNPJ | | `address` | object | ❌ | Customer's address | | `tracking` | object | ❌ | Visit source data (UTMs) | The remaining fields follow the same contract as the [create or update customer](/en/api-reference/customers/criar-atualizar) endpoint. ## Response **201 — customer created successfully** ```json { "data": { "customer": { "id": 1 } } } ``` > **Important** > > Save the `customer_id` returned at this step, even temporarily. You'll need it to create the order if the customer completes the purchase. **422 — validation error** ```json { "message": "The given data failed to pass validation.", "errors": { "message": { "first_name": ["The first_name field is required."], "last_name": ["The last_name field is required."], "phone": ["The phone field must be a string.", "The phone field must have a maximum of 11 characters."], "email": ["The email field is required.", "The email field must be a string."], "ip": ["The ip field is required."] } } } ``` ## See also - [Create or update customer](/en/api-reference/customers/criar-atualizar) - [Appmax JS](/en/guides/appmax-js) - [Create an order](/en/api-reference/orders/criar-pedido) --- Source: https://docs.appmax.com.br/en/guides/exemplo-integracao.md # Full integration example This guide walks through the complete flow of a sale -- from authentication to payment confirmation -- using the **sandbox** environment with ready-to-use examples you can copy and run. > **All examples use sandbox URLs. For production, replace `sandboxappmax` with `appmax` in the URLs.** > > ## What we will build ``` Authenticate -> Collect IP -> Create customer -> Create order -> Pay -> Confirm ``` By the end of this guide you will have completed a full transaction in the sandbox. ## Prerequisites - `client_id` and `client_secret` from the **merchant** (obtained after [installing the app](/en/guides/instalacao)) - HTML page to include the [Appmax JS](/en/guides/appmax-js) - Endpoint to receive [webhooks](/en/guides/webhooks) --- ## 1. Authenticate Obtain a Bearer token using the merchant's credentials. ```bash curl --request POST \ --url https://auth.sandboxappmax.com.br/oauth2/token \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=SEU_CLIENT_ID' \ --data-urlencode 'client_secret=SEU_CLIENT_SECRET' ``` ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600 } ``` Save the `access_token`. It will be used in all subsequent calls as `Bearer YOUR_TOKEN`. The token expires in 1 hour. --- ## 2. Collect the customer's IP Before creating the customer, you must collect the IP using [Appmax JS](/en/guides/appmax-js). Include the script on your checkout page: ```html Checkout
``` > **In the sandbox, if the IP is not available, use `127.0.0.1` for testing. In production, the real IP is required.** > > --- ## 3. Create the customer On your backend, send the customer data to the API. The combination of `first_name` + `last_name` + `email` + `phone` + `ip` is the unique key -- if a match already exists, the customer is updated. ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/customers \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "first_name": "Junior", "last_name": "Almeida", "email": "junior.almeida@email.com", "phone": "51983655100", "ip": "127.0.0.1", "document_number": "25226493029", "address": { "postcode": "91520270", "street": "Rua Francisco Carneiro da Rocha", "number": "582", "complement": "Casa", "district": "Moinhos de Ventos", "city": "Porto Alegre", "state": "RS" } }' ``` ```json { "data": { "customer": { "id": 2023 } } } ``` Save `data.customer.id` -- this is the **customer_id** that will be used in the order. --- ## 4. Create the order Link the order to the customer. Values are in **cents** (R$ 123.00 = `12300`). ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": 2023, "products": [ { "sku": "LIVRO-001", "name": "Livro de receitas", "quantity": 1, "unit_value": 12300, "type": "digital" } ], "shipping_value": 0, "discount_value": 0 }' ``` ```json { "data": { "order": { "id": 3531, "status": "pendente" } } } ``` Save `data.order.id` -- this is the **order_id** that will be used in the payment. The status starts as `pendente`. --- ## 5. Process the payment Choose one of the methods below. In the sandbox, use the [test card](/en/api-reference/payments/cartao-credito#cartoes-de-teste) `4000000000000010` to simulate a successful payment. ##### Credit card There are two approaches: via **token** (recommended) or via **Appmax JS**. This example uses tokenization: **Step 1 -- Tokenize the card:** ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/tokenize \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "payment_data": { "credit_card": { "number": "4000000000000010", "cvv": "123", "expiration_month": "12", "expiration_year": "28", "holder_name": "Junior Almeida" } } }' ``` ```json { "data": { "token": "422146c7523a46119d6073ea56193913" } } ``` **Step 2 -- Pay with the token:** ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/credit-card \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 3531, "customer_id": 2023, "payment_data": { "credit_card": { "token": "422146c7523a46119d6073ea56193913", "holder_document_number": "25226493029", "holder_name": "Junior Almeida", "installments": 1, "soft_descriptor": "MINHALOJA" } } }' ``` ```json { "data": { "order": { "id": 3531, "status": "autorizado" }, "payment": { "method": "creditcard", "installments": 1, "paid_at": "2025-03-15 14:30:00" }, "upsell_hash": "4000114202503117156088040208561001715608804" } } ``` The `autorizado` status indicates the payment was accepted and is undergoing anti-fraud analysis. Wait for the `order_approved` webhook for confirmation. ##### Pix ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/pix \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 3531, "payment_data": { "pix": { "document_number": "25226493029" } } }' ``` ```json { "data": { "order": { "id": 3531, "status": "pendente" }, "pix": { "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANS...", "emv_code": "00020126580014br.gov.bcb.pix0136a1b2c3d4...", "expires_at": "2025-03-15 15:30:00" } } } ``` Display the `qr_code` as an image and the `emv_code` as copyable text. Wait for the `order_paid_by_pix` webhook for confirmation. ##### Boleto ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/boleto \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 3531, "payment_data": { "boleto": { "document_number": "25226493029" } } }' ``` ```json { "data": { "order": { "id": 3531, "status": "pendente" }, "boleto": { "pdf_url": "https://boleto.sandboxappmax.com.br/pdf/abc123...", "digitable_line": "23793.38128 60000.000003 00000.000400 1 84340000012300", "due_date": "2025-03-22" } } } ``` Offer the `pdf_url` as a download button and the `digitable_line` as copyable text. Wait for the `order_paid` webhook when the boleto is settled. --- ## 6. Confirm the payment Confirmation is asynchronous. There are two ways to know the payment was approved: ### Via webhook (recommended) Your endpoint will receive a POST when the status changes: ```json { "event": "order_approved", "event_type": "order", "data": { "order": { "id": 3531, "status": "aprovado", "total_paid": 12300 }, "customer": { "id": 2023, "name": "Junior Almeida", "email": "junior.almeida@email.com" }, "payment": { "method": "creditcard", "installments": 1, "paid_at": "2025-03-15 14:30:00" } } } ``` Respond with **HTTP 200** to acknowledge receipt. ### Via query (polling) If you need to check the status manually: ```bash curl --request GET \ --url https://api.sandboxappmax.com.br/v1/orders/3531 \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' ``` ```json { "data": { "order": { "id": 3531, "status": "aprovado", "total_paid": 12300, "amounts": { "sub_total": 12300, "shipping_value": 0, "discount": 0, "installment_fee": 0 } }, "customer": { "id": 2023, "name": "Junior Almeida", "email": "junior.almeida@email.com" }, "payment": { "method": "creditcard", "installments": 1, "paid_at": "2025-03-15 14:30:00" } } } ``` See all [possible statuses](/en/guides/status-pedidos). --- ## 7. After payment ### Physical products: register the tracking code To release the merchant's withdrawals, update the order with the tracking code: ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders/shipping-tracking-code \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 3531, "shipping_tracking_code": "BR123456789XX" }' ``` ### Refund (if needed) To request a full refund: ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders/refund-request \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 3531, "type": "total", "value": 12300 }' ``` --- ## Flow summary | Step | Endpoint | Result | |------|----------|--------| | Authenticate | `POST /oauth2/token` | `access_token` | | Create customer | `POST /v1/customers` | `customer_id` | | Create order | `POST /v1/orders` | `order_id` (status: `pendente`) | | Tokenize card | `POST /v1/payments/tokenize` | `token` | | Pay | `POST /v1/payments/credit-card` | status: `autorizado` | | Confirm | Webhook `order_approved` | status: `aprovado` | | Tracking | `POST /v1/orders/shipping-tracking-code` | Tracking linked | ## Testing error scenarios Use card `4000000000000028` to simulate a payment failure. The API will return: ```json { "error": { "message": "Payment not authorized" } } ``` See more [test cards](/en/api-reference/payments/cartao-credito#cartoes-de-teste) in the credit card documentation. --- ## Next steps - [Installment calculation](/en/guides/calculo-parcelas): Implement installments by querying Appmax rates. - [Upsell](/en/api-reference/orders/upsell): Offer complementary products after payment. - [Recurring payments](/en/api-reference/recorrencia/criar-recorrencia): Set up automatic recurring charges. - [Apple Pay](/en/api-reference/payments/apple-pay): Accept payments via Apple Pay on Safari. --- Source: https://docs.appmax.com.br/en/guides/exemplo-parcelamento.md # Installment payment This example shows how to implement a complete installment flow: query the rates, display the options to the customer, and process the payment with the correct amount. > **The examples use sandbox URLs. For production, replace `sandboxappmax` with `appmax`.** > > ## Scenario A customer wants to buy a product for **R$ 200.00** and pay in **3 installments**. You need to: 1. Query the merchant's installment rates 2. Display the options to the customer 3. Adjust the order value to include interest 4. Process the payment ## Prerequisites This example assumes you already have: - A valid authentication token ([how to obtain](/en/guides/autenticacao)) - A `customer_id` for the customer ([how to create](/en/api-reference/customers/criar-atualizar)) --- ## 1. Query the installment options Send the total order value (in cents) to get the amounts with interest: ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/installments \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "installments": 12, "total_value": 20000, "settings": true }' ``` ```json { "data": { "installments": { "1": { "total": 20000 }, "2": { "total": 20400 }, "3": { "total": 20812 }, "4": { "total": 21228 }, "5": { "total": 21648 }, "6": { "total": 22072 }, "7": { "total": 22500 }, "8": { "total": 22932 }, "9": { "total": 23368 }, "10": { "total": 23808 }, "11": { "total": 24252 }, "12": { "total": 24700 } }, "settings": { "modality": "PP", "max_installments": 12, "min_installment_value": 500 } } } ``` ## 2. Display the options to the customer On your front-end, calculate the per-installment value by dividing `total` by the number of installments: ```javascript const installments = response.data.installments; const options = Object.entries(installments).map(([n, { total }]) => ({ parcelas: Number(n), valorParcela: total / Number(n), valorTotal: total, temJuros: total > 20000 })); // Result: // 1x of R$ 200.00 (interest-free) // 2x of R$ 102.00 (total R$ 204.00) // 3x of R$ 69.37 (total R$ 208.12) // ... ``` Display to the customer — try changing the value and selecting an installment below: > Ferramenta interativa disponível na versão web desta página. > **The component above is interactive and uses illustrative values based on the example above. In production, query `POST /v1/payments/installments` to get the official values configured by Appmax.** > > ## 3. Create the order with the adjusted value The customer chose **3 installments**. The total with interest is **R$ 208.12** (`20812` cents). Distribute the interest across the products: > **The system **does not calculate interest automatically**. You must send the value with interest already included, either in `unit_value` of the products or in `products_value`.** > > **Option A -- Adjust via `products_value`** (simpler): ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": 2023, "products_value": 20812, "products": [ { "sku": "CURSO-001", "name": "Curso de culinária", "quantity": 1, "type": "digital" } ] }' ``` **Option B -- Adjust via `unit_value`** (when there are multiple products): ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": 2023, "products": [ { "sku": "CURSO-001", "name": "Curso de culinária", "quantity": 1, "unit_value": 20812, "type": "digital" } ] }' ``` Response: ```json { "data": { "order": { "id": 4001, "status": "pendente" } } } ``` ## 4. Process the installment payment Submit the payment specifying the number of installments: ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/credit-card \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 4001, "customer_id": 2023, "payment_data": { "credit_card": { "token": "422146c7523a46119d6073ea56193913", "holder_document_number": "25226493029", "holder_name": "Junior Almeida", "installments": 3, "soft_descriptor": "MINHALOJA" } } }' ``` ```json { "data": { "order": { "id": 4001, "status": "autorizado" }, "payment": { "method": "creditcard", "installments": 3, "paid_at": "2025-03-15 14:30:00" } } } ``` On the customer's card statement it will show: **3x of R$ 69.37**. ## 5. Verify the result Query the order to see the value breakdown: ```bash curl --request GET \ --url https://api.sandboxappmax.com.br/v1/orders/4001 \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' ``` ```json { "data": { "order": { "id": 4001, "status": "aprovado", "total_paid": 20812, "amounts": { "sub_total": 20000, "shipping_value": 0, "discount": 0, "installment_fee": 812 } }, "payment": { "method": "creditcard", "installments": 3, "installments_amount": 6937 } } } ``` The `installment_fee` field shows exactly how much interest was charged (R$ 8.12). --- ## Summary | Step | What to do | Watch out for | |------|------------|---------------| | Query installments | `POST /v1/payments/installments` | Send `settings: true` to know the maximum number of installments | | Display options | Divide `total` by the number of installments | Indicate "interest-free" when `total == original_value` | | Create order | Send the value **with interest included** | Use `products_value` or adjust each `unit_value` | | Pay | Specify `installments` in the payment | The number of installments must match the value sent | --- Source: https://docs.appmax.com.br/en/guides/exemplo-checkout-completo.md # Checkout with multiple products This example shows a real-world e-commerce scenario: a cart with multiple items, shipping, discount, and payment -- including the correct value calculation for installments. > **The examples use sandbox URLs. For production, replace `sandboxappmax` with `appmax`.** > > ## Scenario The customer's cart contains: | Product | Qty | Unit price | Subtotal | |---------|-----|------------|----------| | T-shirt S | 2 | R$ 79.90 | R$ 159.80 | | Cap | 1 | R$ 45.00 | R$ 45.00 | - **Subtotal:** R$ 204.80 - **Shipping:** R$ 18.50 - **Discount coupon:** R$ 20.00 - **Total:** R$ 203.30 - **Payment:** 3 installments on credit card ## Prerequisites - Valid authentication token ([how to obtain](/en/guides/autenticacao)) - Customer's `customer_id` with registered address ([how to create](/en/api-reference/customers/criar-atualizar)) --- ## 1. Look up installments for the total value The base value for installment calculation is **subtotal + shipping - discount**: `20480 + 1850 - 2000 = 20330` cents ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/installments \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "installments": 3, "total_value": 20330, "settings": true }' ``` ```json { "data": { "installments": { "1": { "total": 20330 }, "2": { "total": 20736 }, "3": { "total": 21147 } }, "settings": { "modality": "PP", "max_installments": 12, "min_installment_value": 500 } } } ``` The customer chooses **3 installments** -> total with interest: **R$ 211.47** (`21147` cents). ## 2. Distribute interest across products The interest is R$ 8.17 (`21147 - 20330 + 2000 - 1850 = 817` additional cents on the original subtotal). Distribute proportionally: ```javascript const produtos = [ { sku: 'CAM-P', nome: 'Camiseta P', qtd: 2, valorOriginal: 7990 }, { sku: 'BONE-01', nome: 'Boné', qtd: 1, valorOriginal: 4500 } ]; const subtotalOriginal = 20480; // 159,80 + 45,00 const totalComJuros = 21147; const frete = 1850; const desconto = 2000; // Product value we need to send: // totalComJuros - frete + desconto = adjusted product value // Since we use products_value, we send the adjusted total const productsValue = totalComJuros - frete + desconto; // 21147 - 1850 + 2000 = 21297 ``` > **Use `products_value` to send the total product value with interest already included. This way you don't need to recalculate each `unit_value` individually.** > > ## 3. Create the order ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": 2023, "products_value": 21297, "shipping_value": 1850, "discount_value": 2000, "products": [ { "sku": "CAM-P", "name": "Camiseta P", "quantity": 2, "type": "physical" }, { "sku": "BONE-01", "name": "Boné", "quantity": 1, "type": "physical" } ] }' ``` ```json { "data": { "order": { "id": 6001, "status": "pendente" } } } ``` > **When using `products_value`, **do not provide** `unit_value` in the products. The two modes are mutually exclusive. See [calculation rules](/en/api-reference/orders/calculo-valor) for details.** > > ## 4. Process the payment ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/payments/credit-card \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 6001, "customer_id": 2023, "payment_data": { "credit_card": { "token": "422146c7523a46119d6073ea56193913", "holder_document_number": "25226493029", "holder_name": "Junior Almeida", "installments": 3, "soft_descriptor": "MINHALOJA" } } }' ``` ```json { "data": { "order": { "id": 6001, "status": "autorizado" }, "payment": { "method": "creditcard", "installments": 3, "paid_at": "2025-03-15 14:30:00" }, "upsell_hash": "6000114202503117156088040208561001715608804" } } ``` ## 5. Register the tracking code Since the order contains physical products, register the tracking code after shipping to release withdrawals: ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders/shipping-tracking-code \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "order_id": 6001, "shipping_tracking_code": "BR123456789XX" }' ``` ```json { "data": { "message": "tracking accepted" } } ``` ## 6. Offer an upsell (optional) After payment, offer a complementary product using the `upsell_hash`: ```bash curl --request POST \ --url https://api.sandboxappmax.com.br/v1/orders/upsell \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --data '{ "upsell_hash": "6000114202503117156088040208561001715608804", "products_value": 2990, "products": [ { "sku": "MEIA-01", "name": "Kit meias esportivas", "quantity": 1, "unit_value": 2990, "type": "physical" } ] }' ``` The upsell is automatically charged to the same card, without the customer needing to enter their details again. --- ## Complete flow diagram ```mermaid flowchart TD subgraph Frontend["🛒 Cart (front-end)"] A1["Look up installments"] -->|POST /v1/payments/installments| A2["Display options"] A2 --> A3["Customer chooses 3 installments"] end A3 --> B1 subgraph Backend["⚙️ Backend"] B1["Create order"] -->|POST /v1/orders
adjusted products_value| B2["Pay"] B2 -->|POST /v1/payments/credit-card
installments: 3| B3["Wait for webhook"] B3 -->|order_approved| B4["Confirm in your system"] B4 --> B5["Ship products"] B5 -->|POST /v1/orders/shipping-tracking-code| B6["Upsell (optional)"] B6 -->|POST /v1/orders/upsell| B7["End"] end classDef frontendClass fill:#e3f2fd,stroke:#1976d2,color:#0d47a1 classDef backendClass fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c class Frontend frontendClass class Backend backendClass ``` --- ## Value summary | Field | Value | Cents | |-------|-------|-------| | Subtotal (2 t-shirts + 1 cap) | R$ 204.80 | `20480` | | Shipping | R$ 18.50 | `1850` | | Discount | -R$ 20.00 | `2000` | | Interest (3x) | R$ 8.17 | `817` | | **Total charged** | **R$ 211.47** | **`21147`** | | Card installment | 3x R$ 70.49 | `7049` | --- Source: https://docs.appmax.com.br/en/guides/split-pagamentos.md # Payment split Payment split lets you divide the net amount of an order between a marketplace and one or more pre-registered recipients. This guide walks through the end-to-end flow and product rules. Each individual endpoint is documented on the reference pages linked at the bottom of each section. ## Flow overview The process has three blocks: 1. **Onboarding & KYC** — register the recipient and complete the mandatory facematch. 2. **Order split** — divide the order's net value between the marketplace and the recipients. 3. **Withdrawal** — check balances and request a payout (with or without anticipation). ```mermaid sequenceDiagram participant App as Your backend participant API as api.appmax.com.br/v1 participant KYC as Facematch (SMS) participant Recipient as Recipient rect rgb(227, 242, 253) Note over App,Recipient: 1. Onboarding & KYC App->>API: POST /recipient (account + company data) API-->>App: recipient_hash App->>API: POST /recipient/{hash}/facematch-link API->>KYC: generates link and sends SMS KYC-->>Recipient: facematch link Recipient->>KYC: completes facematch App->>API: GET /recipient/{hash}/status API-->>App: Onboarding completed end rect rgb(232, 245, 233) Note over App,Recipient: 2. Order split App->>API: POST /orders/{orderId}/split-order
(amounts per recipient_hash) API-->>App: Split order created successfully end rect rgb(243, 229, 245) Note over App,Recipient: 3. Withdrawal App->>API: GET /recipient/{hash}/balances API-->>App: available / to_release balances App->>API: GET /recipient/{hash}/withdraw-request/anticipation/simulate API-->>App: net amount + fee App->>API: POST /recipient/{hash}/withdraw-request/anticipation
or /withdraw-request/available API-->>App: withdraw_request_id + status end ``` ## Rules - **Partial refunds are not allowed** on split orders. Only full refunds. - **Split cannot be created or modified** on orders with `approved` status. Create the split before payment approval. - The split is calculated on the **net amount** of the order (after Appmax fees), not on the gross amount. Example: | Component | Amount | | ------------------------------ | ----------- | | Order (gross) | R$ 100.00 | | Net amount (after fees) | R$ 90.00 | | Split to recipients | R$ 40.00 | | Marketplace balance | R$ 50.00 | - All amounts in split and withdrawal payloads are always in **cents** (integers). ## Recipient status Recipient onboarding goes through three possible states, returned by `GET /recipient/{recipient_hash}/status`: | Status | Meaning | | -------------------------------- | -------------------------------------------------------------------- | | `Awaiting face match completion` | The recipient still needs to complete the facematch (KYC) via SMS. | | `Onboarding on verification` | Data + facematch received, under review by Appmax. | | `Onboarding completed` | Approved. The recipient can receive splits. | A `recipient_hash` can only be used in a split after it reaches `Onboarding completed`. For the full status reference — transitions, eligibility per state, and withdraw request (`WithdrawRequest`) statuses — see [Payment split status](/en/guides/split-status). ## Endpoints per stage ### Onboarding & KYC 1. [Create a recipient](/en/api-reference/split/criar-recebedor) — `POST /recipient` 2. [Create facematch link (KYC)](/en/api-reference/split/facematch-link) — `POST /recipient/{recipient_hash}/facematch-link` 3. [Get recipient status](/en/api-reference/split/consultar-recebedor) — `GET /recipient/{recipient_hash}/status` ### Order split 4. [Create order split](/en/api-reference/split/criar-split-pedido) — `POST /orders/{orderId}/split-order` ### Withdrawal 5. [Get recipient balances](/en/api-reference/split/saldos) — `GET /recipient/{recipient_hash}/balances` 6. [Simulate withdrawal anticipation](/en/api-reference/split/simular-antecipacao) — `GET /recipient/{recipient_hash}/withdraw-request/anticipation/simulate` 7. [Request withdrawal anticipation](/en/api-reference/split/antecipacao) — `POST /recipient/{recipient_hash}/withdraw-request/anticipation` 8. [Request withdrawal with available balance](/en/api-reference/split/saque-disponivel) — `POST /recipient/{recipient_hash}/withdraw-request/available` 9. [Get withdrawal request](/en/api-reference/split/consultar-solicitacao-saque) — `GET /withdraw-request/{withdrawRequestId}` ## Withdrawal vs anticipation There are two balance types and two distinct endpoints to withdraw them: | Balance type | Endpoint | Use | | ------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- | | `available` | `POST /recipient/{recipient_hash}/withdraw-request/available` | Already released balance. Direct withdrawal, no anticipation fee. | | `to_release` | `POST /recipient/{recipient_hash}/withdraw-request/anticipation` | Not-yet-released balance. Anticipated payout with fee applied. | Before requesting an anticipation, use the [simulation endpoint](/en/api-reference/split/simular-antecipacao) to show the recipient the net amount and applied fee — the simulation does not create any request. ## Authentication All split routes use the same authentication scheme described in [Authentication](/en/guides/autenticacao): `Authorization: Bearer ` with the **merchant** token obtained via `POST /oauth2/token`. **App** credentials do not work on these routes. ## See also > **Common questions about registration, editing, deletion, KYC and SMS are grouped in [FAQ — Payment split](/en/guides/split-perguntas-frequentes).** > > --- Source: https://docs.appmax.com.br/en/guides/split-status.md # Payment split status Consolidated reference for every status an integrator may observe in Appmax's payment split flow — **recipient** status (returned by `GET /recipient/{hash}/status`) and **withdraw request** status (returned by `withdraw-request` endpoints). Use this page as an enum dictionary: what each value means, when it appears, what to expect next, and the recommended action. For the full flow and code samples, see [Payment split](/en/guides/split-pagamentos). For recurring operational questions, see [FAQ](/en/guides/split-perguntas-frequentes). ## Recipient status Recipient status is returned by [`GET /recipient/{recipient_hash}/status`](/en/api-reference/split/consultar-recebedor) as a string in the `data` field. There are **three possible values**, always in English. | Status | Meaning | Eligible for split? | Expected next action | | -------------------------------- | ------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------- | | `Awaiting face match completion` | Recipient created, waiting for the user to complete the facematch via SMS. | No | Send or resend the [facematch link](/en/api-reference/split/facematch-link) and wait for the user to complete it on their phone. | | `Onboarding on verification` | Facematch received. KYC review in progress or held by a failed check. | No | Wait for automatic approval. If it remains here for more than 24-48 business hours, open a support ticket. | | `Onboarding completed` | Onboarding approved. Recipient is enabled to receive splits and withdraw. | Yes | Use the `recipient_hash` in [`POST /orders/{orderId}/split-order`](/en/api-reference/split/criar-split-pedido). | ### State transitions ```mermaid stateDiagram-v2 [*] --> Awaiting: POST /recipient Awaiting: Awaiting face match completion Awaiting --> Awaiting: POST /facematch-link resent Awaiting --> OnVerification: User completes facematch OnVerification: Onboarding on verification OnVerification --> Completed: Review approved OnVerification --> OnVerification: Review held (stays indefinitely) Completed: Onboarding completed Completed --> Completed: Terminal state note right of OnVerification Indefinite permanence when any check fails. No automatic retry. end note ``` ### Transitions that **do not** exist - There is no path back from `Onboarding completed` to any earlier state. Once approved, a recipient stays approved. - There is no path from `Onboarding on verification` back to `Awaiting face match completion`. Calling `POST /facematch-link` again while in verification **does not restart** the review — the request is accepted but the status does not change. - There is no public rejection or blocked state (`rejected`, `denied`, `blocked`). Rejected recipients remain frozen in `Onboarding on verification`. ### `Onboarding on verification` in detail This is the status that causes the most confusion because **it collapses two different scenarios under the same label**: 1. **Review in progress** — the facematch was received and the KYC pipeline is still processing. Typical window: minutes to a few hours. 2. **Review held by a failed check** — the pipeline finished processing, but one of the KYC criteria was not met. The recipient stays in this state **indefinitely** until manual action is taken by Appmax. The `GET /recipient/{hash}/status` endpoint **does not distinguish** between these two cases. There is no `reason` field, no `rejected` status, and no webhook to notify the change. #### Typical reasons for being held Among the criteria checked during onboarding: - CPF or CNPJ not validated against the Brazilian federal revenue service (Receita Federal) - CNPJ without the responsible party's CPF in the QSA (company ownership structure) - PEP (Politically Exposed Person) - OFAC (US Treasury sanctions list) - CSNU (UN Security Council sanctions list) - Facematch score below the minimum threshold - Liveness (proof-of-life) check failed - Facematch photo does not match the ID document Not every reason is surfaced to the integrator. What is returned is just the `Onboarding on verification` label. #### No automatic retry, no timeout The system **does not automatically reprocess** a recipient stuck in `Onboarding on verification`. There is no timeout to leave this state, no periodic retry, and no rejection notification. Calling `POST /facematch-link` again does not reactivate the document review — it only dispatches another facematch SMS, which on its own does not unlock the other checks. #### Recommended action - **Up to 24-48 business hours in `Onboarding on verification`**: wait. This is a normal processing window. - **Beyond that**: open a support ticket with Appmax including the `recipient_hash`. The internal team checks whether reprocessing, manual review or a definitive rejection applies. - **Do not re-create the recipient**: the CNPJ will return the `company document number already in use` error. - **Do not freeze the end user's flow**: surface the pending state to the merchant and offer an escape hatch while you wait. ## Withdraw request status Every withdraw request — whether via [available balance](/en/api-reference/split/saque-disponivel) or via [anticipation](/en/api-reference/split/antecipacao) — carries a `status` field that represents where it sits in the financial processing cycle. The immediate response from `POST /withdraw-request/*` endpoints **always returns `2` (`PENDING`)**, as an integer (ID). Subsequent transitions happen in the background on Appmax's side. To track the updated status, use [`GET /withdraw-request/{withdrawRequestId}`](/en/api-reference/split/consultar-solicitacao-saque) — which returns `status` already translated as a string (e.g. `pending`, `approved`, `refused`). There is no webhook that notifies transitions; tracking is always done by polling this endpoint. This section documents the **17 possible statuses** that may appear when querying a request, in reports, or during support tickets. ### Terminal statuses States that **do not transition further**. Once here, the request is done. | ID | Constant | Meaning | Integrator action | | -- | ----------- | ---------------------------------------------------------------------------- | ----------------- | | 1 | `REFUSED` | Request refused (validation error, insufficient balance after lock, review rejection, payment provider definitive failure). | Investigate the reason through support. If balance is available again, create a new request. | | 5 | `PAID` | Amount settled — funds left the Appmax account to the recipient's bank account. | None. Flow completed successfully. | ### In-progress statuses Intermediate states. The request is still being processed — wait for natural transition. | ID | Constant | Meaning | Integrator action | | -- | --------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------- | | 2 | `PENDING` | Created. Waiting for processing or internal approval. **This is the value returned in the immediate `POST` response.** | None. Wait. | | 3 | `APPROVED` | Approved internally. Waiting to be sent to the payment provider. | None. Wait. | | 4 | `PROCESSING` | Being processed by the provider (cash-out or bank transfer). | None. Wait. | | 7 | `WAITING_RETURN` | Waiting for payment provider response. | None. Wait. | | 8 | `INITIAL_ANALYSIS` | Under initial review before approval. | None. Wait. | | 12 | `PIX_INCLUSION_IN_RETRY` | PIX key inclusion in automatic retry with the provider. | None. Wait. | | 16 | `PIX_PROCESSING` | PIX payment being processed by the provider. | None. Wait. | | 17 | `PIX_VALIDATION_API_UNAVAILABLE` | The provider's PIX validation API is temporarily unavailable. | None. Wait, or open a ticket if it persists. | ### Statuses that require Appmax intervention States in which something has stalled and **depend on manual action** from Appmax to unblock. If the request stays here for more than a few hours, open a ticket. | ID | Constant | Meaning | Integrator action | | -- | ------------------------------ | ------------------------------------------------------------------------------------------------- | ----------------- | | 6 | `ON_HOLD` | Manual block (risk review, additional verification). | Open a ticket if it does not unblock within a reasonable window. | | 9 | `PENDING_ACCREDITATION` | Recipient not yet accredited at the cash-out provider. | Wait. Open a ticket if it persists. | | 10 | `APPROVED_BUT_NOT_INCLUDED` | Approved internally but not included in a payment batch. | Open a ticket. | | 11 | `PIX_ACCOUNT_VALIDATION_FAIL` | Provider failed to validate the recipient's PIX key. | Open a ticket to validate bank data. | | 13 | `PIX_EXPIRED_INCLUSION` | PIX inclusion expired before completing. | Open a ticket. | | 14 | `PIX_INCLUDED_BUT_REPROVED` | PIX was included but rejected by the provider. | Open a ticket. | | 15 | `PIX_MANUAL_PAYMENT` | Requires manual PIX payment by Appmax's finance team. | Open a ticket. | ### `withdrawal_blocked` — invisible lateral state There is a withdrawal block that **does not show up in the recipient status or in the withdraw request itself**. It is a lateral flag that Appmax can activate on the recipient's account for reasons such as risk, investigation or regulatory requirement. The practical effect: - The recipient reports `Onboarding completed` normally. - Balances exist and can be queried. - But every attempt to create a withdraw request returns **HTTP 403** with `Withdraw not allowed`. If you get `403` on `POST /withdraw-request/available` or `POST /withdraw-request/anticipation` for a recipient that should be eligible, this is most likely the cause — open a support ticket including the `recipient_hash`. Another case that also returns 403 (or 409 `Withdraw request in progress`) is when **another request is already in progress** for the same recipient in the same processing window. Wait for the previous one to finish before creating a new one. ## Consolidated eligibility per recipient status Cross-reference table showing, for each recipient status, what is and is not allowed: | Recipient status | Receive split on a new order | Has balances (`GET /balances`) | Withdraw available balance | Anticipate to-release balance | | -------------------------------- | ---------------------------- | ------------------------------ | -------------------------- | ----------------------------- | | `Awaiting face match completion` | No | No (returns 404) | No | No | | `Onboarding on verification` | No | No (returns 404) | No | No | | `Onboarding completed` | Yes | Yes | Yes, unless `withdrawal_blocked` or withdraw in progress | Yes, unless `withdrawal_blocked` or withdraw in progress | **About `GET /balances`**: before `Onboarding completed`, the [`GET /recipient/{hash}/balances`](/en/api-reference/split/saldos) endpoint returns `404 Balance not found`. This does **not** mean the recipient does not exist — it means balances have not been provisioned yet. Use `GET /status` as the primary source of recipient existence and eligibility. ## What the status does **not** tell you ### Split has no status of its own The order-split entity **does not have a separate lifecycle**. A split's effective status follows the [parent order status](/en/guides/status-pedidos): - Order `pending` → split created, waiting for approval. - Order `approved` → split consolidated, amounts enter the recipients' balance flow. - Order `cancelled` or `refunded` → split discarded together with the order. There is no `GET /split-order/{id}/status` endpoint and no `split_status` field. There is no split-specific webhook either. To know whether a split was effectively applied, check the order status. ### No webhook for recipient status changes Today there is **no event** that notifies transitions of a recipient between `Awaiting face match completion`, `Onboarding on verification` and `Onboarding completed`. The integrator must poll the `GET /status` endpoint. There is also no webhook for `WithdrawRequest` transitions. Recommendations: - Poll at reasonable intervals (for example every 1-5 minutes during active onboarding, reducing cadence after the first hour). - Avoid aggressive polling (more than 1 request per second) — the API has [rate limits](/en/guides/rate-limit). - For recipients stuck in `Onboarding on verification` for more than 24 business hours, stop polling and escalate to support. ### Partial refunds are blocked on split orders Orders with split **only accept full refunds**. Attempting a [partial refund](/en/api-reference/refunds/criar-estorno) on an order that has a split returns a validation error. This is a product rule, not a status — detailed in [Create order split](/en/api-reference/split/criar-split-pedido). ## Questions only support can answer Some data is not exposed in the API and is not publicly documented — it requires a support ticket to be answered case by case: - **Which KYC provider is used for facematch and document checks.** Not exposed in the API. - **Why a specific recipient landed in `Onboarding on verification`.** The detailed reason (which check failed) is not returned. - **SLA for a specific recipient's review to complete.** Only the typical window (24-48 business hours) is public. - **Expected transition time for a specific `WithdrawRequest` between states.** Varies by provider and banking window. - **Reason for `withdrawal_blocked` on an account.** Requires a ticket with the `recipient_hash`. In all these cases, always include the `recipient_hash` (or `withdraw_request_id` when applicable) — it lets support locate the record immediately. ## See also - [Payment split — overview](/en/guides/split-pagamentos) - [FAQ — Payment split](/en/guides/split-perguntas-frequentes) - [Get recipient status](/en/api-reference/split/consultar-recebedor) - [Get withdrawal request](/en/api-reference/split/consultar-solicitacao-saque) - [Create order split](/en/api-reference/split/criar-split-pedido) - [Order status](/en/guides/status-pedidos) --- Source: https://docs.appmax.com.br/en/guides/bancos-homologados.md # Approved banks Full list of banks and financial institutions approved for use on the platform. Use this page as a quick reference when sending the `bankAccount` object in [Create a recipient](/en/api-reference/split/criar-recebedor): each institution is identified by its name and its **bank code (COMPE)**, which is the value expected in the `bankAccount.bank` field. > **The list is updated as new institutions are approved. Last update: **2026-05-07**.** > > ## Account types The `bankAccount.bankAccountType` field accepts one of the codes below. | Code | Type | Description | | :--: | :--------------- | :----------------------------------------------------------------------------- | | `CC` | Checking account | Traditional bank account used for day-to-day transactions. | | `CD` | Digital account | Fully digital account, with no physical branch. | | `PG` | Payment account | Prepaid account intended exclusively for electronic money movement. | | `PP` | Savings account | Account intended for holding funds with monthly interest. | ## Usage example ```json { "bankAccount": { "bank": 104, "agency": "1234-0", "account": "12345-2", "bankAccountType": "CC" } } ``` ## Approved bank list | Bank code | Bank name | | :-------: | :-------- | | 1 | Banco do Brasil S.A. | | 3 | BANCO DA AMAZONIA S.A. | | 4 | Banco do Nordeste do Brasil S.A. | | 21 | BANESTES S.A. BANCO DO ESTADO DO ESPIRITO SANTO | | 25 | Banco Alfa S.A. | | 33 | BANCO SANTANDER (BRASIL) S.A. | | 36 | Banco BRADESCO BBI | | 37 | Banco do Estado do Pará S.A. | | 41 | Banco do Estado do Rio Grande do Sul S.A. | | 47 | Banco do Estado de Sergipe S.A. | | 69 | Banco Crefisa S.A. | | 70 | BRB - BANCO DE BRASILIA S.A. | | 77 | Banco Inter S.A. | | 79 | PICPAY BANK - BANCO MÚLTIPLO S.A | | 81 | BancoSeguro S.A. | | 82 | BANCO TOPÁZIO S.A. | | 84 | SISPRIME DO BRASIL - COOPERATIVA DE CRÉDITO | | 85 | Cooperativa Central de Crédito - Ailos | | 89 | CREDISAN COOPERATIVA DE CRÉDITO | | 93 | PÓLOCRED SOCIEDADE DE CRÉDITO AO MICROEMPREENDEDOR E À EMPRESA DE PEQUENO PORTE LTDA. | | 94 | Banco Finaxis S.A. | | 97 | Credisis - Central de Cooperativas de Crédito Ltda. | | 99 | UNIPRIME CENTRAL NACIONAL - CENTRAL NACIONAL DE COOPERATIVA DE CREDITO | | 104 | CAIXA ECONOMICA FEDERAL | | 107 | Banco Bocom BBM S.A. | | 120 | BANCO RODOBENS S.A. | | 125 | BANCO GENIAL S.A. | | 130 | CARUANA S.A. - SOCIEDADE DE CRÉDITO, FINANCIAMENTO E INVESTIMENTO | | 133 | CONFEDERAÇÃO NACIONAL DAS COOPERATIVAS CENTRAIS DE CRÉDITO E ECONOMIA FAMILIAR E SOLIDÁRIA - CRESOL CONFEDERAÇÃO | | 136 | COOPERATIVA CENTRAL DE CRÉDITO UNICRED DO BRASIL - UNICRED DO BRASIL | | 174 | PEFISA S.A. - CRÉDITO, FINANCIAMENTO E INVESTIMENTO | | 197 | STONE INSTITUIÇÃO DE PAGAMENTO S.A. | | 208 | Banco BTG Pactual S.A. | | 212 | Banco Original S.A. | | 213 | Banco Arbi S.A. | | 218 | Banco BS2 S.A. | | 224 | Banco Fibra S.A. | | 237 | Banco Bradesco S.A. | | 246 | Banco ABC Brasil S.A. | | 260 | NU PAGAMENTOS S.A. - INSTITUIÇÃO DE PAGAMENTO | | 274 | BMP SOCIEDADE DE CRÉDITO AO MICROEMPREENDEDOR E A EMPRESA DE PEQUENO PORTE LTDA. | | 280 | WILL FINANCEIRA S.A. CRÉDITO, FINANCIAMENTO E INVESTIMENTO - EM LIQUIDAÇÃO EXTRAJUDICIAL | | 299 | BANCO AFINZ S.A. - BANCO MÚLTIPLO | | 301 | DOCK INSTITUIÇÃO DE PAGAMENTO S.A. | | 310 | VORTX DISTRIBUIDORA DE TITULOS E VALORES MOBILIARIOS LTDA. | | 318 | Banco BMG S.A. | | 322 | Cooperativa de Crédito Rural de Abelardo Luz - Sulcredi/Crediluz | | 323 | MERCADO PAGO INSTITUIÇÃO DE PAGAMENTO LTDA. | | 329 | QI Sociedade de Crédito Direto S.A. | | 332 | ACESSO SOLUÇÕES DE PAGAMENTO S.A. - INSTITUIÇÃO DE PAGAMENTO | | 335 | Banco Digio S.A. | | 336 | Banco C6 S.A. | | 341 | ITAÚ UNIBANCO S.A. | | 348 | Banco XP S.A. | | 364 | EFÍ S.A. - INSTITUIÇÃO DE PAGAMENTO | | 376 | BANCO J.P. MORGAN S.A. | | 380 | PICPAY INSTITUIçãO DE PAGAMENTO S.A. | | 383 | EBANX INSTITUICAO DE PAGAMENTOS LTDA. | | 389 | Banco Mercantil do Brasil S.A. | | 396 | MAGALUPAY INSTITUIÇÃO DE PAGAMENTO S.A. | | 401 | IUGU INSTITUIÇÃO DE PAGAMENTO S.A. | | 403 | CORA SOCIEDADE DE CRÉDITO, FINANCIAMENTO E INVESTIMENTO S.A. | | 406 | ACCREDITO - SOCIEDADE DE CRÉDITO DIRETO S.A. | | 413 | BANCO BV S.A. | | 414 | LEND SOCIEDADE DE CRÉDITO DIRETO S.A. | | 422 | Banco Safra S.A. | | 435 | DELFINANCE SOCIEDADE DE CREDITO DIRETO S.A. | | 448 | HEMERA DISTRIBUIDORA DE TÍTULOS E VALORES MOBILIÁRIOS LTDA. | | 450 | FITS INSTITUIÇÃO DE PAGAMENTO S.A. | | 457 | UY3 SOCIEDADE DE CRÉDITO DIRETO S/A | | 461 | ASAAS GESTÃO FINANCEIRA INSTITUIÇÃO DE PAGAMENTO S.A. | | 470 | CDC SOCIEDADE DE CRÉDITO DIRETO S.A. | | 481 | SUPERLÓGICA SOCIEDADE DE CRÉDITO DIRETO S.A. | | 487 | DEUTSCHE BANK S.A. - BANCO ALEMAO | | 509 | Max IP (Celcoin) | | 517 | PAGUEVELOZ INSTITUIÇÃO DE PAGAMENTO LTDA. | | 529 | PINBANK BRASIL INSTITUIÇÃO DE PAGAMENTO S.A. | | 542 | CLOUDWALK INSTITUIÇÃO DE PAGAMENTO E SERVICOS LTDA | | 590 | REPASSES FINANCEIROS E SOLUCOES TECNOLOGICAS INSTITUICAO DE PAGAMENTO S.A. | | 594 | ASA SOCIEDADE DE CRÉDITO FINANCIAMENTO E INVESTIMENTO S.A. | | 595 | IFOOD PAGO INSTITUIÇÃO DE PAGAMENTO S.A. | | 600 | Banco Luso Brasileiro S.A. | | 604 | Banco Industrial do Brasil S.A. | | 611 | Banco Paulista S.A. | | 612 | Banco Guanabara S.A. | | 613 | Omni Banco S.A. | | 623 | Banco Pan S.A. | | 633 | Banco Rendimento S.A. | | 634 | BANCO TRIANGULO S.A. | | 637 | BANCO SOFISA S.A. | | 643 | Banco Pine S.A. | | 654 | BANCO DIGIMAIS S.A. | | 655 | Banco Votorantim S.A. | | 660 | PAGME INSTITUIÇÃO DE PAGAMENTO LTDA. | | 707 | Banco Daycoval S.A. | | 741 | BANCO RIBEIRAO PRETO S.A. | | 743 | Banco Semear S.A. | | 745 | Banco Citibank S.A. | | 748 | BANCO COOPERATIVO SICREDI S.A. | | 755 | Bank of America Merrill Lynch Banco Múltiplo S.A. | | 756 | BANCO COOPERATIVO SICOOB S.A. - BANCO SICOOB | | 783 | SWAP INSTITUIÇÃO DE PAGAMENTO S.A. | | 4740876 | COMPANHIA BRASILEIRA DE SOLUÇÕES E SERVIÇOS | | 4833541 | SUPERLOGICA TECNOLOGIAS S.A. | | 10506341 | ENOPP SERVIÇOS DE GESTÃO DE NEGÓCIOS E PROJETOS LTDA | | 10878448 | PAYPAL DO BRASIL INSTITUIÇÃO DE PAGAMENTO LTDA. | | 13966572 | CAPPTA INSTITUICAO DE PAGAMENTO S.A | | 15185132 | PAYLEVEN TECNOLOGIA LTDA | | 18727053 | PAGAR.ME PAGAMENTOS S.A. | | 23613543 | TECPAY S.A. | | 25021356 | DLOCAL BRASIL PAGAMENTOS LTDA | | 26356125 | ZIG TECNOLOGIA S.A. | | 28494032 | ALPE INTERMEDIACAO DE NEGOCIOS S.A. | | 35813685 | QGX PAGAMENTOS S.A. | ## Next steps - [Create a recipient](/en/api-reference/split/criar-recebedor) — send `bankAccount` at registration time. - [Payment split](/en/guides/split-pagamentos) — overview of the full flow. - [Payment split status](/en/guides/split-status) — what each recipient status means. --- Source: https://docs.appmax.com.br/en/guides/split-perguntas-frequentes.md # FAQ — Payment split Recurring questions from integrators about recipient onboarding, KYC, splits, and withdrawals. For the full flow and endpoint references, see [Payment split](/en/guides/split-pagamentos). ## Overview and flow ### What is the correct flow to register a recipient? Follow these steps in order: 1. `POST /recipient` — returns `recipient_hash`. 2. `POST /recipient/{recipient_hash}/facematch-link` with the recipient's phone — triggers the SMS with the facematch link. 3. The recipient completes the facematch using the link received on their phone. 4. `GET /recipient/{recipient_hash}/status` — poll until it returns `Onboarding completed`. 5. From that point on, use the `recipient_hash` in `POST /orders/{orderId}/split-order`. ### Do I need to register the marketplace as a recipient? No. Recipients are only the partners that receive split amounts. The marketplace is the operating account that calls the APIs — it does not register itself as a recipient. ### Does the recipient need to log into the Appmax dashboard? No. The recipient flow is 100% API-based. The recipient does not access the dashboard and does not request withdrawals directly. The marketplace account executes withdrawals and anticipations on behalf of each recipient via the withdrawal endpoints. ## Amounts, fees and split calculation ### How is the Appmax fee charged when I use split? The fee is charged to the marketplace on top of the gross order amount and is deducted automatically by Appmax — **you do not send the fee in the payload** of `POST /orders/{orderId}/split-order`. The applied rate is agreed commercially between the merchant and Appmax. ### What is `partner_total`? `partner_total` is the order amount **minus the Appmax fees** — that is, the net balance available to distribute among the recipients of the split. The sum of the `amount` values sent in the split is capped at this value. ### What happens if the sum of `amount` exceeds `partner_total`? The endpoint **does not return an error**. Appmax performs a proportional split capped at `partner_total`: the first recipients receive the requested value and the last one receives only the residual available. Example: a R$ 100.00 order with a 5% fee → `partner_total` = R$ 95.00. If you send two recipients with R$ 50.00 each (summing R$ 100.00, above the cap): - Recipient 1: **R$ 50.00** (requested value) - Recipient 2: **R$ 45.00** (residual after `partner_total` is exhausted) Since there is no error or warning, it is easy for the last recipient to receive less than expected without anyone noticing. Size your splits assuming there is always a fee applied. ### Can I choose which recipient absorbs the order fee? No. The fee is applied to the marketplace as a whole, and `partner_total` is a single cap shared by all recipients — there is no way to flag in the payload which one absorbs the cost. To work around this, calculate the split values leaving headroom for the fee. ### Is there an endpoint to query the merchant fee before creating the order? No. There is no public route to query the fee or `partner_total` upfront. The rate is agreed commercially between the merchant and Appmax — if you need the exact value for your integration, align with the commercial team that owns the account. ## Recipient registration ### Can I edit a recipient via API? No. Once created, the recipient's data cannot be changed via API — there is no `PATCH` or `PUT` route. The fields submitted at creation time are used for validation against regulatory bodies. If you need to fix any field, open a ticket with Appmax support — the change is handled internally on a case-by-case basis. ### Can I delete a recipient via API? No. Deletion is handled internally by Appmax upon request from the marketplace through support. There is no public delete route. ### I get "O valor indicado para o campo company.company document number já se encontra utilizado" when re-submitting. What should I do? Each CNPJ maps to exactly one recipient on the platform. If the CNPJ has already been used — even if the previous record has wrong data — you cannot re-create it through the API. For **tests**, use a different CNPJ. In **production**, contact support to correct or remove the previous record. ## Facematch and KYC ### Why don't I get the facematch SMS in sandbox? The sandbox environment does not send SMS. Test the facematch dispatch in production. ### Is the phone sent in `POST /recipient` the same one that receives the facematch SMS? No. The SMS is dispatched by the `POST /recipient/{recipient_hash}/facematch-link` call, using the `phone` field in that request body. This number **does not** have to match the `account.phone` provided at creation — use the number that should actually receive the SMS. ## Status and eligibility ### When is the recipient actually ready to receive splits? Only when `GET /recipient/{recipient_hash}/status` returns `Onboarding completed`. In the `Awaiting face match completion` and `Onboarding on verification` states the recipient cannot yet be used in splits. ### What does the `Onboarding on verification` status mean? The status is **derived from the outcome of the KYC checks** — it is not a field flipped by hand. A recipient lands here when the facematch has already been received but one of the onboarding checks held automatic approval back. The checks applied include, among others: - CPF and CNPJ validation against the Brazilian federal revenue service (Receita Federal) - PEP, OFAC and CSNU (international sanctions) lists - QSA validation for the CNPJ — the provided CPF must appear in the company's ownership structure - Facematch score below the minimum threshold - Liveness (proof-of-life) check failed - Face on the facematch does not match the document **Important:** this status **does not distinguish "still under review" from "permanently rejected"**. There is no automatic retry and no timeout for exiting this state — once a check fails, the recipient stays frozen in `Onboarding on verification` until manual action is taken. **Recommended action:** if the recipient remains in this status for more than **24 to 48 business hours**, open a support ticket with Appmax to check whether it needs reprocessing or was rejected. Do not sit in an infinite polling loop waiting for an automatic transition, do not try to re-create the recipient (the CNPJ will hit the "already in use" error), and do not leave your end-user's flow hanging without an escape hatch — surface the pending state to the merchant while you wait for a response. > **See also** > > Full reference of the three recipient statuses, available transitions, and eligibility by state in [Payment split status](/en/guides/split-status). --- Source: https://docs.appmax.com.br/en/guides/ia.md # AI integration Our documentation is already prepared for AI agents. Let Claude, Cursor or any **Model Context Protocol (MCP)** client write the code for you — reading the API reference in real time, without ever leaving the editor. ## Why use AI? Integrating payments means reading a lot of documentation. Authentication, order creation, card tokenization, webhooks, error codes, rate limits, installment math... With the official Appmax MCP, your agent handles all of that on its own: - **Less back and forth** — the agent queries the documentation on its own while writing code - **Instant diagnostics** — HTTP errors identified immediately, with root cause and suggested fix - **Ready-to-use code** — snippets in curl, Node, Python, PHP, and Go with environment-specific URLs - **Guided onboarding** — personalized checklists and typed webhook schemas ## In action ### Build from scratch in minutes Ask for a full checkout integration — customer, order, Pix and card payment — and the agent writes everything while querying the Appmax API in real time. ### Automatic error diagnosis The agent hits an HTTP 401 on `/v1/customers`. Instead of guessing, it calls `diagnose_error` and pinpoints the cause: app credentials were used where merchant credentials were expected. Fix in seconds. ### Webhooks with no surprises From "I need to receive payment confirmation", the agent queries the typed schema for `order_approved` via `get_webhook_schema` and builds a complete handler with correct types. ### Precision payload edits "Add Pix as a payment method in my current flow" → the agent validates the payload with `validate_payload` before sending, and makes the right diff preserving the rest. ## What your agent can do There are **13 tools** the agent calls on its own during the conversation. Here is a summary by category — for the full table with descriptions and detailed usage examples, see the [setup page](/en/llms). **Documentation** - `search_docs` — semantic search across the documentation - `list_pages` — list available pages with prefix filter - `get_page` — get the full content of a page - `get_full_docs` — entire documentation as plain text - `check_health` — server status and stats **Diagnostics** - `diagnose_error` — HTTP error root cause (401/422/429/500) - `validate_payload` — validate JSON payload against endpoint schema - `validate_order_total` — verify order total calculation - `validate_installation_flow` — audits the installation flow implementation from project snippets **Code generation** - `generate_code_snippet` — snippets in curl/Node/Python/PHP/Go - `get_integration_flow` — step-by-step flow (checkout, installation, subscription) **Onboarding and webhooks** - `get_onboarding_checklist` — admin checklist by integration type - `get_webhook_schema` — typed schema + example payload for 28 events ## Compatible clients Any client that implements the [Model Context Protocol](https://modelcontextprotocol.io/): - **Claude Code** and **Claude Desktop** - **Cursor** - **Windsurf** - **VS Code** (MCP extension / Copilot) - **Cline**, **Aider**, **Continue** and others ## Open standard, no lock-in Our server implements the [Model Context Protocol](https://modelcontextprotocol.io/) — the open standard maintained by Anthropic for connecting AI agents to external data sources and tools. Any compatible client connects without proprietary adapters. ## Next steps - [Set up the MCP](/en/llms): Full configuration, tool table, and detailed usage examples. - [Quickstart](/en/quickstart): Prefer to write code by hand? Jump straight into the quickstart. --- Source: https://docs.appmax.com.br/en/guides/ia-ferramentas.md # MCP tool reference The 13 tools that the Appmax MCP server exposes to AI agents. Each entry shows: what you ask, what the agent calls under the hood, and what it returns. > **Prerequisite** > > To use these tools, your agent must be connected to the MCP server. See the [configuration](/en/llms#configuration). ## Documentation ### `search_docs` · semantic search **You ask:** *"How does the payment confirmation webhook work?"* **The agent calls:** `search_docs({ "query": "webhook payment confirmation" })` **Returns:** relevant excerpts from the webhooks and order status guides. The agent answers grounded in real content — no hallucinated endpoints or fields. --- ### `list_pages` · enumerate what exists **You ask:** *"What payment methods does Appmax support?"* **The agent calls:** `list_pages({ "prefix": "api-" })` **Returns:** all 15 API Reference pages — including `api-apple-pay`, `api-pix`, `api-boleto`, `api-cartao-de-credito`, `api-tokenizacao`, `api-parcelas`. Deterministic, no search ranking dependency. --- ### `get_page` · full guide content **You ask:** *"I need to implement card tokenization from scratch."* **The agent calls:** `get_page({ "page": "api-tokenizacao" })` **Returns:** the complete guide in markdown — endpoints, required fields, examples and edge cases. --- ### `get_full_docs` · entire documentation **You ask:** *"Load all of the Appmax documentation into context."* **The agent calls:** `get_full_docs({ "lang": "en" })` **Returns:** ~140 KB of plain text with every page. Use sparingly — prefer `search_docs` + `get_page` for targeted lookups. --- ### `check_health` · server status **The agent calls:** `check_health({})` **Returns:** server status, version, available tools, and stats (page count by language, corpus size). ## Diagnostics ### `diagnose_error` · HTTP error root cause **You ask:** *"I'm getting 401 on `/v1/customers`, what is it?"* **The agent calls:** `diagnose_error({ "status_code": 401, "endpoint": "/v1/customers", "credential_type": "app" })` **Returns:** *"You're using app credentials on a merchant endpoint. Use the merchant token."* — with a link to the authentication guide. **Scenarios covered:** 401 (swapped credentials, expired token), 403 (wrong URL), 404 (resource not found), 422 (incomplete flow, invalid payload), 429 (rate limit), 500 (health check failed), 502 (webhook failed), 503 (service unavailable). --- ### `validate_payload` · validate payload before sending **You ask:** *"Validate this JSON before I send it to the create order API"* **The agent calls:** `validate_payload({ "endpoint": "create_order", "payload": "{...}" })` **Returns:** list of issues — float instead of cents, `products_value` and `unit_value` used together, missing required fields. Prevents the 422 before calling the API. **Supported endpoints:** `create_customer`, `create_order`, `pay_credit_card`, `pay_pix`, `pay_boleto`, `pay_apple_pay`, `tokenize`, `refund`, `create_recurrence`. --- ### `validate_order_total` · verify order calculation **You ask:** *"The order total should be R$214.80 but the API rejects it"* **The agent calls:** `validate_order_total({ "products": [...], "shipping_value": 1500, "expected_total": 21480 })` **Returns:** detailed breakdown (subtotal + shipping - discount = total), comparison with the provided value, and detection of common errors (float, mutual exclusion, rounding). --- ### `validate_installation_flow` · audit the installation flow implementation **You ask:** *"Review whether my installation flow is correct"* **The agent calls:** `validate_installation_flow({ "snippets": [{ "step": "app_token", "code": "..." }, { "step": "authorize", "code": "..." }, { "step": "validation_url", "code": "..." }], "environment": "sandbox" })` **Returns:** a per-step report with status (OK, Issues, Missing), evidence for each check (correct base URL for the environment, HTTP method, headers, payload, `external_id` format, etc.) and a list of next steps. It detects the most frequent mistakes: `app_id` sent as the Numerical ID instead of the UUID, missing `url_callback`, a validation URL handler that only accepts GET, a response shaped as `{"status":"ok"}` instead of `{"external_id":""}`, a hardcoded `external_id`, and merchant credentials used in place of the app ones. **Supported steps:** `app_token` (`POST /oauth2/token` with the APP credentials), `authorize` (`POST /app/authorize`), `redirect` (browser redirect to the Appmax panel), `generate` (`POST /app/client/generate`), `validation_url` (the handler the integrator exposes for the health check). ## Code generation ### `generate_code_snippet` · snippets in 5 languages **You ask:** *"Give me the Python code to create a Pix payment in sandbox"* **The agent calls:** `generate_code_snippet({ "endpoint": "pay_pix", "language": "python", "environment": "sandbox" })` **Returns:** ready-to-use code with the correct URL, headers, body, and notes — in curl, Node.js, Python, PHP, or Go. **Supported endpoints:** `auth`, `auth_app`, `create_customer`, `create_order`, `get_order`, `upsell`, `pay_credit_card`, `pay_pix`, `pay_boleto`, `pay_apple_pay`, `tokenize`, `installments`, `refund`, `create_recurrence`, `app_authorize`, `app_generate`. --- ### `get_integration_flow` · step-by-step flow **You ask:** *"What's the endpoint sequence to install my app?"* **The agent calls:** `get_integration_flow({ "flow": "app_installation", "environment": "sandbox" })` **Returns:** wizard with the 4 steps (token → authorize → redirect → generate), environment-specific URLs, per-step prerequisites, and health check notes. **Available flows:** `app_installation`, `checkout_credit_card`, `checkout_pix`, `checkout_boleto`, `subscription`, `refund`, `upsell`. ## Onboarding and webhooks ### `get_onboarding_checklist` · admin checklist **You ask:** *"I'm starting from scratch, what do I need to do to integrate?"* **The agent calls:** `get_onboarding_checklist({ "integration_type": "checkout_proprio", "stage": "planning" })` **Returns:** personalized checklist — create account, create app, configure URLs, get credentials, implement flow, test, publish. Each item links to the relevant guide. **Integration types:** `checkout_proprio`, `plataforma_publica`, `recorrencia`, `upsell`. **Stages:** `planning`, `development`, `sandbox_testing`, `production_ready`. --- ### `get_webhook_schema` · typed event schemas **You ask:** *"I need the TypeScript types for the order approved webhook"* **The agent calls:** `get_webhook_schema({ "event": "order_approved" })` **Returns:** complete schema with all typed fields (`order_id: int`, `total: int`, `payment_info.credit_card.installments: int`, etc.) + example JSON payload. The agent generates TS/Go/Python types from this. **Event types:** `order` (18 events), `customer` (3), `payment` (2), `subscription` (5). Call with no parameters to see the full list. > **Native multi-language** > > All documentation tools accept a `lang` parameter (`pt` or `en`). Your team works in the documentation's native language, with no translation step in between. --- Source: https://docs.appmax.com.br/en/guides/faq.md # FAQ ## Credentials > **What is the difference between app credentials and merchant credentials?** > > These are two pairs of `client_id` and `client_secret` with completely different purposes: > > | | App credentials | Merchant credentials | > | --- | --- | --- | > | **Obtained from** | Developer dashboard, when creating the app | Returned by `/app/client/generate` | > | **Used for** | Installation flow only | Transactional operations (customers, orders, payments) | > | **How many exist** | 1 pair per app | 1 pair per merchant that installed the app | > > Both use the same authentication endpoint (`POST /oauth2/token`), but the generated token will have different permissions. If you receive `401` when creating customers or orders, you are likely using the app credentials instead of the merchant credentials. > > See the [authentication guide](/en/guides/autenticacao) for more details. > **Can I use app credentials to create orders?** > > **No.** App credentials (`client_id` and `client_secret` obtained when creating the application) are used exclusively for the installation flow. To create customers, orders, and payments, you need the merchant credentials, which are generated at the end of the installation flow via `POST /app/client/generate`. ## Authentication and tokens > **What is the JWT token's validity period?** > > The API access token (JWT) is valid for **1 hour**. > > The `client_id` and `client_secret` are never changed. New ones can only be generated by performing new installations, and existing ones can only be deactivated by uninstalling the app. > **What should I do when a 403 error occurs on the /oauth2/token route?** > > This error occurs when the call is being made to the wrong endpoint. The authentication route is separate from the API: > > - **Authentication:** `https://auth.sandboxappmax.com.br/oauth2/token` > - **API:** `https://api.sandboxappmax.com.br` > > Correct example: > > ```bash curl --location 'https://auth.sandboxappmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=CLIENT_ID' \ --data-urlencode 'client_secret=CLIENT_SECRET' ``` > **I received a 401 when calling the API. What could it be?** > > The most common causes of `401` errors: > > 1. **Wrong credentials:** you are using app credentials instead of merchant credentials (or vice versa). > 2. **Expired token:** the JWT token is valid for 1 hour. Generate a new one with the same credentials. > 3. **App token on transactional route:** if you are calling customer, order, or payment routes, use the token generated with the merchant credentials. > > See the [common errors table](/en/guides/autenticacao#common-credential-errors) for quick diagnosis. > **What should I do when a 401 error occurs when creating a customer?** > > Check: > > 1. Whether the token is valid and not expired. > 2. Whether the token was generated with the **merchant credentials** (not the app credentials) after the installation process. ## App installation > **What should I do when a 500 error occurs during app authentication?** > > This error usually happens when the installation flow is not followed correctly, specifically when the redirect and authorization step is missing. > > The correct flow is: > > 1. **Obtain the app token:** `POST https://auth.sandboxappmax.com.br/oauth2/token` > 2. **Generate the authorization hash:** `POST https://api.sandboxappmax.com.br/app/authorize` > 3. **Redirect the user:** `https://breakingcode.sandboxappmax.com.br/appstore/integration/HASH` > 4. **Generate the merchant credentials:** `POST https://api.sandboxappmax.com.br/app/client/generate` > > The error occurs when the redirect to the authorization step (step 3) is missing. > **How does the integration identify the store performing the installation?** > > Identification occurs through the user's login on the external platform. > > When clicking "Install", a token is generated via `POST https://api.appmax.com.br/app/authorize`. The merchant is redirected to `https://admin.appmax.com.br/appstore/integration/TOKEN_GERADO`, where they enter the store name and select the company registered with Appmax. > > After authorization, the hash is used to generate credentials via `POST https://api.appmax.com.br/app/client/generate`. > > **Note:** The hash can only be used once, but the generated credentials are valid indefinitely until the app is uninstalled. > **What is the difference between external_key and external_id?** > > | Field | Who defines it | Usage | Can repeat? | > | --------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------- | > | `external_key` | External platform / merchant's system | Identify the origin of the installation | Yes | > | `external_id` | Generated by the integrator on the [validation URL health check](/en/guides/instalacao#health-check) | UUID that binds the installation to the store; later used as `external-id` on CDN calls | No | > > **They cannot be the same**, as each field has a distinct purpose: > - `external_key`: identifies the origin in the platform/client context. > - `external_id`: confirmation of the app installation. > > For the use of `external_id` after installation (header `external-id` on CDN calls, parameter of `AppmaxScripts.init`), see [`external-id`](/en/guides/external-id). ## Webhooks > **What are the webhook errors and their meanings?** > > | Code | Cause | > | ---- | ----------------------------------------- | > | `502` | Webhook URL registered incorrectly | --- Source: https://docs.appmax.com.br/en/api-reference/introduction.md # API Introduction ## Base URLs Appmax provides two environments for integration: | Environment | Authentication | API | | ----------- | ------------------------------------- | ------------------------------------ | | Sandbox | `https://auth.sandboxappmax.com.br` | `https://api.sandboxappmax.com.br` | | Production | `https://auth.appmax.com.br` | `https://api.appmax.com.br` | ## Authentication All API requests (except token retrieval) must include the `Authorization` header with a valid Bearer token. ```bash Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp... ``` To obtain a token, send a `POST` request to the authentication endpoint: ```bash curl --location 'https://auth.appmax.com.br/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=SEU_CLIENT_ID' \ --data-urlencode 'client_secret=SEU_CLIENT_SECRET' ``` Response: ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...", "token_type": "Bearer", "expires_in": 3600 } ``` > **The token is valid for 1 hour. After it expires, obtain a new token using the same process. The API does not use refresh tokens.** > > ## Required headers | Header | Value | | --------------- | ---------------------------- | | `Authorization` | `Bearer {TOKEN}` | | `Content-Type` | `application/json` | | `Accept` | `application/json` | ## Response format All API responses follow an envelope format with the `data` field: ```json { "data": { // response content } } ``` ## HTTP status codes | Code | Description | | ------ | -------------------------------------------- | | `200` | Request successful | | `201` | Resource created successfully | | `400` | Bad request (e.g., order already paid) | | `401` | Invalid or expired token | | `404` | Resource not found | | `422` | Data validation error | | `500` | Internal server error | ## Error handling Error responses follow an envelope format with the `error` or `errors` field: ```json { "error": { "message": "Order not found" } } ``` For validation errors (`422`), details for each field are returned: ```json { "message": "The given data failed to pass validation.", "errors": { "message": { "campo": ["Mensagem de validação"] } } } ``` > **Always check the HTTP status code before processing the response body. For `401` errors, obtain a new token and retry the request.** > > ## Monetary values > **All monetary values in the API are represented in **cents** (integers). For example, R$ 123.00 must be sent as `12300`.** > > --- Source: https://docs.appmax.com.br/en/api-reference/customers/criar-atualizar.md # Create or update a customer `POST /v1/customers` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria ou atualiza um cliente identificado pela combinação `first_name + last_name + email + phone + ip`. > **Para criar um cliente, você precisa ter feito a coleta de IP utilizando** > > o script [Appmax JS](/guides/appmax-js). > **Se enviar apenas os campos obrigatórios, o cliente será registrado como** > > "carrinho abandonado" e pode ser atualizado depois pela mesma rota. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `first_name` | string | sim | Nome do cliente. | | `last_name` | string | sim | Sobrenome do cliente. | | `email` | string | sim | E-mail válido do cliente. | | `phone` | string | sim | Telefone com DDD (máximo 11 caracteres). | | `ip` | string | sim | IP de origem do cliente (coletado via Appmax JS). | | `document_number` | string | não | CPF ou CNPJ do cliente. | | `address` | object | não | Endereço do cliente. | | `address.postcode` | string | não | CEP (apenas dígitos). | | `address.street` | string | não | Logradouro. | | `address.number` | string | não | Número do endereço. | | `address.complement` | string | não | Complemento (opcional). | | `address.district` | string | não | Bairro. | | `address.city` | string | não | Cidade. | | `address.state` | string | não | UF. | | `products` | array | não | Lista de produtos vinculados ao cliente. | | `products[].sku` | string | sim | SKU do produto. | | `products[].name` | string | sim | Nome do produto. | | `products[].quantity` | integer | sim | Quantidade do produto. | | `products[].unit_value` | integer | não | Valor unitário do produto em **centavos**. Obrigatório quando `products_value` não é informado no pedido. | | `products[].type` | enum: physical \| digital | não | Tipo do produto. | | `tracking` | object | não | Dados de origem da visita (UTMs). | | `cart_link` | string | não | URL do carrinho abandonado. Envie este campo para acionar a [recuperação de vendas com IA](/guides/recuperacao-vendas-ia) (funcionalidade em fase beta). | ### Exemplo de requisição ```json { "first_name": "Junior", "last_name": "Almeida", "email": "junior.almeida@email.com", "phone": "51983655100", "document_number": "25226493029", "address": { "postcode": "91520270", "street": "Rua Francisco Carneiro da Rocha", "number": "582", "complement": "Casa", "district": "Moinhos de Ventos", "city": "Porto Alegre", "state": "RS" }, "ip": "127.0.0.1", "products": [ { "sku": "9000010", "name": "Livro de receitas", "quantity": 1, "unit_value": 12300, "type": "digital" } ], "tracking": { "utm_source": "google", "utm_campaign": "teste" } } ``` ## Respostas ### 201 Cliente criado com sucesso. Guarde o `customer_id` para criar o pedido. ```json { "data": { "customer": { "id": 1 } } } ``` ### 422 Erro de validação dos campos do payload. --- Source: https://docs.appmax.com.br/en/api-reference/products/listar-produtos.md # List products `GET /v1/products` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Lista os produtos do merchant com paginação, filtro por nome/status e ordenação. Retorna **20 itens por página**. Cada produto pertence a uma *company*, resolvida automaticamente a partir das credenciais. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Consulta | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `page` | integer | não | Página da listagem. | | `name` | string | não | Filtra produtos cujo nome contém o termo informado. | | `status` | enum: active \| inactive \| all | não | Sem valor ou `active`: apenas ativos. `inactive`: apenas inativos. `all`: todos. | | `sort_by` | enum: name \| price \| created_at | não | Campo de ordenação. | | `sort_dir` | enum: asc \| desc | não | Direção da ordenação. | ## Respostas ### 200 Lista de produtos retornada com sucesso. ```json { "data": { "products": [ { "id": 10, "sku": "SKU-1", "name": "Camiseta Preta", "price": 199.9, "description": "Camiseta 100% algodão", "image": "/uploads/a.png", "inventory": 5, "cost": 50, "is_active": true, "created_at": "2026-07-01 10:00:00", "updated_at": "2026-07-02 12:00:00" } ], "pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 } } } ``` ### 422 Parâmetros de consulta inválidos. ```json { "errors": { "message": { "status": [ "The selected status is invalid." ] } } } ``` ### 500 Erro ao listar os produtos. ```json { "errors": { "message": "Erro ao listar os produtos." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/products/consultar-produto.md # Get a product `GET /v1/products/{id}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna um produto específico do merchant pelo identificador (`id`). Produtos de outra company são tratados como inexistentes (`404`). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador único do produto. | ## Respostas ### 200 Produto retornado com sucesso. ```json { "data": { "id": 10, "sku": "SKU-1", "name": "Camiseta Preta", "price": 199.9, "description": "Camiseta 100% algodão", "image": "/uploads/a.png", "inventory": 5, "cost": 50, "is_active": true, "created_at": "2026-07-01 10:00:00", "updated_at": "2026-07-02 12:00:00" } } ``` ### 404 Produto inexistente ou de outra company. ```json { "errors": { "message": "Produto não encontrado." } } ``` ### 500 Erro ao buscar o produto. ```json { "errors": { "message": "Erro ao buscar o produto." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/products/criar-produto.md # Create a product `POST /v1/products` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria um novo produto para o merchant. Os campos `name` e `price` são obrigatórios. O produto é criado ativo (`is_active: true`). > **Unicidade** > > `sku` e `external_id` devem ser únicos por company. Se já existir um > produto com o mesmo valor, a API retorna `422`. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `name` | string | sim | Nome do produto. | | `price` | number | sim | Preço de venda. Maior ou igual a 0. | | `sku` | string | não | Código interno do produto. Único por company. | | `external_id` | string | não | Identificador externo do produto. Único por company. | | `description` | string | não | Descrição livre do produto. | | `image` | string | não | Caminho ou URL da imagem. | | `inventory` | integer | não | Quantidade em estoque. Maior ou igual a 0. | | `cost` | number | não | Custo do produto. Maior ou igual a 0. | ### Exemplo de requisição ```json { "name": "Camiseta Preta", "price": 199.9, "sku": "SKU-1", "external_id": "EXT-123", "description": "Camiseta 100% algodão", "image": "/uploads/a.png", "inventory": 5, "cost": 50 } ``` ## Respostas ### 201 Produto criado com sucesso. ```json { "data": { "id": 10, "sku": "SKU-1", "name": "Camiseta Preta", "price": 199.9, "description": "Camiseta 100% algodão", "image": "/uploads/a.png", "inventory": 5, "cost": 50, "is_active": true, "created_at": "2026-07-01 10:00:00", "updated_at": "2026-07-01 10:00:00" } } ``` ### 422 Erro de validação dos campos ou `sku`/`external_id` duplicado para a company. ```json { "errors": { "message": { "name": [ "The name field is required." ], "price": [ "The price field is required." ] } } } ``` ### 500 Erro ao criar o produto. ```json { "errors": { "message": "Erro ao criar o produto." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/products/atualizar-produto.md # Update a product `PUT /v1/products/{id}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Atualiza os dados de um produto existente. Envie apenas os campos que deseja alterar; todos são opcionais e seguem as mesmas regras da criação. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador único do produto. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `name` | string | não | | | `price` | number | não | | | `sku` | string | não | Único por company. | | `external_id` | string | não | Único por company. | | `description` | string | não | | | `image` | string | não | | | `inventory` | integer | não | | | `cost` | number | não | | ### Exemplo de requisição ```json { "name": "Camiseta Preta Slim", "price": 219.9 } ``` ## Respostas ### 200 Produto atualizado com sucesso. ```json { "data": { "id": 10, "sku": "SKU-1", "name": "Camiseta Preta Slim", "price": 219.9, "description": "Camiseta 100% algodão", "image": "/uploads/a.png", "inventory": 5, "cost": 50, "is_active": true, "created_at": "2026-07-01 10:00:00", "updated_at": "2026-07-02 13:30:00" } } ``` ### 404 Produto inexistente ou de outra company. ```json { "errors": { "message": "Produto não encontrado." } } ``` ### 422 Erro de validação ou `sku`/`external_id` duplicado. ```json { "errors": { "message": "Já existe um produto com o mesmo SKU ou external_id." } } ``` ### 500 Erro ao atualizar o produto. ```json { "errors": { "message": "Erro ao atualizar o produto." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/products/excluir-produto.md # Delete a product `DELETE /v1/products/{id}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Exclui um produto do merchant. A exclusão é feita por **soft delete**: o produto é marcado como inativo (`is_active: false`) e continua consultável com o filtro de status `inactive` ou `all`. > **Bloqueio** > > A exclusão é bloqueada (`409`) quando o produto está vinculado a uma > assinatura. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador único do produto. | ## Respostas ### 200 Produto excluído (soft delete) com sucesso. ```json { "data": { "id": 10, "sku": "SKU-1", "name": "Camiseta Preta", "price": 199.9, "description": "Camiseta 100% algodão", "image": "/uploads/a.png", "inventory": 5, "cost": 50, "is_active": false, "created_at": "2026-07-01 10:00:00", "updated_at": "2026-07-02 14:00:00" } } ``` ### 404 Produto inexistente ou de outra company. ```json { "errors": { "message": "Produto não encontrado." } } ``` ### 409 Produto vinculado a uma assinatura não pode ser excluído. ```json { "errors": { "message": "Produto vinculado a uma assinatura não pode ser excluído." } } ``` ### 500 Erro ao excluir o produto. ```json { "errors": { "message": "Erro ao excluir o produto." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/orders/criar-pedido.md # Create an order `POST /v1/orders` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria um novo pedido na Appmax. Um pedido deve estar sempre vinculado a um cliente previamente criado. > **Pré-requisito** > > Para criar um pedido, você precisa ter o `customer_id` do cliente. Caso > ainda não tenha, veja > [Criar ou atualizar cliente](/api-reference/customers/criar-atualizar). Armazene o `order_id` retornado, pois ele será necessário para efetuar o pagamento ou consultar o status do pedido. Para entender como o valor total é calculado, veja [Cálculo do valor total](/api-reference/orders/calculo-valor). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `customer_id` | integer | sim | ID do cliente (obtido na criação do cliente). | | `products` | array | sim | Lista de produtos do pedido. | | `products[].sku` | string | sim | SKU do produto. | | `products[].name` | string | sim | Nome do produto. | | `products[].quantity` | integer | sim | Quantidade do produto. | | `products[].unit_value` | integer | não | Valor unitário do produto em **centavos**. Obrigatório quando `products_value` não é informado no pedido. | | `products[].type` | enum: physical \| digital | não | Tipo do produto. | | `products_value` | integer | não | Valor total dos produtos em **centavos**. Obrigatório quando `unit_value` não é informado nos produtos. | | `discount_value` | integer | não | Valor do desconto em **centavos**. | | `shipping_value` | integer | não | Valor do frete em **centavos**. | ### Exemplo de requisição ```json { "customer_id": 29, "products_value": 12300, "discount_value": 0, "shipping_value": 3999, "products": [ { "sku": "9000010", "name": "Livro de receitas", "quantity": 1, "unit_value": 12300, "type": "digital" } ] } ``` ## Respostas ### 201 Pedido criado com sucesso. O pedido é criado com status `pendente` até que o pagamento seja processado. ```json { "data": { "order": { "id": 1, "status": "pendente" } } } ``` ### 404 Cliente ou dados não encontrados. ```json { "error": { "message": "Merchant not found" } } ``` ### 422 Erro de validação dos campos do payload. ```json { "errors": { "message": { "products_value": [ "The products value must be an integer." ], "shipping_value": [ "The shipping value must be an integer." ], "products": [ "The products field is required." ] } } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/orders/consultar-pedido.md # Retrieve order details `GET /v1/orders/{order_id}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Consulta os detalhes de um pedido previamente criado para um merchant. Basta informar o ID do pedido na URL. Veja a [lista completa de status](/guides/status-pedidos) para entender o campo `status` retornado. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | ID do pedido na Appmax. | ## Respostas ### 200 Detalhes do pedido retornados com sucesso. ```json { "data": { "order": { "id": 3531, "status": "estornado", "total_paid": 8916, "amounts": { "sub_total": 4662, "shipping_value": 2738, "discount": 0, "installment_fee": 1516 }, "created_at": "2025-02-13 14:09:48", "updated_at": "2025-02-13 14:11:55" }, "customer": { "id": 2023, "name": "Junior Almeida", "email": "junior.almeida@teste.com", "document_number": "19100000000" }, "payment": { "method": "creditcard", "installments": 12, "installments_amount": 743, "card": { "brand": "visa", "number": "400000****0010" }, "paid_at": "2025-02-13 14:10:20" }, "refund": { "refunded_at": "2025-02-13 14:11:55" } } } ``` ### 404 Pedido não encontrado. ```json { "error": { "message": "Order not found" } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/orders/calculo-valor.md # Order total calculation ## Calculation rules There are two ways to calculate the total value of an order: ### 1. Calculation based on product unit_value If the unit price (`unit_value`) of each product is provided, the total value will be the sum of all product values. **Example:** three products priced at R$ 10.00, R$ 30.00, and R$ 50.00 result in a total of R$ 90.00. If the purchase is paid in installments, the interest must be calculated on the total product value plus shipping (`shipping_value`). **Example:** products R$ 90.00 + shipping R$ 15.00 = R$ 105.00. With 10% interest over 5 installments, the final amount will be R$ 115.50. This adjusted amount must be distributed proportionally across the products. ```bash curl --request POST \ --url https://api.appmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "customer_id": 113543689, "discount_value": 0, "shipping_value": 1500, "products": [ { "sku": "46_0", "name": "PRODUCT_TEST_1", "quantity": 1, "unit_value": 1350 }, { "sku": "47_0", "name": "PRODUCT_TEST_2", "quantity": 1, "unit_value": 3350 }, { "sku": "48_0", "name": "PRODUCT_TEST_3", "quantity": 1, "unit_value": 5350 } ] }' ``` ### 2. Calculation based on products_value with interest If the `unit_value` of each product is not provided, you must pass the total product value (`products_value`) with interest and shipping already calculated. **Example:** products + shipping = R$ 105.00. With 10% interest, the final amount will be R$ 115.50. This value must be sent directly in `products_value`. ```bash curl --request POST \ --url https://api.appmax.com.br/v1/orders \ --header 'Authorization: Bearer YOUR_TOKEN' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "customer_id": 113543689, "products_value": 10050, "discount_value": 0, "shipping_value": 1500, "products": [ { "sku": "46_0", "name": "PRODUCT_TEST_1", "quantity": 1 }, { "sku": "47_0", "name": "PRODUCT_TEST_2", "quantity": 1 }, { "sku": "48_0", "name": "PRODUCT_TEST_3", "quantity": 1 } ] }' ``` ## General rules > **- Always send the calculation with interest included, whether in each product's individual value or in the total product value plus shipping.** > > - The system **does not calculate interest automatically**. The submitted value must already be adjusted according to the payment method, applicable interest, and shipping. To look up installment values with the rates configured in Appmax, use the [Installment calculation](/en/api-reference/payments/parcelas) endpoint. --- Source: https://docs.appmax.com.br/en/api-reference/orders/upsell.md # Create an upsell `POST /v1/orders/upsell` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria um upsell vinculado a um pedido cujo pagamento já gerou o `upsell_hash` (retornado pelo pagamento por cartão de crédito). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `upsell_hash` | string | sim | Hash do pedido para upsell (gerado no pagamento por cartão). | | `products_value` | integer | sim | Valor total dos produtos do upsell (em **centavos**). | | `products` | array | sim | | | `products[].sku` | string | sim | SKU do produto. | | `products[].name` | string | sim | Nome do produto. | | `products[].quantity` | integer | sim | Quantidade do produto. | | `products[].unit_value` | integer | não | Valor unitário do produto em **centavos**. Obrigatório quando `products_value` não é informado no pedido. | | `products[].type` | enum: physical \| digital | não | Tipo do produto. | ### Exemplo de requisição ```json { "upsell_hash": "4000114202503117156088040208561001715608804", "products_value": 12300, "products": [ { "sku": "9000010", "name": "Livro de receitas", "quantity": 1, "unit_value": 12300, "type": "digital" } ] } ``` ## Respostas ### 201 Upsell criado com sucesso. ```json { "data": { "message": "Transacao efetuada com sucesso", "redirect_url": "example.com/order/success-by-order?hash=..." } } ``` ### 404 Pedido não encontrado. ### 422 Erro de validação dos campos do payload. --- Source: https://docs.appmax.com.br/en/api-reference/orders/codigo-rastreio.md # Register a tracking code `POST /v1/orders/shipping-tracking-code` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cadastra um código de rastreio em um pedido criado na Appmax. > **Para que os saques do merchant sejam aprovados, é necessário atualizar** > > o pedido com o código de rastreamento da entrega. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | ID do pedido. | | `shipping_tracking_code` | string | sim | Código de rastreio do pedido. | ### Exemplo de requisição ```json { "order_id": 2, "shipping_tracking_code": "EEEASDASDAS1239A" } ``` ## Respostas ### 201 Código de rastreio incluído com sucesso. ```json { "data": { "message": "tracking accepted" } } ``` ### 400 Pedido não encontrado ou falha ao armazenar o código. ### 422 Erro de validação dos campos do payload. --- Source: https://docs.appmax.com.br/en/api-reference/payments/visao-geral.md # Payments — overview The Appmax API lets you create payments for existing orders, using different methods. ## Available methods - [Credit card](/en/api-reference/payments/cartao-credito) - [Pix](/en/api-reference/payments/pix) - [Boleto](/en/api-reference/payments/boleto) - [Apple Pay](/en/api-reference/payments/apple-pay) ## General prerequisite > **Before creating a payment, you need:** > > - `order_id` — the order's ID > - `customer_id` — the customer's ID > > See how to obtain them in [Create or update customer](/en/api-reference/customers/criar-atualizar) and [Create an order](/en/api-reference/orders/criar-pedido). --- Source: https://docs.appmax.com.br/en/api-reference/payments/cartao-credito.md # Credit card payment `POST /v1/payments/tokenize` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Substitui os dados reais do cartão (número, CVV, validade) por um token único e seguro, permitindo realizar transações sem expor as informações originais. > **Tokenização server-side exige PCI-DSS** > > Quando você tokeniza pelo seu backend, o seu servidor toca o número de > cartão e o CVV em claro. Isso **só é permitido** se sua arquitetura > está em escopo PCI-DSS. Se você não tem certeza, use o caminho via CDN > — o script Appmax JS isola os dados sensíveis do seu servidor. Existem dois caminhos de autenticação distintos para este endpoint: via CDN (header `external-id`) ou via backend (header `Authorization: Bearer`). Veja [`external-id`](/guides/external-id) para o contexto. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `payment_data` | object | sim | | | `payment_data.credit_card` | object | sim | | | `payment_data.credit_card.number` | string | sim | Número do cartão de crédito. | | `payment_data.credit_card.cvv` | string | sim | Código de segurança (máximo 4 caracteres). | | `payment_data.credit_card.expiration_month` | string | sim | Mês de expiração (1 a 12). | | `payment_data.credit_card.expiration_year` | string | sim | Ano de expiração (2 a 4 caracteres). | | `payment_data.credit_card.holder_name` | string | sim | Nome do titular do cartão. | ### Exemplo de requisição ```json { "payment_data": { "credit_card": { "number": "4444222222222222", "cvv": "123", "expiration_month": "12", "expiration_year": "28", "holder_name": "John Doe" } } } ``` ## Respostas ### 201 Token gerado com sucesso. ```json { "data": { "token": "422146c7523a46119d6073ea56193913" } } ``` ### 401 Nem `external-id` nem `Authorization` foram enviados. ### 404 `external-id` enviado mas não corresponde a nenhuma instalação ativa. ### 422 Body inválido (campos faltando ou em formato incorreto). A credit card payment happens in **two steps, in this order**: 1. **[Tokenization](#tokenizacao)** — the sensitive card data (number, CVV, expiry) is exchanged for a single-use `token`. It is this token, never the card number, that travels to the payment API. 2. **[Payment](#pagamento)** — the token is sent to `POST /v1/payments/credit-card`, along with `order_id` and `customer_id`, to complete the charge. Testing in sandbox? Check the [test cards](#cartoes-de-teste) at the end of this page. > **Tokenizing from the front-end?** > > At checkout, tokenization is usually done by `appmax.js` via CDN — see [Appmax JS](/en/guides/appmax-js). The endpoint in step 1 documents the underlying contract, useful for custom implementations (without the script) and for debugging. ## 1. Tokenization ## 2. Payment ## Test cards To test the flow in the **sandbox** environment, use the cards below with a **future expiration date**: | Card number | Scenario | | --------------------- | ------------------------------------------------------------------------------------------------------ | | `4000000000000010` | **Approved and captured.** The order becomes `aprovado`, with `paid_at` and `captured_at` set. | | `4000000000000028` | **Approved without capture** (pre-authorization). The order becomes `autorizado`, `captured_at` null. | | `4000000000000002` | **Declined by the issuer.** The payment returns an error and the order is canceled. | | `4000000000000036` | **Transaction error.** Processing failure; the payment returns an error. | | `4000000000000044` | **Order failed.** The order fails at the gateway; the payment returns an error. | | `4000000000009999` | **Gateway unavailable.** Simulates a payment-provider outage. | | Any other card | Declined. | The scenarios work both when sending the card data directly to `POST /v1/payments/credit-card` and in the tokenized flow (`POST /v1/payments/tokenize`, then paying with the `token`): a token minted from a test card reproduces that card's scenario. > **Use card `4000000000000010` to test the full successful payment flow, and card `4000000000000002` to test payment error handling. Note that `4000000000000028` **approves** the transaction, it just does not capture it.** > > --- Source: https://docs.appmax.com.br/en/api-reference/payments/pix.md # Pix payment `POST /v1/payments/pix` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Gera as instruções de pagamento via Pix para um pedido existente: QR Code (imagem base64) e código EMV (copia-e-cola). > **Exiba um cronômetro de expiração** > > Na página de sucesso, mostre o QR Code, o copia-e-cola e um cronômetro > com o tempo restante até `pix_expiration_date`. Calcule a contagem > regressiva **dinamicamente a partir desse campo** — não fixe um tempo > de expiração no seu código, pois esse valor pode variar. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | ID do pedido. | | `payment_data` | object | não | | | `payment_data.pix` | object | não | | | `payment_data.pix.document_number` | string | não | CPF ou CNPJ do pagador. | | `payment_data.subscription` | object | não | **Pendente de especificação.** Placeholder criado só para destravar a resolução de `$ref` no restante da spec — o formato real de `payment_data.subscription` ainda não foi documentado. Antes de usar em produção, substituir pelos campos reais (ex.: os mesmos de `CreateSubscriptionRequest`, sem `order_id`, ou outro formato a confirmar com o time responsável). | ### Exemplo de requisição ```json { "order_id": 113, "payment_data": { "pix": { "document_number": "19100000000" } } } ``` ## Respostas ### 200 Instruções Pix retornadas. Os campos `pix_qrcode` (PNG em base64, sem prefixo `data:`), `pix_emv` (BR Code copia-e-cola) e `pix_expiration_date` (data/hora de expiração do Pix, formato `Y-m-d H:i:s`) vivem em `data.payment`. Exiba o QR Code e o copia-e-cola ao cliente. ```json { "data": { "payment": { "cashback": 0, "pay_reference": "115063889f2f0f1e51428104d1b542f8299", "pix_qrcode": "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD...", "pix_emv": "00020101021226850014br.gov.bcb.pix2563qrcodepix.bb.com.br/pix/v2/fab6ead0-131f-46b6-aa19-d17be75b00535204000...", "pix_expiration_date": "2026-05-21 05:34:30" } } } ``` ### 404 Pedido não encontrado ou já pago. --- Source: https://docs.appmax.com.br/en/api-reference/payments/boleto.md # Boleto payment `POST /v1/payments/boleto` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria um boleto bancário vinculado a um pedido existente. Retorna o link do PDF e a linha digitável. O boleto é uma opção de pagamento **offline**: o cliente paga em um banco, lotérica ou pelo internet banking, fora do seu checkout. > **Exiba o boleto na página de sucesso** > > Mostre a opção de baixar o PDF (`pdf_url`) e de copiar a linha > digitável (`digitable_line`) na página de sucesso, para que o cliente > possa efetuar o pagamento pelo canal de sua preferência. > **Não abra o PDF em iframe** > > O download do boleto (`pdf_url`) deve **redirecionar o cliente para > outra página**, e não ser aberto embutido em um iframe na sua página > de checkout/sucesso. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | ID do pedido. | | `payment_data` | object | sim | | | `payment_data.boleto` | object | sim | | | `payment_data.boleto.document_number` | string | sim | CPF ou CNPJ do pagador. | ### Exemplo de requisição ```json { "order_id": 113, "payment_data": { "boleto": { "document_number": "19100000000" } } } ``` ## Respostas ### 201 Boleto gerado com sucesso. ```json { "data": { "order": { "id": 113, "status": "pendente" }, "boleto": { "pdf_url": "https://boleto.appmax.com.br/pdf/abc123...", "digitable_line": "23793.38128 60000.000003 00000.000400 1 84340000012300", "due_date": "2025-03-22" } } } ``` ### 404 Pedido não encontrado ou já pago. --- Source: https://docs.appmax.com.br/en/api-reference/payments/apple-pay.md # Apple Pay payment `POST /v1/payments/apple-pay` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br O Apple Pay é uma solução de pagamento digital da Apple que permite realizar compras com cartão de crédito de forma rápida, segura e conveniente em dispositivos compatíveis (iPhone, iPad, Apple Watch e Mac). Utilizando tokenização e autenticação biométrica (Face ID ou Touch ID), o Apple Pay elimina a digitação manual dos dados do cartão a cada compra, reduzindo o tempo de checkout e aumentando a conversão. Além disso, transações via Apple Pay não possuem risco de chargeback por fraude. > **Compatibilidade** > > O Apple Pay só é exibido no navegador Safari (macOS ou iOS) e em > dispositivos Apple com suporte ao método. Em outros navegadores, o botão > não aparece. ## Antes de começar: escolha o seu modelo de integração Esta é a decisão que mais gera dúvidas. Existem dois modelos de integração com Apple Pay pela Appmax, e a diferença está em quem registra o domínio junto à Apple e em quem controla o `merchantIdentifier`. > **Em todos os modelos, você hospeda o arquivo `.well-known`** > > Independentemente do modelo abaixo, é sempre responsabilidade do lojista > (ou do integrador, em nome da loja) publicar o arquivo > `.well-known/apple-developer-merchantid-domain-association` na raiz de > cada domínio onde o Apple Pay será usado — inclusive no modelo integrado. > Veja o passo a passo completo, com o arquivo para download, em > [Configuração de domínios para Apple Pay](/api-reference/payments/apple-pay-dominio). | Aspecto | Modelo integrado (loja integrada / integrador via AppStore) | Fluxo direto | | --- | --- | --- | | Quem cadastra o domínio na Apple | A Appmax, por API, sob a conta/merchant Apple da Appmax | O próprio lojista, na conta Apple dele | | Quem hospeda o `.well-known` | O lojista/integrador, em todos os casos | O lojista hospeda o arquivo na raiz do domínio | | `merchantIdentifier` | Gerido pela Appmax, compartilhado entre vários domínios do integrador | Do próprio lojista | | Indicado para | Plataformas/integradores que operam várias lojas (ex.: e-commerce SaaS) | Lojista único que gerencia a própria conta Apple | > **Como saber qual é o seu caso?** > > Se você é uma plataforma/integrador e instala o app da Appmax em nome de > várias lojas (fluxo de AppStore, com `client_id`/`external_id` por loja), > você está no **modelo integrado**. Se você é um lojista configurando o > Apple Pay diretamente na sua própria conta Apple, você está no > **fluxo direto**. Nos dois casos, você precisa publicar o arquivo > `.well-known` (veja o aviso acima). Os passos abaixo indicam, quando relevante, o que muda entre os dois modelos. ## Glossário | Termo | Descrição | | --- | --- | | Apple Pay | Solução de pagamento da Apple para compras seguras via Apple Wallet, com autenticação biométrica e criptografia ponta a ponta. | | Apple Token (Apple Pay Token) | Objeto JSON criptografado retornado pela PaymentSheet após o usuário confirmar o pagamento, contendo `paymentData`, `paymentMethod` e `transactionIdentifier`. É o cartão tokenizado e serve para efetivar o pagamento na API Appmax. Gerado por transação. | | PaymentSheet | Interface nativa do Apple Pay que exibe valor, métodos disponíveis e solicita autenticação (Face ID, Touch ID ou senha). | | Merchant Session | Sessão JSON assinada pela Apple, válida para um domínio e `merchantIdentifier`. Valida o site antes de exibir a PaymentSheet. Obtida em tempo real a cada transação. | | `.well-known/apple-developer-merchantid-domain-association` | Arquivo estático de validação de domínio da Apple, vinculado ao `merchantIdentifier` (não à transação). Responsabilidade do lojista em **todos** os modelos de integração — veja [Configuração de domínios para Apple Pay](/api-reference/payments/apple-pay-dominio). | | `external_id` | UUID que identifica o par (app instalado + loja). Gerado na instalação e usado nas chamadas da CDN. Veja [Instalação do Aplicativo](/guides/instalacao). | ## Pré-requisitos - **Navegador compatível** — Safari (macOS ou iOS). - **Carteira Apple configurada** — ao menos um cartão Visa ou Mastercard no Apple Wallet. - **Instalação do app** feita com o parâmetro `domain_name` (passo 1). - **Arquivo `.well-known` publicado** e domínio validado junto à Apple (passo 2). - **Appmax JS incluído e inicializado** com os callbacks (passo 3). Componentes do fluxo: - **Script JS:** `https://scripts.appmax.com.br/appmax.min.js` (produção) — em sandbox use `https://scripts.sandboxappmax.com.br/appmax.min.js`. - **Domínio validado** junto à Apple. - **Requisição de pagamento** para a API Appmax com `appleToken`, `order_id` e `customer_id` válidos. ## 1. Autorizar a instalação do aplicativo Registre o aplicativo na Appmax informando o domínio: ```bash curl --location 'https://api.appmax.com.br/app/authorize' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer SEU_TOKEN' \ --data '{ "app_id": "APP_ID", "external_key": "EXTERNAL_KEY", "url_callback": "URL_CALLBACK", "domain_name": "subdominio.dominio.com.br" }' ``` > **O parâmetro `domain_name` deve conter subdomínio + domínio (ex.:** > > `minhaloja.minhaintegracao.com.br`). Também é possível enviar vários > domínios com `domain_names` (array). Mais detalhes em > [Instalação do Aplicativo](/guides/instalacao). ## 2. Publicar o `.well-known` e validar o domínio ### 2.1 Publicar o arquivo `.well-known` (obrigatório em todos os modelos) Em **todos** os modelos de integração, cada domínio onde o botão Apple Pay será exibido precisa servir, na raiz do site, o arquivo de validação: ``` https://subdominio.dominio.com.br/.well-known/apple-developer-merchantid-domain-association ``` O arquivo, o passo a passo de publicação e o comando para verificar se ele está acessível estão em [Configuração de domínios para Apple Pay](/api-reference/payments/apple-pay-dominio). > **O conteúdo do `.well-known` **não** é o Apple Token. O Apple Token é o** > > cartão tokenizado, gerado por transação. O `.well-known` é um arquivo > estático de validação de domínio, vinculado ao `merchantIdentifier` — o > mesmo arquivo é usado em todos os domínios da sua implementação. ### 2.2 Cadastrar o domínio junto à Apple > **Este passo muda conforme o modelo de integração.** > > **Modelo integrado (loja integrada / integrador):** você não precisa cadastrar o domínio pelo painel da Apple. O registro é feito por API, pela Appmax, sob o `merchantIdentifier` da Appmax. Basta que o domínio tenha sido informado na instalação (passo 1) e que o arquivo `.well-known` (passo 2.1) já esteja publicado — a Apple confere o arquivo ao validar o domínio. Se o domínio do lojista mudar: o novo domínio é tratado como um domínio novo e precisa ser registrado novamente. Não é necessário criar um novo merchant. **Fluxo direto:** o cadastro do domínio é feito por você, diretamente no painel de desenvolvedor da Apple, na sua própria conta. A Apple valida o arquivo `.well-known` publicado no passo 2.1 no momento do cadastro. ## 3. Configuração e inicialização do script no front-end > **Esta seção resume o essencial. Para o passo a passo completo — seletores** > > do botão, ordem de carregamento em SPA, contrato de DOM e troubleshooting > — veja [Implementando o botão Apple Pay com o Appmax JS](/api-reference/payments/apple-pay-appmax-js). > **O `externalId` esperado aqui é o mesmo `external_id` que você retornou com** > > HTTP 200 na etapa de [Instalação do Aplicativo](/guides/instalacao). O `appmax.min.js` faz três coisas: - Estiliza o botão com o design oficial da Apple (opcional). - Inicializa o botão para abrir a PaymentSheet com os dados do carrinho. - Dispara os callbacks de sucesso, erro, atualização e autorização. ### 3.1 Inicializar com onSuccess, onError, externalId, onUpdate e onAuthorize ```html ``` > **Atenção à ordem e aos nomes dos callbacks. O callback que recebe o token é** > > o `onAuthorize` (não `onAutorize`). A ordem em `init` é: `onSuccess`, > `onError`, `externalId`, `onUpdate`, `onAuthorize`. ### 3.2 Mantendo o onUpdate atualizado (JS puro) O `onUpdate` deve retornar os dados atuais do checkout para alimentar a PaymentSheet (valor, frete, desconto, parcelas, itens): ```js function getCheckoutData() { const products = Array.from(document.querySelectorAll('.product-item')).map(item => ({ name: item.querySelector('.product-name').textContent, price: parseFloat(item.querySelector('.product-price').value), quantity: parseInt(item.querySelector('.product-quantity').value, 10) })); const freight = parseFloat(document.getElementById('freight').value || 0); const discount = parseFloat(document.getElementById('discount').value || 0); const totalItems = products.reduce((sum, p) => sum + p.price * p.quantity, 0); return { orderId: sessionStorage.getItem('order_id') || '', total: totalItems + freight - discount, freight: freight, discount: discount, installments: parseInt(document.getElementById('installments').value, 10) || 1, products: products }; } ``` > **Sobre o `orderId` no `onUpdate`: o campo aqui serve à montagem da** > > PaymentSheet. O `order_id` obrigatório é o do request de pagamento > (`POST /v1/payments/apple-pay`) — ele não precisa necessariamente ser > setado no `onUpdate`, mas precisa ser o `order_id` de uma Order já > existente na Appmax (veja o passo 4). ## 4. Processar o pagamento O pagamento Apple Pay exige três elementos: `customer_id`, `order_id` e o `appleToken`. A ordem é: criar customer → criar order → efetivar o pagamento. ```js async function processarPagamento(appleToken) { // 1. Criar customer — POST /v1/customers (veja /api-reference/customers/criar-atualizar) // 2. Criar order — POST /v1/orders (veja /api-reference/orders/criar-pedido) // 3. Efetivar pagamento — POST /v1/payments/apple-pay const paymentPayload = { order_id, // Order já existente na Appmax customer_id, payment_data: { apple_pay: { installments: "3", // 1 a 12 holder_document_number: "22233344450", // obrigatório soft_descriptor: "EXEMPLOLOJA", // opcional, máx. 13 caracteres // Do appleToken.paymentData: payment_data: { version: "EC_v1", data: "exemplo", signature: "signature", header: { ephemeralPublicKey: "MFk...==", transactionId: "trx....wvu" } }, // Do appleToken.paymentMethod: payment_method: { displayName: "Visa •••• 3714", network: "Visa", type: "credit" }, // Do appleToken.transactionIdentifier: transaction_identifier: "trx....wvu" } } }; const resp = await fetch('https://api.appmax.com.br/v1/payments/apple-pay', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer SEU_TOKEN` }, body: JSON.stringify(paymentPayload) }); if (!resp.ok) throw new Error('Falha no pagamento'); return resp.json(); } ``` Mapeamento do Apple Token para o payload: | Campo do `appleToken` | Vai em | | --- | --- | | `paymentData` | `payment_data.apple_pay.payment_data` | | `paymentMethod` | `payment_data.apple_pay.payment_method` | | `transactionIdentifier` | `payment_data.apple_pay.transaction_identifier` | > **Campos obrigatórios: `order_id`, `installments` (1–12) e** > > `holder_document_number`. `soft_descriptor` é opcional (máx. 13 caracteres). ## Validação em tempo real (Merchant Session) A validação do domínio junto à Apple não é única — a Apple revalida o domínio/merchant a cada transação, no momento em que a PaymentSheet é aberta (a Merchant Session é obtida em tempo real). É um processo rápido e transparente para o usuário; o Appmax JS cuida disso automaticamente. Veja [`POST /v1/apple-pay/merchant-session`](/api-reference/payments/apple-pay-merchant-session). ## Testes e Sandbox > **Não é possível testar o Apple Pay em `localhost`. O Apple Pay exige um** > > domínio público, com HTTPS válido e devidamente registrado/configurado. A > Apple não valida domínios não públicos. > **Não existe merchant session de sandbox** > > A validação de domínio/merchant é sempre feita contra a infraestrutura de > **produção** da Apple, mesmo em ambiente de sandbox — não existe uma > segunda instância só para teste. Trate sandbox e produção como > assimétricos nesse ponto específico: o script de sandbox aponta para o > mesmo endpoint de validação que o de produção. Cartões de teste da Apple: a referência oficial é [developer.apple.com/apple-pay/sandbox-testing](https://developer.apple.com/apple-pay/sandbox-testing) — que exige uma Sandbox Apple Account própria. > **Modelo integrado** > > Como o pagamento roda sob a conta Apple da Appmax, não é possível usar > cartões de teste da Apple da sua própria conta (não conseguimos provisionar > credenciais de teste de terceiros na nossa conta Apple, por segurança). > Nesse caso, a forma prática de testar é usar um cartão real e > cancelar/estornar as ordens pelo admin. ## Ambientes | | Sandbox | Produção | | --- | --- | --- | | Script JS | `https://scripts.sandboxappmax.com.br/appmax.min.js` | `https://scripts.appmax.com.br/appmax.min.js` | | API | `https://api.sandboxappmax.com.br` | `https://api.appmax.com.br` | ## Perguntas frequentes (FAQ) **O `appleToken` é usado para quê?** É o cartão tokenizado pela Apple. Ele precisa ser enviado ao `POST /v1/payments/apple-pay` (nos campos `payment_data`, `payment_method`, `transaction_identifier`). Sem ele, o pagamento não é processado. É de uso único por transação. **Preciso hospedar o arquivo `.well-known` em cada domínio?** Sim, em **todos** os modelos de integração — isso não muda entre o modelo integrado e o fluxo direto. O que muda entre os modelos é apenas quem cadastra o domínio junto à Apple (a Appmax, por API, no modelo integrado; você mesmo, pelo painel da Apple, no fluxo direto). Veja [Configuração de domínios para Apple Pay](/api-reference/payments/apple-pay-dominio). **O `order_id` do `onUpdate` precisa ser de uma Order real da Appmax?** O `order_id` obrigatório é o do request de pagamento e precisa ser de uma Order existente (criada via `POST /v1/orders`). Ele não precisa vir do `onUpdate`, mas precisa estar no payload do pagamento. **Se o domínio do lojista mudar, preciso de um novo merchant?** Não. O novo domínio é registrado novamente na instalação existente. **Existe um token/arquivo por lojista, ou um geral?** O Apple Token é sempre por transação. O conteúdo do arquivo de validação de domínio é o mesmo para todos os domínios vinculados ao `merchantIdentifier` gerido pela Appmax — não é gerado por lojista. Ainda assim, cada domínio precisa hospedar sua própria cópia desse mesmo arquivo (veja [Configuração de domínios para Apple Pay](/api-reference/payments/apple-pay-dominio)). O que é por lojista são as credenciais (`client_id`/`client_secret`) e o `external_id`. **A validação do domínio demora?** O registro do domínio é rápido (chamada síncrona). A validação da Merchant Session ocorre em tempo real, a cada transação — também rápida. **Consigo testar pelo sandbox da Appmax?** É preciso um domínio público registrado e configurado. No modelo integrado, o caminho prático é cartão real + estorno (veja Testes e Sandbox). ## Conclusão — checklist 1. Autorizar a instalação com `domain_name`. 2. Publicar o arquivo `.well-known` em cada domínio (obrigatório em todos os modelos) e cadastrar o domínio junto à Apple — Appmax por API (modelo integrado) ou você mesmo (fluxo direto). Veja [Configuração de domínios para Apple Pay](/api-reference/payments/apple-pay-dominio). 3. Incluir e inicializar o Appmax JS com `onSuccess`, `onError`, `externalId`, `onUpdate`, `onAuthorize`. 4. Criar customer → criar order → processar o `appleToken` via `POST /v1/payments/apple-pay`. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | | | `customer_id` | integer | sim | | | `payment_data` | object | sim | | | `payment_data.apple_pay` | object | sim | | | `payment_data.apple_pay.installments` | string | não | | | `payment_data.apple_pay.holder_document_number` | string | não | | | `payment_data.apple_pay.soft_descriptor` | string | não | | | `payment_data.apple_pay.payment_data` | object | não | Apple Pay Token devolvido pela PaymentSheet do Safari. | | `payment_data.apple_pay.payment_data.data` | string | não | | | `payment_data.apple_pay.payment_data.signature` | string | não | | | `payment_data.apple_pay.payment_data.header` | object | não | | | `payment_data.apple_pay.payment_data.header.publicKeyHash` | string | não | | | `payment_data.apple_pay.payment_data.header.ephemeralPublicKey` | string | não | | | `payment_data.apple_pay.payment_data.header.transactionId` | string | não | | | `payment_data.apple_pay.payment_data.version` | string | não | | | `payment_data.apple_pay.payment_method` | object | não | | | `payment_data.apple_pay.payment_method.displayName` | string | não | | | `payment_data.apple_pay.payment_method.network` | string | não | | | `payment_data.apple_pay.payment_method.type` | string | não | | | `payment_data.apple_pay.transaction_identifier` | string | não | | ### Exemplo de requisição ```json { "order_id": 123, "customer_id": 456, "payment_data": { "apple_pay": { "installments": "3", "holder_document_number": "22233344450", "soft_descriptor": "EXEMPLOLOJA", "payment_data": { "data": "exemplo", "signature": "signature", "header": { "publicKeyHash": "Z...", "ephemeralPublicKey": "MFk...==", "transactionId": "trx....wvu" }, "version": "EC_v1" }, "payment_method": { "displayName": "Visa 3714", "network": "Visa", "type": "credit" }, "transaction_identifier": "trx....wvu" } } } ``` ## Respostas ### 201 Pagamento Apple Pay aprovado. --- Source: https://docs.appmax.com.br/en/api-reference/payments/apple-pay-dominio.md # Domain configuration for Apple Pay To enable Apple Pay in your store, you must serve the `.well-known/apple-developer-merchantid-domain-association` file at the root of **every domain** where the payment button will be shown. > **Required in every integration model** > > This step is required even if you install the Appmax app via the AppStore > (integrated model, with a `client_id`/`external_id` per store). What changes > between models is only who registers the domain with Apple — you always > publish the `.well-known` file yourself. See > [Apple Pay payment](/en/api-reference/payments/apple-pay) for the full flow > and the distinction between models. --- ## What is this file for? Apple requires this file as part of its domain verification process. It proves that Appmax is authorized to process Apple Pay payments on the domains you inform. Apple uses this file to validate that the domain is correctly linked to the **Merchant ID** configured by Appmax. Without it, the "Pay with Apple Pay" button won't work in production — the PaymentSheet won't open, or domain validation fails on every transaction attempt. > **The `.well-known` file is not the Apple Token** > > The Apple Token is the tokenized card, generated per transaction (see > [Apple Pay payment](/en/api-reference/payments/apple-pay)). The > `.well-known` file is **static**, the same on every domain, tied to > Appmax's `merchantIdentifier` — not to the transaction. --- ## 1. File content Save the text below to a file named `apple-developer-merchantid-domain-association` (no extension). It is the same content for every domain in your implementation: 7b2276657273696f6e223a312c227073704964223a2238383637324534354136323336423032384645463731323938334343354338354339334633353231433430374142313338414543354144434641334330334442222c22637265617465644f6e223a313735323630333939333937337d > **Copy it exactly as is** > > The content is validated by Apple — one extra space or line break > invalidates the domain verification. --- ## 2. Publish the file on each domain The file must be published at the following path on **each** domain where Apple Pay will be used, without renaming it: ``` https:///.well-known/apple-developer-merchantid-domain-association ``` Examples: - `https://domain1.com/.well-known/apple-developer-merchantid-domain-association` - `https://domain2.com/.well-known/apple-developer-merchantid-domain-association` - `https://store3.com/.well-known/apple-developer-merchantid-domain-association` --- ## 3. Verify accessibility After publishing the file, confirm it's accessible: ```bash curl -I https:///.well-known/apple-developer-merchantid-domain-association ``` The expected response should contain: - `HTTP/2 200 OK` (or `HTTP/1.1 200 OK`) - `Content-Type: text/plain` - No redirects (neither `301`/`302` from `http` to `https`, nor from `www` to the root domain or vice versa) --- ## 4. Registering the domain with Apple Once the file is published and accessible, how the domain gets registered with Apple depends on your integration model: - **Integrated model** (AppStore installation): registration is done by Appmax, via API, as soon as the domain is informed during [app installation](/en/guides/instalacao) (the `domain_name` or `domain_names` parameter). The file just needs to already be published by then. - **Direct flow**: you register the domain yourself, in the Apple Developer portal, under your own Apple account. Apple validates the `.well-known` file at registration time. In both cases, if the store's domain changes, the new domain is treated as a new one — republish the `.well-known` file on it and register it again (no need to create a new merchant). --- ## Best practices - **HTTPS is mandatory**: the domain must respond only over a secure connection. - **Avoid redirects**: the URL must respond directly with a 200 status, without going through `www`, an `http→https` redirect, or any other redirect before reaching the file. - **Don't change the file's content**: its internal hash is validated by Apple. - **Content-Type**: serve the file as `text/plain`. - **Suggested Cache-Control**: `public, max-age=3600`. If you have questions, contact Appmax technical support. --- Source: https://docs.appmax.com.br/en/api-reference/payments/apple-pay-appmax-js.md # Implementing the Apple Pay button with the Appmax JS This guide covers the part of the Apple Pay flow that runs **in the browser, via `appmax.min.js`**: how the button is rendered, the DOM contract the script requires, and how to wire the callbacks up to payment. For the rest of the flow, see: - [App installation](/en/guides/instalacao) — `domain_name` needs to be configured before anything here works. - [Domain configuration for Apple Pay](/en/api-reference/payments/apple-pay-dominio) — publishing the `.well-known` file, required in every integration model. - [Appmax JS](/en/guides/appmax-js) — the full `init` signature, IP collection, and card tokenization (this guide assumes you've already read the "How to use" section there). - [Apple Pay payment](/en/api-reference/payments/apple-pay) — the endpoint contract (payload, Apple Token mapping, integrated model × direct flow, FAQ). > **Quick prerequisites** > > 1. Installation authorized with `domain_name` (or `domain_names`). > 2. `.well-known` file published on the domain. > 3. Script (`scripts.appmax.com.br/appmax.min.js` in production) loaded on the page. > > Without all three, the button may still show up, but domain validation fails when the PaymentSheet tries to open. --- ## Rendering the button `appmax.min.js` recognizes two DOM selectors. You only need one of them — not both. | Selector | Role | | --- | --- | | `.appmax-apple-pay-btn` | Container **you** render empty. The SDK replaces its `innerHTML` with Apple's official button (SVG and styles included). Recommended path — you don't need to draw the button yourself. | | `[data-appmax-apple-pay]` | The button itself. This is the element the SDK registers the `click` on. Use this attribute directly on your own button if you'd rather control the markup. | Simplest path — let the SDK draw the button: ```html
``` The SDK injects something like: ```html ``` > **The button only appears if the device supports Apple Pay** > > Outside Safari (or on a device with no Apple Pay set up), `ApplePaySession.canMakePayments()` returns `false` and the SDK doesn't activate the button — it may stay empty or hidden, depending on how you styled the container. That's expected, not a bug. --- ## Load order: the button needs to exist before `init` `AppmaxScripts.init(...)` looks for the button **once**, at the moment it runs, and doesn't observe DOM changes after that — the same contract described in ["DOM contract" on the Appmax JS page](/en/guides/appmax-js#dom-contract-init-is-not-reactive). For the Apple Pay button, the practical consequence is: - If the container/button only mounts **after** `init` — behind a route, a checkout step, a `v-if`/conditional — the click won't do anything. No error, no log. - If the component that calls `init` can re-render (StrictMode, Fast Refresh, a poorly-scoped effect dependency), remember that `init()` is **not idempotent**: each call registers a new listener on top of the previous one. **Practical rule for SPAs:** make sure the button's container is already in the DOM at the exact moment `AppmaxScripts.init(...)` is called — never the other way around. --- ## Initializing with the Apple Pay callbacks For Apple Pay, `init` requires `externalId`, `onUpdate`, and `onAuthorize` (plus `onSuccess`/`onError`, always required). The full reference for each parameter is in [Appmax JS → Initialize AppmaxScripts](/en/guides/appmax-js#how-to-use); the focus here is how these three connect to the Apple Pay flow specifically. ```html
``` > **Parameter order and names** > > `init(onSuccess, onError, externalId, onUpdate, onAuthorize)` — in that order. Swapping `onUpdate` and `onAuthorize`, or writing `onAutorize`, makes the SDK treat the wrong parameter as the callback function. `externalId` is the same `external_id` you returned with an HTTP 200 during [app installation](/en/guides/instalacao#health-check) — without it, `init` throws a synchronous exception (see the warning in [Appmax JS](/en/guides/appmax-js#how-to-use)). --- ## What `onUpdate` must return `onUpdate` is called when the PaymentSheet opens and whenever the user changes something in it. The returned object describes the **cart**, with numeric values **in BRL** — the SDK converts it into the format Safari's PaymentSheet consumes. You don't build the `lineItems` or format the total. ```javascript const onUpdate = () => ({ total: 129.90, freight: 15.00, discount: 10.00, installments: 1, products: [ { name: 'Black T-shirt', price: 62.45, quantity: 2 }, ], }); ``` | Field | Type | Required | What shows up on the PaymentSheet | | --- | --- | --- | --- | | `total` | `number` | **Yes** | Total line. The label is hardcoded to `"Total"` and is not configurable. | | `products` | `array` | Recommended | One line per item. Without it, the sheet shows the total only. | | `products[].name` | `string` | Yes, if `products` is present | Line label. | | `products[].price` | `number` | Yes, if `products` is present | **Unit** price, in BRL. | | `products[].quantity` | `number` | Yes, if `products` is present | Quantity. The displayed value is `price × quantity`. | | `freight` | `number` | No | `"Frete"` line. Omitted when `0` or absent. | | `discount` | `number` | No | `"Desconto"` line. Omitted when `0` or absent. | | `installments` | `number` | No | Does not affect the PaymentSheet. Accepted for compatibility. | > **Values are numbers in BRL — not cents, not strings** > > This is the most common cause of the PaymentSheet failing to open. > > - `total: 12990` charges **R$ 12,990.00**, not R$ 129.90. > - `total: '129.90'` (string) makes the SDK throw `Error: Error processing payment...` **before** the sheet opens, because it calls `.toFixed(2)` on the value. > > For the same reason, do **not** return the already-formatted object: > > ```javascript // ✗ Wrong — this is the INTERNAL format, which the SDK builds from your return value { total: { label: 'Total', amount: { currency: 'BRL', value: '129.90' } }, displayItems: [ /* ... */ ] } // ✓ Correct — the cart, with numbers { total: 129.90, products: [{ name: 'Black T-shirt', price: 62.45, quantity: 2 }] } ``` --- ## How to signal failure in `onAuthorize` The SDK decides the PaymentSheet outcome from the **state of the Promise** your `onAuthorize` returns: | Your Promise | What the SDK does | | --- | --- | | Resolves | `completePayment(STATUS_SUCCESS)` — the sheet closes with Apple's confirmation. | | Rejects | `completePayment(STATUS_FAILURE)` — the sheet reports the failure to the buyer. | > **`return false` does not signal failure** > > Only **rejecting** the Promise is read as a failure. A `try/catch` around your backend call — the natural pattern for showing an error message on screen — makes the Promise resolve, and the buyer sees Apple's confirmation on a payment that was **declined**. > > If you need to handle the error, rethrow it: > > ```javascript const onAuthorize = async (appleToken) => { try { const res = await fetch('/checkout/apple-pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order_id: orderId, customer_id: customerId, appleToken }), }); if (!res.ok) throw new Error('Payment declined'); } catch (err) { showError(err); throw err; // required: without this, the sheet closes as a success } }; ``` --- ## From click to payment 1. The user clicks the button → the SDK opens the `ApplePaySession` (Safari's PaymentSheet) and validates the merchant automatically — you don't call any endpoint for this. 2. If the user changes shipping, installments, or an item along the way, Apple calls your `onUpdate` to refresh the displayed total. 3. On confirmation (Face ID/Touch ID), the SDK calls your `onAuthorize(appleToken)`. 4. In `onAuthorize`, with the `appleToken` in hand: create the customer, create the order, and settle the payment at `POST /v1/payments/apple-pay` — the full `appleToken` → payload mapping is in the "Process the payment" section of [Apple Pay payment](/en/api-reference/payments/apple-pay). --- ## Quick troubleshooting | Symptom | Likely cause | | --- | --- | | Button doesn't appear | Outside Safari, or the device has no card set up in Apple Wallet. Confirm with `ApplePaySession.canMakePayments()`. | | `Error processing payment...` in `onError` and the PaymentSheet **never opens** | The `onUpdate` return value is off-contract — usually values in cents, as strings, or the already-formatted object. See ["What `onUpdate` must return"](#what-onupdate-must-return). | | A declined payment shows up as **approved** to the buyer | `onAuthorize` caught the error without rethrowing. See ["How to signal failure in `onAuthorize`"](#how-to-signal-failure-in-onauthorize). | | Click does nothing, no error | The button was mounted **after** `init` (SPA) — see "Load order" above. | | `init()` throws `Error: External ID is required...` | `externalId` missing or invalid — it's a synchronous exception, it does not go through `onError`. | | Generic Safari error on confirmation (e.g. `DOMException`) | Invalid merchant session — check whether the domain has the `.well-known` file published ([guide](/en/api-reference/payments/apple-pay-dominio)) and whether `external_id` is the installation's latest one. | | Works on one store but not another, same code | The second store's domain wasn't informed during installation (`domain_name`) or doesn't have the `.well-known` file published. | > **Did the sheet never open, or open and then fail?** > > That's the first question to ask, and it separates two worlds that never overlap: > > - **Never appeared** → the problem is in the browser, before any network call: the `onUpdate` contract, DOM/`init` order, or a missing `externalId`. > - **Appeared and failed midway** → the problem is merchant validation: the installation's `domain_name`, the `.well-known` file, or `external_id`. You can't test the full flow on `localhost` — see [Testing and Sandbox](/en/api-reference/payments/apple-pay) for environment details. --- > **Reference project for implementation** > > The [appmaxbrasil/appstore-demo-php](https://github.com/appmaxbrasil/appstore-demo-php) > repository contains a reference implementation of the full Apple Pay integration — > App Store installation, button rendering and `appmax.js` callbacks on the checkout, > and the call to the payment endpoint — in plain PHP and vanilla JavaScript, with no > framework. Use it as a study base and a starting point for your own implementation, > not as production code. > > The repository's [Apple Pay walkthrough](https://github.com/appmaxbrasil/appstore-demo-php/blob/main/FLUXO-APPLE-PAY.md) > links directly to the code line corresponding to each step described in this guide. --- Source: https://docs.appmax.com.br/en/api-reference/payments/parcelas.md # Installment calculation `POST /v1/payments/installments` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna o valor total com os juros aplicados em cada modalidade de parcelamento. A integração deve realizar a divisão para obter o valor de cada parcela. > **A quantidade e os valores das parcelas podem ser personalizados de 1 a** > > 12 parcelas. Os valores são configuráveis individualmente para cada > merchant. **Modalidades** - **PP** (Simples por parcela): a taxa de juros é aplicada diretamente sobre o valor de cada parcela. - **AM** (Financiamento): a taxa de juros é calculada mensalmente sobre o saldo devedor total. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `installments` | integer | sim | Número de parcelas desejado. | | `total_value` | integer | sim | Valor total do pedido em **centavos**. | | `settings` | boolean | não | Quando `true`, retorna as configurações de parcelamento do merchant. | ### Exemplo de requisição ```json { "installments": 10, "total_value": 10000, "settings": true } ``` ## Respostas ### 200 Cálculo realizado com sucesso. ```json { "data": { "installments": { "1": { "total": 10000 }, "2": { "total": 10200 }, "3": { "total": 10404 }, "10": { "total": 11600 } }, "settings": { "modality": "PP", "max_installments": 12, "min_installment_value": 500 } } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/listar-assinaturas.md # List subscriptions `GET /v1/subscriptions` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Lista as assinaturas de um cliente, identificado pelo **e-mail**, com paginação e filtro por status. Retorna 10 itens por página. > **Formato de data na listagem** > > Nesta rota `next_charge_at` e `charges[].charged_at` vêm no formato > `dd/mm/aaaa`. No detalhe da assinatura, `next_charge_at` vem como > `aaaa-mm-dd hh:mm:ss`. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Consulta | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `email` | string | sim | E-mail do cliente. | | `status` | enum: ACTIVE \| PAUSED \| CANCELLED | não | Filtra pelo status da assinatura. Aceita apenas `ACTIVE`, `PAUSED` e `CANCELLED`. Sem filtro, a listagem devolve todos os status, incluindo `OVERDUE`, `SUSPENDED` e `FINISHED` — veja `status` no item da listagem. | | `page` | integer | não | Página da listagem. | ## Respostas ### 200 Lista de assinaturas retornada com sucesso. ```json { "data": { "subscriptions": [ { "subscription_id": 10, "status": "ACTIVE", "products": [ { "name": "Camiseta Preta", "price": 199.9, "quantity": 1, "shopify_product_id": null, "variant_id": null, "product_id": 55, "image_url": null, "compare_at_price": null } ], "value": 199.9, "freight_value": 19.9, "current_cycle": 3, "customer": { "name": "Maria Silva", "email": "cliente@exemplo.com", "document": "12345678909", "phone": "11999999999", "address": { "street": "Rua São Bento", "number": "111", "complement": "Bloco 7", "district": "Centro", "city": "São Paulo", "state": "SP", "postcode": "01010000" } }, "payment": { "payment_info": { "brand": "visa", "final": "1234", "expiration": null } }, "charges": [ { "cycle": 1, "order_id": 12346, "shopify_order_id": null, "charged_at": "15/07/2026", "value": 199.9, "status": "paid" } ], "next_charge_at": "15/08/2026" } ], "pagination": { "total": 1, "per_page": 10, "current_page": 1, "last_page": 1 } } } ``` ### 422 Parâmetros inválidos (por exemplo, `email` ausente ou inválido). ```json { "errors": { "message": { "email": [ "The email field is required." ] } } } ``` ### 500 Erro ao listar as assinaturas. ```json { "errors": { "message": "Erro ao listar as assinaturas." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/criar-assinatura.md # Create subscription `POST /v1/subscriptions` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria uma assinatura a partir de um pedido existente. O pedido informado em `order_id` é transformado em uma cobrança recorrente. > **Pré-requisitos do pedido** > > - Deve existir e pertencer à mesma loja das credenciais. > - Forma de pagamento deve ser **cartão de crédito** ou **PIX**. > - Status deve ser **aprovado** ou **integrado** (ou seja, já liberado pelo antifraude). Um mesmo pedido pode originar mais de uma assinatura. > **Pedidos originados de um checkout Shopify** > > Quando o pedido tem carrinho Shopify vinculado, `products` é **obrigatório** e > cada item precisa de `shopify_product_id` **e** `shopify_variant_id` (o preço é > lido da variante na Shopify, não do payload). > > - Com `interval` informado, a cadência enviada vale para todos os itens. > - Sem `interval`, a cadência vem do plano de assinatura da variante e todos os > itens precisam ter a mesma frequência. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | Identificador do pedido de origem, que será transformado em assinatura. | | `interval` | enum: week \| month \| year | não | Unidade da periodicidade (semanas, meses ou anos). Combine com `interval_count` para obter a cadência — `month` + `2` = a cada dois meses. | | `interval_count` | integer | não | Quantidade de intervalos entre cobranças. | | `max_cycles` | integer | não | Número máximo de ciclos. Sem valor = ilimitado. | | `next_charge_at` | string | não | Data da próxima cobrança. | | `fail_max_tries` | integer | não | Tentativas em caso de falha na cobrança. | | `fail_interval_hours` | integer | não | Horas entre tentativas de cobrança. | | `products` | array | não | Produtos que compõem a assinatura. Informe **ou** `product_id` (produto interno da Appmax) **ou** o par `shopify_product_id` + `shopify_variant_id` (produto da Shopify). Obrigatório quando o pedido veio de um checkout Shopify — nesse caso só a forma Shopify é aceita. | | `products[].product_id` | integer | não | Identificador do produto interno da Appmax. | | `products[].shopify_product_id` | string | não | Identificador do produto na Shopify. Exigido junto de `shopify_variant_id`. | | `products[].shopify_variant_id` | string | não | Identificador da variante na Shopify. O preço é lido da variante, não do payload. | | `products[].quantity` | integer | não | Quantidade do produto cobrada a cada ciclo. | | `freight_value` | number | não | Valor do frete. | | `discount` | number | não | Desconto aplicado ao valor. | ### Exemplo de requisição ```json { "order_id": 12345, "interval": "month", "interval_count": 1, "max_cycles": 12, "products": [ { "product_id": 55, "quantity": 1 } ], "freight_value": 19.9, "discount": 10 } ``` ## Respostas ### 201 Assinatura criada com sucesso. ```json { "data": { "id": 10, "uuid": "6f1c0f1e-6a1c-4a2b-9f0e-3d5c7a9b1234", "status": "active", "subscription_status_id": 1, "interval": "month", "interval_count": 1, "interval_name": "Mensal", "charge_day": 15, "fail_max_tries": 3, "fail_interval_hours": 24, "max_cycles": 12, "completed_cycles": 0, "current_cycle": 1, "next_charge_at": "2026-08-15 00:00:00", "created_at": "2026-07-15 10:00:00", "freight_value": 19.9, "products": [ { "name": "Camiseta Preta", "price": 199.9, "quantity": 1, "product_id": 55, "shopify_product_id": null, "variant_id": null, "sku": "SKU-1", "image_url": null, "compare_at_price": null } ], "customer": { "name": "Maria Silva", "email": "cliente@exemplo.com", "document": "12345678909", "phone": "11999999999", "address": { "street": "Rua São Bento", "number": "111", "complement": "Bloco 7", "district": "Centro", "city": "São Paulo", "state": "SP", "postcode": "01010000" } }, "payment": { "payment_info": { "brand": "visa", "final": "1234", "expiration": "12/2029" } }, "charges": [ { "cycle": 0, "order_id": 12345, "shopify_order_id": null, "charged_at": "15/07/2026", "value": 209.8, "status": "paid" } ], "panel_url": "https://assinaturas.appmax.com.br/painel/6f1c0f1e-6a1c-4a2b-9f0e-3d5c7a9b1234" } } ``` ### 400 Pedido inválido para virar assinatura (forma de pagamento, status, token reutilizável ausente ou produtos inválidos). ```json { "errors": { "message": "Apenas pedidos de cartão de crédito ou PIX podem virar assinatura." } } ``` ### 404 Pedido não encontrado (ou de outra loja). ```json { "errors": { "message": "Pedido não encontrado." } } ``` ### 409 Conflito na seleção de produtos (o mesmo produto informado duas vezes). ```json { "errors": { "message": "Produto duplicado na seleção." } } ``` ### 422 Erro de validação dos campos ou regra de negócio (produto inválido, variante inexistente na Shopify, frequências divergentes ou desconto que deixa o valor abaixo do mínimo). ```json { "errors": { "message": "O desconto informado deixa o valor da assinatura abaixo do mínimo permitido." } } ``` ### 500 Erro ao criar a assinatura. ```json { "errors": { "message": "Erro ao criar a assinatura." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/consultar-assinatura.md # Get subscription `GET /v1/subscriptions/{id}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna os detalhes completos da assinatura, incluindo produtos, ciclos e o histórico de cobranças. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Respostas ### 200 Detalhes da assinatura retornados com sucesso. ```json { "data": { "subscription_id": 10, "status": "ACTIVE", "products": [ { "name": "Camiseta Preta", "shopify_product_id": null, "variant_id": null, "product_id": 55, "sku": "SKU-1", "quantity": 1, "price": 199.9, "image_url": null, "compare_at_price": null } ], "value": 199.9, "freight_value": 19.9, "interval": "month", "interval_count": 1, "current_cycle": 3, "completed_cycles": 2, "max_cycles": 12, "customer": { "name": "Maria Silva", "email": "cliente@exemplo.com", "document": "12345678909", "phone": "11999999999", "address": { "street": "Rua São Bento", "number": "111", "complement": "Bloco 7", "district": "Centro", "city": "São Paulo", "state": "SP", "postcode": "01010000" } }, "payment": { "payment_info": { "brand": "visa", "final": "1234", "expiration": "12/2029" } }, "next_charge_at": "2026-08-15 00:00:00", "last_charge_at": "15/07/2026", "charges": [ { "cycle": 2, "order_id": 12347, "shopify_order_id": null, "charged_at": "15/07/2026", "value": 199.9, "status": "paid" } ], "created_at": "2026-06-15 10:00:00", "canceled_at": null, "paused_from": null, "paused_until": null } } ``` ### 404 Assinatura não encontrada (ou de outra loja). ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 500 Erro ao buscar a assinatura. ```json { "errors": { "message": "Erro ao buscar a assinatura." } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/pausar-assinatura.md # Pause subscription `PATCH /v1/subscriptions/{id}/pause` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Pausa temporariamente a assinatura. Opcionalmente informe uma data até a qual ela permanece pausada e um motivo. Retorna o detalhe atualizado com `status: PAUSED`. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `paused_until` | string | não | Data até a qual a assinatura fica pausada. Sem valor, a pausa é por prazo indeterminado. | | `reason` | string | não | Motivo da pausa. | ### Exemplo de requisição ```json { "paused_until": "2026-09-01", "reason": "Cliente solicitou pausa temporária" } ``` ## Respostas ### 200 Assinatura pausada. Retorna o detalhe atualizado. ```json { "data": { "subscription_id": 10, "status": "PAUSED", "paused_from": "2026-08-04 00:00:00", "paused_until": "2026-09-01 00:00:00" } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 422 Erro de validação dos campos. ### 500 Erro ao pausar a assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/reativar-assinatura.md # Reactivate subscription `PATCH /v1/subscriptions/{id}/activate` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Reativa uma assinatura pausada, retomando as cobranças recorrentes. Não requer corpo. Retorna o detalhe atualizado com `status: ACTIVE`. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Respostas ### 200 Assinatura reativada. Retorna o detalhe atualizado. ```json { "data": { "subscription_id": 10, "status": "ACTIVE", "paused_from": null, "paused_until": null } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 500 Erro ao reativar a assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/cancelar-assinatura.md # Cancel subscription `PATCH /v1/subscriptions/{id}/cancel` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cancela a assinatura de forma definitiva — nenhuma nova cobrança será gerada. Opcionalmente registre um motivo. Retorna o detalhe atualizado com `status: CANCELLED` e `canceled_at` preenchido. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `reason` | string | não | Motivo do cancelamento. | ### Exemplo de requisição ```json { "reason": "Cliente não deseja mais o produto" } ``` ## Respostas ### 200 Assinatura cancelada. Retorna o detalhe atualizado. ```json { "data": { "subscription_id": 10, "status": "CANCELLED", "canceled_at": "2026-08-04 12:00:00" } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 422 Erro de validação dos campos. ### 500 Erro ao cancelar a assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/alterar-dia-cobranca.md # Change charge day `PATCH /v1/subscriptions/{id}/charge-day` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Altera o dia fixo de cobrança da assinatura. A mudança vale para todos os ciclos futuros e a próxima data de cobrança (`next_charge_at`) é recalculada. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `charge_day` | integer | sim | Dia fixo de cobrança (1 a 28). | ### Exemplo de requisição ```json { "charge_day": 15 } ``` ## Respostas ### 200 Dia de cobrança atualizado. ```json { "data": { "charge_day": 15, "next_charge_at": "2026-08-15 00:00:00" } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 422 `charge_day` fora do intervalo permitido (1 a 28). ```json { "errors": { "message": { "charge_day": [ "The charge day may not be greater than 28." ] } } } ``` ### 500 Erro ao alterar o dia de cobrança. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/alterar-periodicidade.md # Change frequency `PATCH /v1/subscriptions/{id}/frequency` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Altera a periodicidade (`interval` + `interval_count`) da assinatura e recalcula `next_charge_at`. Retorna o detalhe atualizado da assinatura. A periodicidade enviada precisa ser oferecida pelo produto da assinatura. Para assinaturas de produtos Shopify, isso significa existir uma variante de assinatura naquela cadência; caso contrário a troca é recusada com `422`. > **Quando a troca é recusada** > > - assinatura não está ativa ou está pausada; > - a periodicidade enviada é igual à atual; > - já existe uma cobrança em processamento (`409`); > - a próxima cobrança está dentro da janela do agendador (`409`). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `interval` | enum: week \| month \| year | sim | Nova unidade da periodicidade (semanas, meses ou anos). Combine com `interval_count` para obter a cadência — `month` + `2` = a cada dois meses. | | `interval_count` | integer | sim | Quantidade de intervalos entre cobranças. | ### Exemplo de requisição ```json { "interval": "month", "interval_count": 2 } ``` ## Respostas ### 200 Periodicidade alterada. Retorna o detalhe atualizado. ```json { "data": { "subscription_id": 10, "status": "ACTIVE", "interval": "month", "interval_count": 2, "next_charge_at": "2026-10-15 00:00:00" } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 409 Existe uma cobrança em processamento ou a próxima cobrança já entrou na janela do agendador. ```json { "errors": { "message": "charge_in_scheduler_window" } } ``` ### 422 Periodicidade indisponível para o produto, igual à atual, assinatura inativa/pausada ou campos inválidos. ```json { "errors": { "message": "frequency_not_available_for_product" } } ``` ### 500 Erro ao alterar a periodicidade da assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/pular-ciclo.md # Skip cycle `PATCH /v1/subscriptions/{id}/cycles/{cycleIndex}/skip` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Marca um ciclo futuro para ser pulado: a assinatura segue ativa, mas aquele ciclo não gera cobrança nem pedido — a cobrança salta para o próximo ciclo não pulado. `cycleIndex` é o número do ciclo (1 = primeiro ciclo de recorrência). Só é possível pular ciclos ainda **não processados** — use `completed_cycles` do detalhe da assinatura como referência. Para desfazer, chame [desfazer o skip](/api-reference/subscriptions/desfazer-skip-ciclo). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | | `cycleIndex` | integer | sim | Número do ciclo de recorrência (1 = primeiro ciclo após a cobrança de origem). Precisa ser maior que `completed_cycles`. | ## Respostas ### 200 Ciclo marcado como pulado. ```json { "data": { "cycle_index": 3, "status": "skipped", "next_charge_at": "2026-10-15 00:00:00" } } ``` ### 400 Ciclo já processado, fora do limite de `max_cycles` ou assinatura que não está ativa. ```json { "errors": { "message": "Só é possível pular ciclos que ainda não foram processados." } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 500 Erro ao pular o ciclo da assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/desfazer-skip-ciclo.md # Unskip cycle `PATCH /v1/subscriptions/{id}/cycles/{cycleIndex}/unskip` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Remove a marcação de "pulado" de um ciclo futuro, devolvendo-o à fila de cobrança. O ciclo precisa estar marcado como pulado e ainda não ter sido processado. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | | `cycleIndex` | integer | sim | Número do ciclo de recorrência (1 = primeiro ciclo após a cobrança de origem). Precisa ser maior que `completed_cycles`. | ## Respostas ### 200 Skip desfeito. O ciclo volta a ser cobrado. ```json { "data": { "cycle_index": 3, "status": "unbilled", "next_charge_at": "2026-09-15 00:00:00" } } ``` ### 400 Ciclo já processado ou que não estava marcado como pulado. ```json { "errors": { "message": "O ciclo informado não está marcado como pulado." } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 500 Erro ao desfazer o skip do ciclo da assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/atualizar-endereco.md # Update delivery address `PATCH /v1/subscriptions/{id}/address` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Atualiza o endereço de entrega associado à assinatura (dados do cliente vinculado). Aplica-se aos próximos ciclos de cobrança. O `postcode` é gravado apenas com dígitos (a pontuação enviada é removida) e o `state` é gravado em maiúsculas — a resposta devolve os valores já normalizados. > **A alteração é feita no **cadastro do cliente**, então vale para todas as** > > assinaturas e pedidos futuros dele, não só para esta assinatura. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `address` | object | sim | | | `address.postcode` | string | sim | CEP. | | `address.street` | string | sim | Logradouro. | | `address.number` | string | sim | Número. | | `address.complement` | string | não | Complemento. | | `address.district` | string | sim | Bairro. | | `address.city` | string | sim | Cidade. | | `address.state` | string | sim | UF (sigla de 2 letras). | ### Exemplo de requisição ```json { "address": { "postcode": "01010-000", "street": "Rua São Bento", "number": "111", "complement": "Bloco 7", "district": "Centro", "city": "São Paulo", "state": "SP" } } ``` ## Respostas ### 200 Endereço atualizado. ```json { "data": { "address": { "street": "Rua São Bento", "number": "111", "complement": "Bloco 7", "district": "Centro", "city": "São Paulo", "state": "SP", "postcode": "01010000" } } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 422 Erro de validação dos campos do endereço. ```json { "errors": { "message": { "address.postcode": [ "The address.postcode field is required." ] } } } ``` ### 500 Erro ao atualizar o endereço da assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/atualizar-tag.md # Update subscription tag `PATCH /v1/subscriptions/{id}/tag` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Define o apelido da assinatura — um rótulo livre de até 255 caracteres que você escolhe para identificar a assinatura nos seus relatórios. Envie `tag` vazia (`""` ou `null`) para remover o apelido. Sem apelido definido, todas as respostas da API devolvem o padrão `#assinatura{id}`. > **A tag não aparece no painel Appmax nem no portal do assinante — ela existe** > > apenas nas respostas desta API. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `tag` | string \| null | sim | Apelido da assinatura. Vazio ou `null` remove o apelido e faz a API voltar a devolver o padrão `#assinatura{id}`. | ### Exemplo de requisição ```json { "tag": "Clube do Whey" } ``` ## Respostas ### 200 Tag atualizada. Retorna o detalhe atualizado da assinatura. ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 422 Erro de validação da tag. ```json { "errors": { "message": { "tag": [ "The tag may not be greater than 255 characters." ] } } } ``` ### 500 Erro ao atualizar o apelido da assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/produtos-disponiveis.md # List available products `GET /v1/subscriptions/{id}/available-products` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Lista os produtos que ainda não estão na assinatura e podem ser adicionados a ela, já filtrados pela periodicidade da assinatura. O formato de cada item depende da origem da assinatura: - **Assinaturas de produtos Shopify** — um item por produto, com a lista de `variants` compatíveis com a cadência da assinatura. `q` é obrigatório na prática: sem termo de busca a resposta vem vazia. - **Assinaturas de produtos internos da Appmax** — lista simples de produtos ativos da loja (`product_id`, `name`, `price`, `sku`), até 30 itens. Sem `q`, retorna os primeiros produtos em ordem alfabética. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Parâmetros de Consulta | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `q` | string | não | Termo de busca por nome ou SKU do produto. Para assinaturas Shopify, sem este parâmetro a resposta vem vazia. | ## Respostas ### 200 Produtos disponíveis retornados com sucesso. ```json { "data": { "products": [ { "shopify_product_id": "123", "title": "Camiseta Branca", "variants": [ { "shopify_variant_id": "456", "title": "Mensal", "price": "189.90", "sku": "SKU-2", "interval": "month", "interval_count": 1 } ] } ] } } ``` ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 500 Erro ao listar os produtos disponíveis. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/adicionar-produtos.md # Add products `POST /v1/subscriptions/{id}/products` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Adiciona um ou mais produtos a uma assinatura existente. Retorna o detalhe atualizado da assinatura. Funciona para os dois tipos de assinatura, e a forma de identificar o produto muda conforme o tipo — sempre use os identificadores devolvidos por [produtos disponíveis](/api-reference/subscriptions/produtos-disponiveis): - **Produtos internos da Appmax** → `product_id` (+ `quantity`). Nome, preço e SKU vêm do cadastro do produto; se enviados, são ignorados. - **Produtos da Shopify** → `shopify_product_id` **e** `shopify_variant_id` (+ `quantity`). O preço é lido da variante. > **A assinatura precisa estar "parada"** > > Alterar produtos só é permitido enquanto a assinatura está ativa, não > pausada e sem cobrança em andamento. Veja os erros `409` e `422` abaixo. > **Assinaturas sem produtos** > > A gestão de produtos exige uma assinatura com produtos definidos: criada > com `products` no [criar assinatura](/api-reference/subscriptions/criar-assinatura) > ou originada de um checkout Shopify. Assinaturas criadas sem `products` > (que herdam o valor do pedido) respondem `404` aqui. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `products` | array | sim | Lista de produtos a adicionar. | | `products[].product_id` | integer | não | Produto interno da Appmax. Usado em assinaturas de produtos internos. | | `products[].shopify_product_id` | string | não | Produto na Shopify. Exigido junto de `shopify_variant_id`. | | `products[].shopify_variant_id` | string | não | Variante na Shopify. Exigido junto de `shopify_product_id`. | | `products[].name` | string | não | Opcional e apenas informativo — nome, preço e SKU são resolvidos do cadastro do produto (interno) ou da variante (Shopify). | | `products[].price` | number | não | Opcional e apenas informativo. Veja `name`. | | `products[].sku` | string | não | Opcional e apenas informativo. Veja `name`. | | `products[].qty` | integer | não | Quantidade (alias de `quantity`). | | `products[].quantity` | integer | não | Quantidade do produto cobrada a cada ciclo. | ### Exemplo de requisição ```json { "products": [ { "product_id": 55, "quantity": 1 } ] } ``` ## Respostas ### 200 Produtos adicionados. Retorna o detalhe atualizado. ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 409 Produto repetido na requisição, produto que já está na assinatura, ou assinatura com cobrança em andamento / dentro da janela do agendador. ```json { "errors": { "message": "Produto já está na assinatura." } } ``` ### 422 Erro de validação, produto inválido para a assinatura, ou assinatura inativa/pausada. ```json { "errors": { "message": { "products": [ "The products field is required." ] } } } ``` ### 500 Erro ao adicionar produto à assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/alterar-quantidade-produto.md # Update product quantity `PATCH /v1/subscriptions/{id}/products/{variantId}/quantity` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Altera a quantidade de um produto já presente na assinatura. Vale para os dois tipos de assinatura — o produto é identificado na URL pelo `variantId`, que é o `product_id` (produtos internos da Appmax) ou o `variant_id` (produtos da Shopify) mostrado no detalhe da assinatura. Retorna o detalhe atualizado. > **A assinatura precisa estar "parada"** > > Alterar produtos só é permitido enquanto a assinatura está ativa, não > pausada e sem cobrança em andamento. Veja os erros `409` e `422` abaixo. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | | `variantId` | string | sim | Identificador do produto dentro da assinatura, retornado no detalhe: - assinaturas de produtos Shopify → `variant_id` (o `shopify_variant_id`); - assinaturas de produtos internos da Appmax → `product_id`. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `quantity` | integer | sim | Nova quantidade do produto. | ### Exemplo de requisição ```json { "quantity": 3 } ``` ## Respostas ### 200 Quantidade atualizada. Retorna o detalhe atualizado. ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 409 Assinatura com cobrança em andamento ou dentro da janela do agendador. ```json { "errors": { "message": "charge_in_scheduler_window" } } ``` ### 422 Erro de validação, produto ausente na assinatura ou assinatura inativa/pausada. ```json { "errors": { "message": { "quantity": [ "The quantity field is required." ] } } } ``` ### 500 Erro ao atualizar quantidade do produto na assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/subscriptions/remover-produto.md # Remove product `DELETE /v1/subscriptions/{id}/products/{variantId}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Remove um produto da assinatura. Vale para os dois tipos de assinatura — o produto é identificado na URL pelo `variantId`, que é o `product_id` (produtos internos da Appmax) ou o `variant_id` (produtos da Shopify) mostrado no detalhe da assinatura. Retorna o detalhe atualizado, sem o produto removido. A assinatura precisa manter pelo menos um produto — para encerrá-la por completo use [cancelar assinatura](/api-reference/subscriptions/cancelar-assinatura). > **A rota é `DELETE /v1/subscriptions/{id}/products/{variantId}` — sem o** > > sufixo `/quantity`. Alterar produtos só é permitido enquanto a assinatura > está ativa, não pausada e sem cobrança em andamento. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `id` | integer | sim | Identificador da assinatura. | | `variantId` | string | sim | Identificador do produto dentro da assinatura, retornado no detalhe: - assinaturas de produtos Shopify → `variant_id` (o `shopify_variant_id`); - assinaturas de produtos internos da Appmax → `product_id`. | ## Respostas ### 200 Produto removido. Retorna o detalhe atualizado. ### 404 Assinatura não encontrada. ```json { "errors": { "message": "Assinatura não encontrada." } } ``` ### 409 Assinatura com cobrança em andamento ou dentro da janela do agendador. ```json { "errors": { "message": "charge_in_flight" } } ``` ### 422 Último produto da assinatura, produto ausente na assinatura ou assinatura inativa/pausada. ```json { "errors": { "message": "A assinatura deve manter ao menos um produto." } } ``` ### 500 Erro ao remover produto da assinatura. --- Source: https://docs.appmax.com.br/en/api-reference/refunds/criar-estorno.md # Create a refund `POST /v1/orders/refund-request` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria uma solicitação de estorno (total ou parcial) para um pedido. Para **boleto**, é necessário informar os dados bancários do cliente na Appmax para que o valor seja ressarcido. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `order_id` | integer | sim | ID do pedido. | | `type` | enum: total \| partial | sim | Tipo de reembolso. Padrão: `total`. | | `value` | integer | não | Valor do reembolso em **centavos**. Obrigatório quando `type = partial`. | ### Exemplo de requisição ```json { "order_id": 1, "type": "total" } ``` ## Respostas ### 201 Estorno aceito. ```json { "data": { "message": "Refund request accepted" } } ``` ### 400 Erro de validação ou regra de negócio. ### 404 Pedido não encontrado. ### 500 Erro interno ao processar o estorno. --- Source: https://docs.appmax.com.br/en/api-reference/payment-links/criar-link-pagamento.md # Create a payment link `POST /v1/payment-link` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria um novo link de pagamento. A resposta traz o `checkout_url` — a URL hospedada pela Appmax que você compartilha com o comprador para que ele finalize o pagamento. A requisição deve conter um token Bearer do **merchant**, obtido no fluxo de autenticação. Veja [Autenticação](/guides/autenticacao). > **O campo `value` é informado em **centavos** e o mínimo aceito é `500`** > > (R$ 5,00). O campo `fee_transfer_from_installment` define a partir de qual parcela o custo do parcelamento é repassado ao comprador. Com o valor `0` (default), o custo é absorvido pelo merchant em todas as parcelas. O campo `document` é opcional. Quando informado, deve conter um CPF ou CNPJ válido e o merchant deve possuir vínculo com a empresa do documento informado. > **Comportamento padrão do documento** > > Quando já existe um link de pagamento criado e vinculado a uma empresa, > a última empresa utilizada na criação de um link passa a ser o padrão. > Portanto, ao criar um novo link **sem informar o campo `document`**, o > sistema continuará criando o link vinculado à última empresa utilizada, > mesmo que uma nova empresa tenha sido cadastrada posteriormente. Para > garantir que o link seja vinculado a uma empresa específica, informe o > `document` correspondente no payload. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `name` | string | sim | Nome do link de pagamento. | | `value` | integer | sim | Valor do link em **centavos**. Mínimo `500`. | | `description` | string | sim | Descrição do link de pagamento. | | `product_type` | enum: physical \| digital | não | Tipo do produto. | | `document` | string | não | CPF ou CNPJ vinculado ao link de pagamento. Quando informado, deve ser um documento válido e o merchant precisa possuir vínculo com a empresa deste documento. | | `payments` | array | sim | Métodos de pagamento habilitados no link. | | `max_installments` | integer | sim | Número máximo de parcelas. | | `fee_transfer_from_installment` | integer | sim | Parcela a partir da qual o custo do parcelamento é absorvido/repassado. Com `0`, o custo é absorvido pelo merchant. | | `allow_multiple_sales` | boolean | não | Indica se o link de pagamento permite alterar a quantidade de produtos a ser vendidos no carrinho. Quando `true`, permite editar a quantidade; quando `false`, é desativado. Por padrão, o valor é `true`; caso o dado não seja enviado, o link permanecerá com a opção ativa. | ## Respostas ### 201 Link de pagamento criado com sucesso. ### 400 Requisição inválida. ### 401 Não autorizado. ### 403 Usuário bloqueado para criar links de pagamento. ### 404 Empresa do documento não encontrada ou não vinculada ao merchant. ### 422 Erro na validação dos dados ou erro de negócio ao processar a criação. ```json { "errors": { "message": { "name": [ "O campo name é obrigatório." ], "value": [ "O campo value é obrigatório." ], "description": [ "O campo description é obrigatório." ], "payments": [ "O campo payments é obrigatório." ] } } } ``` --- Source: https://docs.appmax.com.br/en/api-reference/payment-links/consultar-link-pagamento.md # Get payment link data `GET /v1/payment-link/{payment_link_id}/orders` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna os pedidos gerados por um determinado link de pagamento, com o status de cada um. Use para acompanhar as conversões do link criado em [`POST /v1/payment-link`](/api-reference/payment-links/criar-link-pagamento). A resposta é paginada: `meta` traz os totais da consulta e `links` traz as URLs de navegação entre as páginas. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `payment_link_id` | string | sim | ID do link de pagamento (retornado em `POST /v1/payment-link`). | ## Respostas ### 200 Link de pagamento encontrado com sucesso. ```json { "data": [ { "order": { "id": 1, "status": "cancelado", "total": 6000 } }, { "order": { "id": 2, "status": "pendente", "total": 6000 } } ], "meta": { "total": 2, "page": 1, "per_page": 20, "total_pages": 1 }, "links": { "self": "https://api.appmax.com.br/v1/payment-link/1/orders?page=1", "next": null, "prev": null, "last": "https://api.appmax.com.br/v1/payment-link/1/orders?page=1" } } ``` ### 401 Erro de autenticação. --- Source: https://docs.appmax.com.br/en/api-reference/split/criar-recebedor.md # Create a recipient `POST /v1/recipient` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cadastra um recebedor para receber valores de [split de pagamentos](/guides/split-pagamentos). O cadastro é só a primeira etapa. Para completar o fluxo, o recebedor também precisa concluir o [facematch (KYC)](/api-reference/split/facematch-link) — enquanto o KYC não é aprovado, ele não participa de splits. Os três status possíveis do onboarding são: | Status | Significado | | --- | --- | | `Awaiting face match completion` | Aguardando o recebedor concluir a etapa de facematch (KYC). | | `Onboarding on verification` | Recebemos todos os dados do formulário + facematch e estamos analisando as informações. | | `Onboarding completed` | Cadastro completo e aprovado. | Referência completa em [Status do split de pagamentos](/guides/split-status#status-do-recebedor-recipient). A conta bancária (`bankAccount`) pode ser enviada já no cadastro. Os códigos de banco (COMPE) homologados e os tipos de conta aceitos estão em [Bancos homologados](/guides/bancos-homologados). > **Uma vez criado, o recebedor **não pode ser editado nem excluído via** > > API**. Revise os dados antes de enviar — correções são feitas pelo > suporte da Appmax caso a caso. > **Um CNPJ só pode ser cadastrado uma vez. Tentar recriar retorna `422`** > > com a mensagem `"O valor indicado para o campo company.company > document number já se encontra utilizado."`. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `triage` | object | sim | | | `triage.revenue` | integer | sim | Faturamento mensal online em reais (inteiro). `1000` = R$ 1.000,00. | | `triage.storeUrl` | string | sim | URL da página de vendas da loja. | | `account` | object | sim | | | `account.email` | string | sim | E-mail do titular da conta. | | `account.name` | string | sim | Nome completo do titular. | | `account.cpf` | string | sim | CPF do titular (apenas dígitos). | | `account.phone` | string | sim | Telefone do titular. | | `account.dateOfBirth` | string | sim | Data de nascimento (YYYY-MM-DD). | | `company` | object | sim | | | `company.companyName` | string | sim | Razão social da empresa. | | `company.companyDocumentNumber` | string | sim | CNPJ da empresa — apenas dígitos, 14 posições, sem pontuação. | | `company.companyPostcode` | string | sim | CEP do endereço da empresa (apenas dígitos). | | `company.companyAddress` | string | sim | Logradouro (rua, avenida) do endereço da empresa. | | `company.companyAddressNumber` | string | sim | Número do endereço. | | `company.companyAddressState` | string | sim | Estado (UF) em duas letras. | | `company.companyAddressNeighborhood` | string | sim | Bairro. | | `company.companyCity` | string | sim | Cidade. | | `company.companyAddressComplement` | string | não | Complemento do endereço (opcional). | | `bankAccount` | object | não | Conta bancária do recebedor, usada para os saques. Se o objeto for enviado, **todos os campos abaixo passam a ser obrigatórios**. | | `bankAccount.bank` | integer | sim | Código do banco (COMPE). Consulte a lista completa de instituições homologadas em [Bancos homologados](/guides/bancos-homologados). | | `bankAccount.agency` | string | sim | Agência da conta, com dígito quando houver. | | `bankAccount.account` | string | sim | Número da conta, com dígito verificador. | | `bankAccount.bankAccountType` | enum: CC \| CD \| PG \| PP | sim | Tipo de conta: `CC` (conta corrente), `CD` (conta digital), `PG` (conta de pagamento) ou `PP` (conta poupança). Veja [Bancos homologados](/guides/bancos-homologados). | | `config` | object | não | Configurações do recebedor na plataforma. | | `config.hasAccountAccess` | boolean | não | Define se o recebedor terá acesso ao painel da própria conta. Enviado como booleano (`true` ou `false`). | ## Respostas ### 201 Recebedor criado com sucesso. ### 403 Token ausente ou inválido. ### 422 Erro de validação dos dados enviados. ```json { "data": { "message": { "account.name": [ "O campo account.name é obrigatório." ], "account.cpf": [ "O campo account.cpf é obrigatório." ], "company.companyName": [ "O campo company.company name é obrigatório." ], "company.companyDocumentNumber": [ "O campo company.company document number é obrigatório." ], "company.companyPostcode": [ "O campo company.company postcode é obrigatório." ], "company.companyAddress": [ "O campo company.company address é obrigatório." ], "company.companyAddressNumber": [ "O campo company.company address number é obrigatório." ], "company.companyAddressState": [ "O campo company.company address state é obrigatório." ], "company.companyCity": [ "O campo company.company city é obrigatório." ], "company.companyAddressNeighborhood": [ "O campo company.company address neighborhood é obrigatório." ], "triage.revenue": [ "O campo triage.revenue é obrigatório." ], "triage.storeUrl": [ "O campo triage.store url é obrigatório." ] } } } ``` ### 500 Erro do Servidor Interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/facematch-link.md # Create facematch link (KYC) `POST /v1/recipient/{recipient_hash}/facematch-link` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Gera o link de facematch (KYC) para um recebedor recém-criado e envia um SMS com esse link para o telefone informado. > **SMS **não são enviados em homologação**. Teste o disparo do facematch** > > em produção. > **O telefone enviado neste endpoint **não precisa ser o mesmo** do** > > `account.phone` usado na criação do recebedor. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `recipient_hash` | string | sim | Hash do recebedor (retornado em `POST /v1/recipient`). | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `phone` | string | sim | Telefone que recebe o SMS (formato com DDD: `11999999999`). | ### Exemplo de requisição ```json { "phone": "11999999999" } ``` ## Respostas ### 201 Link de facematch criado e SMS disparado. ```json { "message": "Facematch created successfully" } ``` ### 500 Erro interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/consultar-recebedor.md # Get recipient status `GET /v1/recipient/{recipient_hash}/status` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna o status atual do onboarding do recebedor. Use para descobrir se o recebedor está pronto para receber splits (`Onboarding completed`). Para a referência completa dos status, veja [Status do split de pagamentos](/guides/split-status). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `recipient_hash` | string | sim | Hash do recebedor (retornado em `POST /v1/recipient`). | ## Respostas ### 200 Status retornado. ```json { "data": "Onboarding completed" } ``` ### 500 Erro interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/criar-split-pedido.md # Create order split `POST /v1/orders/{orderId}/split-order` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Divide o valor líquido de um pedido entre um ou mais recebedores. Os valores são informados em **centavos** e cada linha referencia um `recipient_hash` previamente aprovado (`Onboarding completed`). > **Não é permitido criar ou alterar split em pedidos com status** > > `aprovado`. Pedidos com split também não aceitam estorno parcial — só > estorno total. > **Valores e taxas** > > O split é limitado ao `partner_total` — valor do pedido menos as taxas > da Appmax. A taxa é descontada automaticamente do marketplace; **não > envie taxa no payload**. Se a soma ultrapassar o `partner_total`, a > divisão é proporcional e o último recebedor recebe apenas o residual, > **sem retorno de erro**. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `orderId` | integer | sim | ID do pedido na Appmax. | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `split` | array | sim | | | `split[].amount` | integer | sim | Valor destinado ao recebedor em **centavos**. | | `split[].recipient_hash` | string | sim | Hash do recebedor previamente aprovado. | ### Exemplo de requisição ```json { "split": [ { "amount": 1000, "recipient_hash": "d9bd0dae-3274-5e5a-939f-f50d867eb652" }, { "amount": 500, "recipient_hash": "d9bd0dae-3274-5e5a-939f-f50d867eb653" } ] } ``` ## Respostas ### 201 Split criado com sucesso. ```json { "message": "Split order created successfully" } ``` ### 422 Erro de validação dos itens do split. --- Source: https://docs.appmax.com.br/en/api-reference/split/saldos.md # Get recipient balances `GET /v1/recipient/{recipient_hash}/balances` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna os saldos financeiros do recebedor. Pode haver dois tipos: - `available` — saldo disponível para saque imediato. - `to_release` — saldo ainda em compensação (pode ser antecipado). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `recipient_hash` | string | sim | Hash do recebedor (retornado em `POST /v1/recipient`). | ## Respostas ### 200 Saldos retornados. ```json { "data": [ { "type": "available", "value": "150.00" }, { "type": "to_release", "value": "250.00" } ] } ``` ### 404 Saldos não provisionados para esse recebedor. ### 500 Erro interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/simular-antecipacao.md # Simulate withdrawal anticipation `GET /v1/recipient/{recipient_hash}/withdraw-request/anticipation/simulate` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Simula a antecipação de um saque e retorna valor bruto, valor líquido, taxa aplicada e percentual da taxa. > ****Nenhuma solicitação de saque é criada** por este endpoint. Ele é** > > puramente informativo, para cálculo antes da confirmação real. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `recipient_hash` | string | sim | Hash do recebedor (retornado em `POST /v1/recipient`). | ## Parâmetros de Consulta | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `value` | string | sim | Valor do saque a simular em **centavos**. | ## Respostas ### 200 Simulação concluída. ```json { "data": { "value": 10000, "net_value": 4700, "withdraw_tax": 300, "tax_percentage": 300 } } ``` ### 404 Recebedor não encontrado. ### 422 Conta bancária inválida. ### 423 Saque já em andamento. ### 500 Erro interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/antecipacao.md # Request withdrawal anticipation `POST /v1/recipient/{recipient_hash}/withdraw-request/anticipation` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria uma solicitação real de antecipação usando saldo do tipo `to_release`. Ao contrário da simulação, este endpoint efetivamente movimenta o saldo. ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `recipient_hash` | string | sim | Hash do recebedor (retornado em `POST /v1/recipient`). | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `value` | integer | sim | Valor do saque em **centavos** (inteiro positivo). | ### Exemplo de requisição ```json { "value": 1000 } ``` ## Respostas ### 201 Antecipação criada com status `2 = pending`. ```json { "data": { "withdraw_request_id": 1, "status": 2, "value": 10000, "net_value": 4700, "withdraw_tax": 5000 } } ``` ### 403 `Withdraw not allowed` (pode indicar `withdrawal_blocked`). ### 404 Recebedor não encontrado. ### 409 Saque em andamento para o mesmo recebedor. ### 422 Validação de input ou regra de negócio (saldo insuficiente, limite, conta inválida). ### 500 Erro interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/saque-disponivel.md # Request withdrawal with available balance `POST /v1/recipient/{recipient_hash}/withdraw-request/available` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Cria uma solicitação de saque usando o saldo **available**. Sem taxa de antecipação — o saldo já está liberado. > **Se o recebedor tem `to_release` mas não `available`, este endpoint** > > retorna `Insufficient balance`. Nesse caso, use > [antecipação](/api-reference/split/antecipacao). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `recipient_hash` | string | sim | Hash do recebedor (retornado em `POST /v1/recipient`). | ## Corpo da requisição (obrigatório) | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `value` | integer | sim | Valor do saque em **centavos** (inteiro positivo). | ### Exemplo de requisição ```json { "value": 1000 } ``` ## Respostas ### 201 Saque criado com status `2 = pending`. ```json { "data": { "withdraw_request_id": 1, "status": 2, "value": 0, "net_value": 0, "withdraw_tax": 0 } } ``` ### 403 Saque não permitido. ### 404 Recebedor não encontrado. ### 409 Saque em andamento. ### 422 Validação ou regra de negócio (`Insufficient balance`, `Invalid bank account`). ### 500 Erro interno. --- Source: https://docs.appmax.com.br/en/api-reference/split/consultar-solicitacao-saque.md # Get withdrawal request `GET /v1/withdraw-request/{withdrawRequestId}` **Base URL** - Produção: https://api.appmax.com.br - Sandbox: https://api.sandboxappmax.com.br Retorna os detalhes de uma solicitação de saque específica pelo seu identificador (`withdrawRequestId`), incluindo status atual, valores e a conta bancária vinculada. Cobre tanto solicitações criadas via [saldo disponível](/api-reference/split/saque-disponivel) quanto via [antecipação](/api-reference/split/antecipacao). ## Autenticação - `Authorization: Bearer ` Token Bearer do **merchant** obtido via `POST /oauth2/token` usando as credenciais do merchant (não do app). Veja [Autenticação](/guides/autenticacao). ## Parâmetros de Caminho | Campo | Tipo | Obrigatório | Descrição | |---|---|---|---| | `withdrawRequestId` | integer | sim | ID da solicitação de saque. | ## Respostas ### 200 Solicitação encontrada. ```json { "data": { "withdraw_request_id": 69, "status": "pending", "net_value": 984, "gross_value": 1000, "withdraw_tax": 0, "currency": "BRL", "created_at": "2025-12-30 11:08:04", "source_type": "recipient", "source_id": "4471b2d1-5e30-5f4d-a64b-c18d72c82a79", "bank_account": { "id": 9, "bank": "66", "agency": "512", "account": "9260373", "name": null, "type": "national_pj", "profile": "CC", "register_number": null, "pix_key": "test@test.com", "bank_account_type": null } } } ``` ### 403 Não autorizado. ```json { "message": "Unauthorized" } ``` ### 404 Solicitação de saque não encontrada. ```json { "error": "Withdraw request not found" } ``` ### 500 Erro interno. ```json { "error": "Internal Server Error" } ``` --- Source: https://docs.appmax.com.br/en/llms.md # MCP & llms.txt Configure your AI agent to access the Appmax documentation — via plain text file or MCP server with 13 integrated tools. ## llms.txt We provide the entire Appmax API documentation in a single plain text file, following the [llms.txt](https://llmstxt.org/) standard, to make it easy for language models (LLMs), AI agents, and automation tools to consume. ### Download - [llms.txt (English)](/llms-en.txt): Full documentation in plain text (~262 KB, 54 sections). - [llms.txt (Portugues)](/llms.txt): Complete documentation in Portuguese plain text (~269 KB, 54 sections). ### What is llms.txt? [llms.txt](https://llmstxt.org/) is an open standard that allows websites to provide their content in a format optimized for LLMs. Similar to `robots.txt` for crawlers, `llms.txt` makes it easy for AI agents to access and understand documentation efficiently. ### What's included The file contains all documentation pages in clean Markdown format: - **Guides** — Quickstart, getting started, authentication, installation & callback, external-id, webhooks, rate limit, environments, publishing to production, FAQ, AI & MCP - **Examples** — Full integration, installments, recurring, checkout - **API Reference** — Customers, orders, payments (credit card, Pix, boleto, Apple Pay, Apple Pay merchant session), refunds, subscriptions, payment links - **Payment split** — Guides (overview, status, approved banks, FAQ) + endpoints (recipient, facematch, order split, balances, anticipation, withdrawal) ### How to use #### With Claude, ChatGPT, or another LLM Paste the file content as context in your conversation: ``` Here is the Appmax API documentation: [llms.txt content] Based on this documentation, help me implement... ``` #### With AI agents Point your agent directly to the URL: ``` https://docs.appmax.com.br/llms-en.txt ``` ## MCP server > **Open standard** > > Our server implements the [**Model Context Protocol**](https://modelcontextprotocol.io/) — the open standard maintained by Anthropic for connecting AI agents to external data sources and tools. Any MCP-compatible client (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code with Copilot, among others) connects without any proprietary adapter. The server exposes **13 tools**: **Documentation** | Tool | Description | | ---- | ----------- | | `list_pages` | List available pages. Supports `prefix` filter (e.g., `api-` to list only endpoints). | | `get_page` | Get the full content of a page by ID | | `search_docs` | Search the documentation by term (top 10 by relevance) | | `get_full_docs` | Get the entire documentation as plain text (~140 KB) | | `check_health` | Server status and documentation stats | **Diagnostics** | Tool | Description | | ---- | ----------- | | `diagnose_error` | Diagnose HTTP errors (401/422/429/500) with likely root cause and suggested fix | | `validate_payload` | Validate a JSON payload against the endpoint schema before sending to the API | | `validate_order_total` | Verify order total calculation locally | | `validate_installation_flow` | Audits the installation flow implementation (4 steps + validation URL) from project snippets, reporting pass/fail per step | **Code generation** | Tool | Description | | ---- | ----------- | | `generate_code_snippet` | Generate a ready-to-use snippet in curl/Node/Python/PHP/Go for any endpoint | | `get_integration_flow` | Get a step-by-step flow (checkout, installation, subscription, etc.) | **Onboarding and webhooks** | Tool | Description | | ---- | ----------- | | `get_onboarding_checklist` | Personalized admin checklist by integration type and stage | | `get_webhook_schema` | Typed schema + example payload for any of the 28 webhook events | ### Health check Before using the MCP, verify the server is operational: ```bash curl https://m7nwi1m199.execute-api.us-east-1.amazonaws.com/health ``` Expected response: ```json { "status": "healthy", "name": "appmax-docs", "version": "1.0.0", "tools": ["list_pages", "get_page", "search_docs", "get_full_docs", "check_health", "diagnose_error", "validate_payload", "validate_order_total", "validate_installation_flow", "generate_code_snippet", "get_integration_flow", "get_onboarding_checklist", "get_webhook_schema"] } ``` ### Configuration #### Claude Code Add to `.mcp.json` in your project root: ```json { "mcpServers": { "appmax-docs": { "type": "http", "url": "https://m7nwi1m199.execute-api.us-east-1.amazonaws.com/mcp" } } } ``` #### Claude Desktop Claude Desktop still connects natively over stdio, so you need the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) shim (installed on demand via `npx`) to bridge stdio ↔ HTTP. Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "appmax-docs": { "command": "npx", "args": [ "-y", "mcp-remote", "https://m7nwi1m199.execute-api.us-east-1.amazonaws.com/mcp" ] } } } ``` Requires Node.js installed on your system (`npx` ships with it). #### Any MCP-compatible client Point to the endpoint: ``` https://m7nwi1m199.execute-api.us-east-1.amazonaws.com/mcp ``` ## Quick test Once configured, test with a simple call. Example with `search_docs`: ``` You: "How does the payment confirmation webhook work?" The agent calls: search_docs({ "query": "webhook payment confirmation" }) Returns: relevant excerpts from the webhooks and order status guides. ``` If the agent answered with Appmax documentation content, the connection is working. You now have access to all 13 tools. [See the full reference for all tools →](/en/guides/ia-ferramentas) ## Next steps - [Why use AI?](/en/guides/ia): See the benefits, demo videos, and how AI accelerates your integration. - [Quickstart](/en/quickstart): Prefer to write code by hand? Jump straight into the quickstart.