# API Reference
Source: https://docs.wava.co/api-reference/introduction
Complete reference for all Wava API endpoints.
# API Reference
This section provides the complete technical reference for all Wava API endpoints, auto-generated from our OpenAPI specification.
## Base URLs
| Environment | URL |
| - | - |
| Production | `https://api.wava.co/v1` |
| Development | `https://api.dev.wava.co/v1` |
## Authentication
Include your merchant key in every request:
```bash theme={null}
merchant-key: YOUR_MERCHANT_KEY
```
Partners include two additional headers for two-factor authentication:
```bash theme={null}
X-API-Key: YOUR_PARTNER_API_KEY
X-API-Secret: YOUR_PARTNER_SECRET_KEY
```
## Interactive playground
Each endpoint below includes an interactive playground where you can test API calls directly. Switch between development and production using the server selector.
The interactive playground makes real API calls. Use your development credentials and the development server for testing.
# Create payment link
Source: https://docs.wava.co/api-reference/links/create-payment-link
/api-reference/openapi.yaml post /links
Create a shareable payment link that redirects buyers to a hosted Wava checkout page. The buyer selects their preferred payment method and completes the transaction on the hosted page.
**Standard link**: Include the `amount` field to create a link with a fixed payment amount.
**Dynamic link**: Omit the `amount` field to create a link where the buyer enters the amount before paying. Useful for donations, tips, or variable-price scenarios.
The returned `link` can be shared via email, messaging, social media, or embedded in your website.
**Idempotency**: `order_key` identifies the order on your side and acts as an idempotency key scoped to `(store, order_key)`. Reposting the same `order_key` may return the link that already exists for it instead of creating a new one — see the `order_key` field description. Use a unique `order_key` per buyer.
# Get dashboard stats
Source: https://docs.wava.co/api-reference/merchant-dashboard/get-dashboard-stats
/api-reference/openapi.yaml get /merchant/{storeId}/dashboard/stats
Returns all data needed to render the merchant dashboard in a single call: KPI cards, daily revenue trend, gateway split, and fees vs net breakdown.
**Authentication:** Merchant JWT (`x-auth-token`). The `:storeId` in the path must be included in the JWT's `stores` array — otherwise the endpoint returns 401.
**Timezone:** All date parameters are interpreted in COT (UTC-5). `date_to` is inclusive end-of-day. Dates in the response are COT calendar dates.
**Currency filter:** Strict — a store with both COP and USD wallets that requests `currency=COP` will receive only COP data.
**Zero-days:** `daily_revenue` and `fees_vs_net` include an entry for every day in the range, including days with no transactions (`value: 0`, `fees: 0`, `net_to_merchant: 0`). This ensures chart libraries render a continuous line/bar without gaps.
**Delta calculations:** `delta_revenue_pct` and `delta_orders_pct` compare the requested window against the immediately preceding window of equal length. A 30-day request (Apr 1–Apr 30) compares against the 30 days prior (Mar 2–Mar 31). Returns `null` when the previous window has zero activity (avoids division by zero).
**Gateway split:** Only includes gateways for which fee data has been synced from the ledger (`Order_financials`). `pct` values sum to ~1.0 with floating-point tolerance.
# Cancel an order
Source: https://docs.wava.co/api-reference/orders/cancel-an-order
/api-reference/openapi.yaml post /orders/cancel/{orderId}
Cancel a pending or processing order. Confirmed orders cannot be cancelled — use the refund endpoint instead.
Cancelling an order also cancels any pending payment transactions associated with it.
# Create direct order
Source: https://docs.wava.co/api-reference/orders/create-direct-order
/api-reference/openapi.yaml post /orders
Create and process a payment order directly. The API initiates payment processing immediately based on the selected gateway.
**Nequi and Daviplata orders require the shopper object** with `email`, `phone_number`, `country`, `id_number` and `id_type`; a missing field returns `422`. Use the Get Document Types endpoint to retrieve valid `id_type` values. **For Breb the shopper object is optional** (see below).
**Gateway-specific behavior:**
**Nequi** — Additionally requires `phone_number` in the shopper object. A push notification is sent to the buyer's Nequi app. The buyer approves or rejects in-app. You receive a webhook when the payment is confirmed or cancelled.
**Daviplata** — Uses `id_number` and `id_type` to initiate the OTP flow. Only accepts CC, CE, and TI document types. The buyer receives an OTP via SMS. You must submit the OTP using the Daviplata OTP endpoint to complete payment.
**Breb** — No gateway-specific fields and the `shopper` object is optional: the QR code and transfer key are generated from the order itself, not from the buyer's data. You can omit `shopper` or send only the fields you have; fields you send are validated for format, none is required, and the ones you did not send come back as `null`. The API returns a QR code (base64 encoded) and a transfer key. The buyer uses either to complete the interbank transfer via their banking app. You receive a webhook when the payment is confirmed. The payer's data (name and masked document) arrives after the payment is confirmed and is best-effort: it is the identity reported by the payer's bank, which may differ from the shopper sent on the order. Email and phone number are never guaranteed for Breb payers.
The `id_payment_gateway` value must come from the Get Payment Gateways endpoint. Do not hardcode gateway IDs as they may vary by store configuration.
# Get order details
Source: https://docs.wava.co/api-reference/orders/get-order-details
/api-reference/openapi.yaml get /orders/{orderId}
Retrieve details for a specific order including current status, payment information, and timestamps.
Use this endpoint to:
- Check payment status after creating an order
- Poll for status updates as an alternative to webhooks
- Retrieve order data for reconciliation
# Refund an order
Source: https://docs.wava.co/api-reference/orders/refund-an-order
/api-reference/openapi.yaml post /orders/refund/{orderId}
Process a full refund for a confirmed order. Refunds must be requested within 96 hours of the original purchase. Partial refunds are not currently supported.
# Submit Daviplata OTP
Source: https://docs.wava.co/api-reference/orders/submit-daviplata-otp
/api-reference/openapi.yaml post /orders/daviplata/{orderId}
Complete a Daviplata payment by submitting the OTP that the buyer received via SMS.
Only used for Daviplata orders where the create order response includes `daviplata_token_required: true`.
**Development testing OTPs:**
- `123456` — Always succeeds
- `000000` — Always succeeds
- `111111` — Always succeeds
- Any other code — Returns invalid OTP error
# Create direct order on behalf of a store
Source: https://docs.wava.co/api-reference/partners/create-direct-order-on-behalf-of-a-store
/api-reference/openapi.yaml post /orders#partners
Partners create and process orders for a merchant using that merchant's `merchant-key`, delivered to the partner in the `store_onboarded` webhook. The request body is identical to the standard Create Direct Order endpoint.
The `merchant-key` is what authorizes the call and what decides which store the order belongs to. Partner credentials may be sent alongside it for attribution, but never change the target store; on their own they are rejected with `403`.
All payment gateways available to the merchant can be used.
# Create payment link on behalf of a store
Source: https://docs.wava.co/api-reference/partners/create-payment-link-on-behalf-of-a-store
/api-reference/openapi.yaml post /links#partners
Partners create payment links for a merchant using that merchant's `merchant-key`, delivered to the partner in the `store_onboarded` webhook. The request body is identical to the standard Create Payment Link endpoint.
The `merchant-key` is what authorizes the call and what decides which store the link belongs to. Partner credentials may be sent alongside it for attribution, but never change the target store; on their own they are rejected with `403`.
The partner relationship is resolved from the merchant's integration: webhooks for the resulting link are delivered to the partner's webhook URL.
`order_key` behaves exactly as on the standard endpoint: it is the identifier of the order on the partner's side and an idempotency key scoped to `(store, order_key)`, so a repeated `order_key` may return the existing link instead of creating a new one. Use a unique `order_key` per buyer.
# Disconnect a merchant
Source: https://docs.wava.co/api-reference/partners/disconnect-a-merchant
/api-reference/openapi.yaml post /partners/stores/{storeId}/deactivate
Deactivates the partner's integration on the given merchant's store. The merchant's Wava account is unaffected — only the link to the partner platform is removed. The store stops appearing in `GET /partners/stores` and its events stop reaching the partner's webhook URL.
# Get webhook configuration
Source: https://docs.wava.co/api-reference/partners/get-webhook-configuration
/api-reference/openapi.yaml get /partners/webhook
Returns the partner's webhook URL and subscribed events. The signing key is never returned.
# List connected merchants
Source: https://docs.wava.co/api-reference/partners/list-connected-merchants
/api-reference/openapi.yaml get /partners/stores
Returns every merchant with an active integration with the authenticated partner. This is the authoritative list of who is connected to the partner — use it to reconcile against processed `store_onboarded` webhooks.
Merchant keys are **not** returned here; they are delivered only in the `store_onboarded` webhook.
# List subscribable events
Source: https://docs.wava.co/api-reference/partners/list-subscribable-events
/api-reference/openapi.yaml get /partners/events
Returns the events that `PATCH /partners/webhook` accepts. Order and payment-link events are delivered to partners as well, but are configured by Wava rather than through this endpoint.
# Send a test webhook
Source: https://docs.wava.co/api-reference/partners/send-a-test-webhook
/api-reference/openapi.yaml post /partners/webhook/test
Sends a `test.ping` payload to the partner's configured webhook URL, signed exactly like a real event, and reports the HTTP status and round-trip time observed.
# Update webhook configuration
Source: https://docs.wava.co/api-reference/partners/update-webhook-configuration
/api-reference/openapi.yaml patch /partners/webhook
Updates the partner's webhook URL and/or subscribed events. Both fields are optional — omitting one leaves it unchanged. The change is applied across the partner account and all merchant integrations attached to it.
The URL must use HTTPS. Events must be drawn from `GET /partners/events`.
# Get available payment gateways
Source: https://docs.wava.co/api-reference/payment-gateways/get-available-payment-gateways
/api-reference/openapi.yaml get /orders/paymentGateways
Retrieve the list of payment gateways available for your store. Returns all active gateways with their specific requirements, supported currencies, and display information.
Use this endpoint to:
- Display payment options dynamically in your checkout
- Determine which fields to collect from the buyer based on each gateway's requirements
- Get gateway logos and descriptions for UI display
- Check which gateways support direct API orders vs. hosted checkout only
Amount validation (min/max limits per gateway) is performed during order creation, not during gateway discovery.
# Get document types by country
Source: https://docs.wava.co/api-reference/utilities/get-document-types-by-country
/api-reference/openapi.yaml get /national-document-types/{countryCode}
Retrieve valid national document types for a specific country. Use this to get valid `id_type` values for the shopper object when creating direct API orders.
The returned `id_national_document_type` value should be passed as `id_type` in the shopper object.
# Error Codes
Source: https://docs.wava.co/errors/codes
Complete list of API error codes.
# Error Codes
These are the error codes that can be returned by the Wava API to external consumers. Each error follows the [standard error format](/errors/overview).
## Authentication errors (1000-1099)
| Code | API Code | Message | Description |
| - | - | - | - |
| 1001 | `MISSING_MERCHANT_KEY` | Missing merchant-key header | The `merchant-key` header is required for API authentication |
| 1002 | `INVALID_MERCHANT_KEY` | Invalid merchant key | The provided merchant key is not valid or has been revoked |
| 1003 | `UNAUTHORIZED_ACCESS` | Unauthorized access | You do not have permission to access this resource |
| 1004 | `INSUFFICIENT_SCOPE` | Insufficient scope | You don't have access to this resource |
## Validation errors (2000-2099)
| Code | API Code | Message | Description |
| - | - | - | - |
| 2000 | `VALIDATION_FAILED` | Validation failed | One or more fields failed validation. Check the `validation_errors` array for details |
| 2001 | `REQUIRED_FIELD_MISSING` | Required field missing | One or more required fields are missing from the request |
| 2002 | `INVALID_FIELD_FORMAT` | Invalid field format | One or more fields have an invalid format |
| 2003 | `AMOUNT_REQUIRED` | Amount is required | The `amount` field is required and must be a positive number |
| 2004 | `DESCRIPTION_REQUIRED` | Description is required | The `description` field is required |
| 2005 | `INVALID_CURRENCY` | Invalid currency | Currency must be a valid 3-letter ISO code (e.g., `COP`) |
| 2006 | `INVALID_EMAIL` | Invalid email format | The email address provided is not in a valid format |
| 2007 | `INVALID_PHONE` | Invalid phone number | Phone number must contain only digits |
| 2008 | `INVALID_URL` | Invalid URL format | The URL provided is not in a valid format |
## Order and store errors (3000-3099)
| Code | API Code | Message | Description |
| - | - | - | - |
| 3001 | `STORE_NOT_FOUND` | Store not found | The specified store does not exist |
| 3002 | `STORE_INACTIVE` | Store inactive | The store is not currently active |
| 3003 | `ORDER_NOT_FOUND` | Order not found | The specified order does not exist or you do not have access to it |
| 3004 | `ORDER_ALREADY_PROCESSED` | Order already processed | This order has already been processed and cannot be modified |
| 3005 | `INVALID_ORDER_STATUS` | Invalid order status | The order is not in a valid status for this operation |
| 3010 | `DATA_NOT_FOUND` | Data not found | The requested data could not be found |
| 3017 | `INVALID_SORT_COLUMN` | Invalid sort column | The specified column is not allowed for sorting |
| 3018 | `REFUND_WINDOW_EXPIRED` | Refund window expired | The refund period for this order has expired |
| 3026 | `WALLET_NOT_FOUND` | Wallet not found | The `wallet_id` provided does not belong to this merchant |
| 3037 | `WALLET_NOT_ACTIVE` | Wallet is not active | The `wallet_id` provided is a suspended or closed wallet; only active wallets can receive payments |
| 3038 | `CURRENCY_MISMATCH` | Currency mismatch | The `wallet_id` provided is a wallet in a different currency than the resource it is being bound to |
| 3056 | `ORDER_KEY_CONFLICT` | order\_key already belongs to an order with a different amount or currency | HTTP 409. A direct order with this `order_key` already exists on this store for another amount or currency. The response carries `existing_amount` and `existing_currency`. Use a unique `order_key` per order. |
| 3057 | `INVALID_CHECK_DIGIT` | Invalid check digit | HTTP 422. The check digit (DV) of a Colombian NIT or cédula does not match the number. The response carries `expected_check_digit`. Verify the number against the RUT. |
## Payment errors (4000-4099)
| Code | API Code | Message | Description |
| - | - | - | - |
| 4001 | `PAYMENT_GATEWAY_REQUIRED` | Payment gateway required | A valid payment gateway must be provided |
| 4002 | `PAYMENT_FAILED` | Payment failed | The payment could not be processed |
| 4003 | `INVALID_PAYMENT_AMOUNT` | Invalid payment amount | The payment amount is invalid or does not match the order total |
| 4004 | `PAYMENT_GATEWAY_NOT_SUPPORTED` | Payment gateway not supported | The specified payment gateway is not supported by this store |
| 4005 | `PAYMENT_GATEWAY_VALIDATION` | Payment gateway validation failed | The payment gateway information provided is invalid or incomplete (e.g., missing phone for Nequi) |
## Gateway-specific errors (6000+)
These errors originate from the payment gateway itself. The response will include a `gateway_name` field.
### Nequi errors
| Code | API Code | Description |
| - | - | - |
| 6001 | `NEQUI_PAYMENT_REJECTED` | The buyer rejected the payment in the Nequi app |
| 6006 | `NEQUI_TIMEOUT` | The push notification expired before the buyer could approve it |
| 6007 | `NEQUI_INSUFFICIENT_FUNDS` | The buyer does not have enough balance in their Nequi account |
### Daviplata errors
| Code | API Code | Description |
| - | - | - |
| 6101 | `DAVIPLATA_INVALID_DOCUMENT` | The national ID or document type is not valid for Daviplata |
| 6105 | `DAVIPLATA_OTP_ERROR` | Invalid or expired OTP code |
## Server errors (5000-5099)
| Code | API Code | Message | Description |
| - | - | - | - |
| 5002 | `SERVICE_UNAVAILABLE` | Service temporarily unavailable | The service is temporarily unavailable, please try again later |
| 5003 | `RATE_LIMIT_EXCEEDED` | Rate limit exceeded | Too many requests, please slow down and try again later |
Gateway-specific errors (6000+) depend on the payment gateway and may include additional fields. Always check the `gateway_name` field to determine which gateway returned the error.
# Error Handling
Source: https://docs.wava.co/errors/overview
How to handle API errors.
# Error Handling
All Wava API errors return a consistent JSON structure:
```json theme={null}
{
"code": 4001,
"api_code": "PAYMENT_GATEWAY_REQUIRED",
"message": "Payment gateway is required",
"description": "A payment gateway must be specified",
"error": true
}
```
## Error fields
| Field | Description |
| - | - |
| `code` | Numeric error code |
| `api_code` | Machine-readable error identifier |
| `message` | Short human-readable message |
| `description` | Detailed explanation |
| `error` | Always `true` for error responses |
| `gateway_name` | Present for gateway-specific errors (6000+) |
| `validation_errors` | Array of field-level errors (for validation failures) |
## Validation error example
When validation fails (code `2000` or `4005`), the response includes an array of field-specific errors:
```json theme={null}
{
"code": 2000,
"api_code": "VALIDATION_FAILED",
"message": "Validation failed",
"description": "One or more fields failed validation",
"error": true,
"validation_errors": [
{
"field": "shopper.email",
"message": "Invalid email format",
"value": "not-an-email"
},
{
"field": "amount",
"message": "Amount is required",
"value": null
}
]
}
```
## Gateway error example
When a payment gateway returns an error, the response includes the gateway name:
```json theme={null}
{
"code": 6105,
"api_code": "DAVIPLATA_OTP_ERROR",
"message": "Invalid or expired OTP",
"description": "The OTP code provided is invalid or has expired",
"error": true,
"gateway_name": "daviplata"
}
```
## HTTP status codes
| Status | Meaning |
| - | - |
| `200` | Success |
| `202` | Accepted (refund processing) |
| `400` | Bad request / Validation error |
| `401` | Unauthorized (invalid or missing credentials) |
| `404` | Resource not found |
| `422` | Unprocessable (e.g., refund window expired) |
| `429` | Rate limit exceeded |
| `500` | Server error |
| `503` | Service temporarily unavailable |
## Best practices
* Always check the HTTP status code first.
* Use `api_code` for programmatic error handling (not `message`, which may change or be localized).
* For `400` errors, check the `validation_errors` array for field-specific issues.
* For `500` and `503` errors, retry with exponential backoff.
* For gateway errors (6000+), check `gateway_name` to determine which gateway returned the error and handle accordingly.
# Authentication
Source: https://docs.wava.co/getting-started/authentication
How to authenticate your API requests with Wava.
# Authentication
All Wava API requests require authentication via headers. The type of header depends on your integration role.
## Merchant authentication
Every request must include your store's merchant key:
```bash theme={null}
curl -X GET "https://api.wava.co/v1/orders/paymentGateways" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json"
```
Your merchant key identifies your store and determines which payment gateways are available, what currency is used, and where payouts are sent.
## Partner authentication
Partners operating on behalf of onboarded stores include three headers:
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: STORE_MERCHANT_KEY" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ ... }'
```
* The `merchant-key` identifies the target store
* The `X-API-Key` and `X-API-Secret` authenticate the partner (two-factor authentication)
The operation is associated with both the store and the partner for tracking and reporting.
## Obtaining your credentials
### Create a development account
Go to [https://app.dev.wava.co/](https://app.dev.wava.co/) and use your email to create an account.
Create a test store with test data. This will be your sandbox environment.
Fill the compliance form with test data — there are no required fields in development.
Upon activation, go to the **Settings** section, then open the **Integrations** tab.
Under **API**, you'll find your merchant key for this environment. You can also configure your webhook URL here.
### Dashboard links
| Environment | Dashboard URL | Integrations page |
| - | - | - |
| Development | [app.dev.wava.co](https://app.dev.wava.co) | [app.dev.wava.co/config#integrations](https://app.dev.wava.co/config#integrations) |
| Production | [app.wava.co](https://app.wava.co) | [app.wava.co/config#integrations](https://app.wava.co/config#integrations) |
Your merchant key is available in the [Wava Dashboard](https://app.wava.co) under **Settings > Integrations > API**. A separate key is provided for development and production environments.
Partner credentials (API Key and API Secret) are issued upon approval. Contact [soporte@wava.co](mailto:soporte@wava.co) to apply for partner access. Both the API Key (`X-API-Key`) and API Secret (`X-API-Secret`) are required for authentication.
## Security
Never expose your merchant key, partner API key, or partner API secret in client-side code (browser JavaScript, mobile apps). Always make API calls from your server.
Your keys should be stored as environment variables or in a secrets manager, never committed to source control.
If you believe any credential has been compromised, contact [soporte@wava.co](mailto:soporte@wava.co) immediately to rotate it.
# Environments
Source: https://docs.wava.co/getting-started/environments
Development and production environment configuration.
# Environments
Wava provides separate environments for development and production use.
## Base URLs
| Environment | Base URL | Dashboard | Purpose |
| - | - | - | - |
| Development | `https://api.dev.wava.co/v1` | [app.dev.wava.co](https://app.dev.wava.co) | Testing and integration development |
| Production | `https://api.wava.co/v1` | [app.wava.co](https://app.wava.co) | Live transactions with real money |
## Development environment
The development environment is a sandbox that mirrors production behavior without processing real payments. Use it to build and test your integration before going live.
Key differences from production:
* No real money is moved. All transactions are simulated.
* Test phone numbers and document IDs can be used (see [Test Data](/testing/test-data)).
* Nequi orders auto-confirm when you call `GET /v1/orders/{orderId}` (see [Sandbox](/testing/sandbox) for details).
* Daviplata orders accept test OTP codes.
* Webhooks are sent to your configured development URL.
* Rate limits are more permissive.
You receive a separate `merchant-key` for development when your account is created.
### Getting your development credentials
1. Go to [https://app.dev.wava.co/](https://app.dev.wava.co/) and create an account.
2. Create a test store with test data.
3. Fill the compliance form (no required fields in development).
4. Navigate to **Settings > Integrations > API** to get your merchant key.
Direct link: [https://app.dev.wava.co/config#integrations](https://app.dev.wava.co/config#integrations)
## Production environment
Production processes real payments. Before switching to production:
1. Complete your integration testing in development.
2. Follow the [Go Live](/getting-started/go-live) checklist.
3. Submit your UX implementation for review.
4. Switch your base URL and merchant key to production values.
### Getting your production credentials
1. Register your business at [app.wava.co](https://app.wava.co).
2. Complete your business profile with required documentation.
3. Get your production merchant key from [app.wava.co/config#integrations](https://app.wava.co/config#integrations).
Never use production credentials in development or testing. Use the development environment and its corresponding merchant key.
# Go Live
Source: https://docs.wava.co/getting-started/go-live
Checklist for moving your integration to production.
# Go Live
Before your integration can process live payments, Wava requires activation of your account and approval of your payment flow implementation to ensure the best customer experience.
## Prerequisites
* Completed integration testing in development environment
* Valid business registration in Colombia
* Implementation of at least one payment gateway (Nequi, Daviplata, or Breb)
## Step 1: Set up your account
Register your business at [app.wava.co](https://app.wava.co).
Complete your business profile with required documentation.
## Step 2: Technical requirements
All payment flows tested successfully in the development environment, including success, failure, and cancellation scenarios for each gateway you plan to use.
Your production webhook URL is set up, responding with `200 OK`, and handling all relevant event types. See [Webhooks](/webhooks/overview).
Your integration gracefully handles API errors, network timeouts, and unexpected responses. See [Error Handling](/errors/overview).
Your system uses `order_key` to prevent duplicate orders from retries or network issues.
All API calls are made from your backend. No merchant keys are exposed in client-side code.
## Step 3: UX flow documentation
Before going live, you must submit visual documentation of your payment implementation for approval.
### Required documentation
* **Complete user journey screenshots** from payment initiation to confirmation
* **Each implemented payment gateway** must be documented separately
* **Mobile and desktop views** if applicable
* **Error handling scenarios** (optional but recommended)
### Screenshot or video evidence
* High-quality images showing the complete flow
* Include payment gateway selection, amount display, and confirmation screens
* Show payment gateway logos as implemented
* Demonstrate proper loading states and user feedback
## Step 4: Submit for review
Send your submission via email:
* **To**: [go-live@wava.co](mailto:go-live@wava.co)
* **Subject**: `Production Approval Request - [Your Business Name]`
**Include the following:**
* Store name / business name
* Registered email address
* Website/app URL where integration is implemented (if applicable)
* Payment gateways implemented (Nequi, Daviplata, Breb, or all)
* Screenshots or video evidence
* Expected go-live date
### Review process
* **Initial review**: 2-3 business days
* **Feedback**: If changes are needed, we'll provide specific guidance
* **Approval**: Once approved, your merchant key will be activated for production
* **Go-live support**: Technical support available during your launch
## Step 5: Production activation
Update your base URL from `https://api.dev.wava.co/v1` to `https://api.wava.co/v1`.
Once your account is approved, your production merchant key will be available in the Wava dashboard. Go to **Settings > Integrations > API** to retrieve it.
Replace your development merchant key with your activated production key.
Configure your production webhook endpoint in the dashboard.
Verify everything works correctly with small real payments.
Use the [Wava dashboard](https://app.wava.co) to monitor your first live transactions.
## Support during go-live
* **Technical support**: Available during business hours (8 AM - 6 PM COT, Monday - Friday)
* **Response time**: Less than 2 hours during business hours
* **Emergency contact**: Provided after approval for critical issues
* **Dashboard monitoring**: Real-time transaction monitoring available
* **Integration assistance**: [integrations@wava.co](mailto:integrations@wava.co)
Your integration must be reviewed and approved by the Wava team before going live. Contact [soporte@wava.co](mailto:soporte@wava.co) if you have any questions about the process.
# Introduction
Source: https://docs.wava.co/getting-started/introduction
Wava API enables payment processing through Colombian digital wallets with a single integration.
# Wava API
Wava Technologies is a Colombian fintech company that enables payment processing for e-commerce and digital businesses. We provide a unified API that connects merchants with digital wallets and alternative payment methods.
## What you can do with the Wava API
Process payments through Nequi, Daviplata, Breb, and Stripe with a single integration.
Create shareable payment URLs for invoices, social commerce, and more.
Create orders and links on behalf of your onboarded stores.
Connect Tiendanube, WooCommerce, or Stripe without writing code.
## Supported payment methods
| Gateway | Currency | Requirements | Flow |
| - | - | - | - |
| Nequi | COP | Phone number + National ID | Push notification approval |
| Daviplata | COP | National ID + Document type | OTP verification via SMS |
| Breb | COP | No gateway-specific fields | QR code / Transfer key |
| Stripe | Multiple | Dashboard configuration | Card payment (redirect) |
## Quick start
Create a development account at [app.dev.wava.co](https://app.dev.wava.co) and get your merchant key from **Settings > Integrations > API**. See [Authentication](/getting-started/authentication) for details.
Call `GET /v1/orders/paymentGateways` to see which payment methods are active for your store.
Call `POST /v1/orders` with the buyer's details and selected payment gateway to initiate a payment. Nequi and Daviplata orders require the buyer's national ID (`id_number` and `id_type`) in the shopper object; for Breb the shopper is optional. See [Creating Orders](/orders/creating-orders) for full examples.
Listen for webhook notifications or poll `GET /v1/orders/{orderId}` for status updates.
## Environments
| Environment | Base URL |
| - | - |
| Production | `https://api.wava.co/v1` |
| Development | `https://api.dev.wava.co/v1` |
All API requests require a `merchant-key` header. See [Authentication](/getting-started/authentication) for details.
# API Keys
Source: https://docs.wava.co/help/api-keys
Managing your API credentials.
# API Keys
Your API keys are available in the dashboard under **Settings > Integrations > API**.
## Key types
| Key | Purpose | Dashboard |
| - | - | - |
| Development merchant key | Testing in the sandbox environment | [app.dev.wava.co/config#integrations](https://app.dev.wava.co/config#integrations) |
| Production merchant key | Live payment processing | [app.wava.co/config#integrations](https://app.wava.co/config#integrations) |
## Where to find your keys
1. Log into the appropriate dashboard (dev or production).
2. Go to **Settings > Integrations**.
3. Under **API**, you'll see your merchant key and webhook configuration.
## Webhook configuration
On the same Integrations page, you can configure your webhook URL. Wava will send POST requests to this URL whenever an order status changes.
## Security
* Keep your keys confidential. Do not share them in public repositories, client-side code, or emails.
* If a key is compromised, contact [soporte@wava.co](mailto:soporte@wava.co) immediately to rotate it.
* Use environment variables or a secrets manager to store keys in your application.
* Never expose your merchant key in client-side (browser) code. All API calls must be made from your server.
# Dashboard Overview
Source: https://docs.wava.co/help/dashboard-overview
Getting around the Wava merchant dashboard.
# Dashboard Overview
The Wava merchant dashboard at [app.wava.co](https://app.wava.co) is where you manage your store, view transactions, configure integrations, and access your API keys.
## Main sections
* **Home**: Overview of recent transactions and key metrics.
* **Orders**: Browse, search, and manage all orders.
* **Payouts**: View scheduled and completed payouts to your bank account.
* **Settings**: Configure payment gateways, webhooks, API keys, and integrations.
Detailed dashboard documentation with screenshots coming soon.
# Account Setup
Source: https://docs.wava.co/help/getting-started
Setting up your Wava merchant account.
# Account Setup
To start accepting payments with Wava, you need a merchant account in both development and production environments.
## Development account
Visit [app.dev.wava.co](https://app.dev.wava.co) and create an account with your email.
Create a test store with test data.
Fill the compliance form with test data — there are no required fields in development.
Navigate to **Settings > Integrations > API** to get your development merchant key.
Direct link: [app.dev.wava.co/config#integrations](https://app.dev.wava.co/config#integrations)
## Production account
Visit [app.wava.co](https://app.wava.co) and create an account.
Fill in your business information and required documentation.
Enable the payment gateways you want to offer (Nequi, Daviplata, Breb).
Configure your bank account for payouts.
Navigate to **Settings > Integrations > API** for your production merchant key.
Direct link: [app.wava.co/config#integrations](https://app.wava.co/config#integrations)
Before processing live payments, you must complete the [Go Live](/getting-started/go-live) process. Contact [soporte@wava.co](mailto:soporte@wava.co) for assistance with account setup.
# Managing Orders
Source: https://docs.wava.co/help/managing-orders
View and manage orders in the dashboard.
# Managing Orders
The Orders section in the dashboard lets you browse all transactions, filter by status, search by order ID or buyer information, and perform actions like refunds.
## Order statuses
| Status | Meaning | Next possible status |
| - | - | - |
| `pending` | Order created, payment not yet initiated | `processing`, `cancelled` |
| `processing` | Payment in progress — waiting for buyer action | `confirmed`, `cancelled` |
| `confirmed` | Payment completed successfully | `refunded` |
| `cancelled` | Order was cancelled (rejected, timed out, or cancelled via API) | — |
| `refunded` | Payment was refunded to the buyer | — |
### Status flow
```
pending → processing → confirmed → refunded
→ cancelled
```
## Searching orders
You can search orders by:
* **Order ID** (`id_order`)
* **External reference** (`order_key`)
* **Buyer email or phone**
* **Status**
* **Date range**
## Refunding an order
To refund a confirmed order, select the order in the dashboard and click **Refund**. Refunds are subject to a refund window — if the window has expired, the refund cannot be processed (error `3018: REFUND_WINDOW_EXPIRED`).
Detailed guide with screenshots coming soon.
# Payouts
Source: https://docs.wava.co/help/payouts
How and when you receive your money.
# Payouts
Wava processes payouts to your registered bank account on a regular schedule. This page explains how payouts work.
## Payout schedule
Payouts are processed on **Tuesdays and Fridays**. The payout includes all confirmed transactions since the last payout, minus Wava's processing fees.
## Viewing payouts
In the dashboard, go to **Payouts** to see scheduled and completed payouts, including the breakdown of transactions included in each payout.
Detailed payout documentation coming soon. Contact [soporte@wava.co](mailto:soporte@wava.co) for questions about your payouts.
# Integrations
Source: https://docs.wava.co/integrations/overview
Connect Wava to your e-commerce platform without code.
# E-commerce Integrations
Wava offers pre-built integrations with popular e-commerce platforms. These do not require direct API integration — setup is done through the Wava merchant dashboard.
Native app for Tiendanube stores in Latin America.
Plugin for WordPress-based online stores.
Connect Stripe for card payments through Wava.
All integrations are managed in the [Wava Dashboard](https://app.wava.co) under **Settings > Integrations**.
# Stripe
Source: https://docs.wava.co/integrations/stripe
Accept card payments through Stripe via the Wava platform.
# Stripe Integration
Connect your Stripe account to accept card payments alongside Wava's digital wallet payment methods. Buyers see Stripe as an additional payment option when paying through Wava checkout or payment links.
## How it works
Stripe integration is configured through the Wava merchant dashboard — it is not available through the direct API. Once connected, card payments appear automatically as a payment option in your Wava checkout flow.
## Setup
In the [Wava Dashboard](https://app.wava.co), navigate to **Settings > Integrations > Stripe**.
Click **Connect with Stripe** and follow the Stripe OAuth flow to authorize Wava.
Choose which currencies and payment methods to enable.
Stripe payments are processed through Stripe's infrastructure. Wava handles the checkout experience and order tracking, while Stripe handles the actual card processing and payouts. Detailed setup guide coming soon.
# Tiendanube
Source: https://docs.wava.co/integrations/tiendanube
Set up Wava payments in your Tiendanube store.
# Tiendanube Integration
Connect your Tiendanube store to accept payments through Nequi, Daviplata, Breb, and other Wava payment methods.
## Setup
Find the Wava app in the Tiendanube app store and install it on your store.
Log in with your Wava credentials to link your merchant account.
Select which payment gateways to enable for your Tiendanube checkout.
Place a test order to verify the integration works correctly.
Detailed setup guide with screenshots coming soon. Contact [soporte@wava.co](mailto:soporte@wava.co) if you need assistance.
# WooCommerce
Source: https://docs.wava.co/integrations/woocommerce
Set up Wava payments in your WooCommerce store.
# WooCommerce Integration
Add Wava payment methods to your WordPress/WooCommerce store.
## Setup
Download the Wava WooCommerce plugin from the Wava dashboard or contact support.
Go to **Plugins > Add New > Upload Plugin** in your WordPress admin and upload the plugin file.
Navigate to **WooCommerce > Settings > Payments > Wava** and enter your merchant key.
Select which Wava payment gateways to show at checkout.
Detailed setup guide with screenshots coming soon. Contact [soporte@wava.co](mailto:soporte@wava.co) if you need assistance.
# Dynamic Links
Source: https://docs.wava.co/links/dynamic-links
Payment links where the buyer enters the amount.
# Dynamic Links
A dynamic link is a payment link without a fixed amount. The buyer enters the amount they wish to pay before selecting a payment method.
## When to use dynamic links
* **Donations**: Let supporters choose their contribution amount.
* **Tips or gratuities**: Let customers decide how much to tip.
* **Flexible invoicing**: When the amount varies per transaction.
* **Pay-what-you-want**: For products or services with flexible pricing.
## Creating a dynamic link
Omit the `amount` field from the request:
```bash theme={null}
curl -X POST "https://api.wava.co/v1/links" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Donation to Foundation XYZ",
"currency": "COP",
"redirect_link": "https://mysite.com/thanks"
}'
```
The response includes `expires_at` (`null` when no TTL is set):
```json theme={null}
{
"result": {
"link": "https://checkout.wava.co/link/xyz789abc012",
"hash": "xyz789abc012",
"expires_at": null
}
}
```
When the buyer opens the link, they see an amount input field before proceeding to payment method selection.
The only difference between a regular payment link and a dynamic link is whether you include the `amount` field in the request. Everything else — response format, webhooks, redirects, TTL — works the same way.
## Link expiration
Dynamic links support the same `ttl_minutes` parameter as fixed-amount links. See the [Payment Links overview](/documentation/links/overview) for full details on expiration behavior, including in-flight payment handling.
## Reading link metadata
`GET /v1/links/public/:hash` returns the link's current state, including expiration:
```json theme={null}
{
"result": {
"amount": null,
"currency": "COP",
"description": "Donation to Foundation XYZ",
"collect_shopper_data": false,
"expires_at": "2026-06-18T14:30:00.000Z",
"expired": false
}
}
```
| Field | Description |
| - | - |
| `expires_at` | UTC expiration timestamp, or `null` |
| `expired` | `true` if the link has passed its `expires_at`; `false` otherwise |
If the link is inactive or expired, the endpoint returns `410` with `PAYMENT_LINK_INACTIVE` or `PAYMENT_LINK_EXPIRED` respectively.
## Collecting shopper contact data
By default, dynamic links only ask buyers for the information their payment method requires (phone number for Nequi, national ID for Daviplata, etc.). If you need the buyer's name, email, or phone number to identify the payer in your backoffice, use the `user_data_required` flag (or the legacy `collect_shopper_data` alias):
```bash theme={null}
curl -X POST "https://api.wava.co/v1/links" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Donation to Foundation XYZ",
"currency": "COP",
"redirect_link": "https://foundation.org/thanks",
"user_data_required": true
}'
```
When this flag is `true`, the checkout page shows a contact form (first name, last name, email, phone) before the buyer proceeds to payment method selection. The submitted data is stored with the order.
### Behavior
| `user_data_required` | What the buyer sees |
| - | - |
| `false` | Goes straight to payment method selection |
| `true` | Contact form → payment method selection |
| omitted (dynamic link) | `false` by default |
| omitted (fixed-amount link) | `true` by default |
### Field reference
| Field | Type | Notes |
| - | - | - |
| `user_data_required` | boolean | Works on **any** link type (dynamic or fixed-amount). Explicit value overrides the type default |
| `collect_shopper_data` | boolean | Legacy alias — only meaningful for dynamic links. Prefer `user_data_required` for new integrations |
### Rules
* `user_data_required` is valid on both dynamic and fixed-amount links.
* If omitted: dynamic links default to `false`; fixed-amount links default to `true`.
* The resolved value is returned by `GET /v1/links/public/:hash` as `user_data_required` (boolean), so the checkout never needs to infer the default.
Use `collect_shopper_data` when you need to reconcile payments against a known buyer (e.g., donations, event registrations) and your payment gateway alone doesn't provide enough buyer identification.
# Payment Links
Source: https://docs.wava.co/links/overview
Create shareable payment URLs for your buyers.
# Payment Links
Payment links let you collect payments without building a custom checkout. Create a link, share it via any channel, and the buyer completes payment through the hosted Wava checkout page.
## Use cases
* **Invoicing**: Send a payment link with a fixed amount for an invoice.
* **Social commerce**: Share links via WhatsApp, Instagram, or email.
* **Donations**: Create dynamic links where the buyer enters the amount.
* **Recurring manual payments**: Generate a new link for each billing cycle.
## Creating a payment link
```bash theme={null}
curl -X POST "https://api.wava.co/v1/links" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"description": "Invoice #1234",
"currency": "COP",
"order_key": "invoice-1234",
"redirect_link": "https://mystore.com/success",
"redirect_link_cancel": "https://mystore.com/cancel",
"redirect_link_failure": "https://mystore.com/error",
"ttl_minutes": 1440
}'
```
### Request fields
| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | Yes | Label shown to the buyer on the checkout page |
| `amount` | number | No | Fixed amount in the smallest currency unit. Omit for dynamic links |
| `currency` | string | No | 3-letter ISO code (e.g. `COP`). Defaults to the store's currency |
| `order_key` | string | No | Identifier of the order **on your side**. Acts as an idempotency key — resending it does not reliably create a new link. See [Order key semantics](#order-key-semantics) |
| `redirect_link` | string (URL) | No | Redirect after successful payment |
| `redirect_link_cancel` | string (URL) | No | Redirect when buyer cancels |
| `redirect_link_failure` | string (URL) | No | Redirect on payment failure |
| `ttl_minutes` | integer | No | Minutes until the link expires (1–525600). Omit for no expiration |
| `user_data_required` | boolean | No | Whether the checkout collects shopper contact data (name, email, phone). Omit to use the type default: `true` for fixed-amount links, `false` for dynamic links |
| `wallet_id` | integer | No | Wava balance wallet this link's payments should credit. Only for merchants with multiple wallets enabled — see [Routing payments to a wallet](#routing-payments-to-a-wallet) |
### Response
```json theme={null}
{
"result": {
"link": "https://checkout.wava.co/link/abc123def456",
"hash": "abc123def456",
"expires_at": "2026-06-18T14:30:00.000Z",
"wallet_id": null
}
}
```
`expires_at` is the UTC timestamp when the link expires, or `null` if no `ttl_minutes` was provided.
`wallet_id` echoes the wallet the link was bound to, or `null` when it was not bound to one.
`expires_at` and `wallet_id` are present **when the request actually created a link**. They are not returned when the request was answered with an already-existing link — but that is incidental to how that code path builds its response, not a contract you can rely on to detect reuse. See [Order key semantics](#order-key-semantics).
`hash` is the link identifier embedded in the URL. It is returned to requests authenticated with a `merchant-key`; other callers may get a response without it. Use `link` as the shareable URL.
Share the `link` with your buyer. When they open it, they see the payment amount and can select their preferred payment method (Nequi, Daviplata, Breb, or Stripe if enabled).
## Order key semantics
`order_key` is **the identifier of the order on your side**, not a free-form reference field. One `order_key` = one order = one link = one payment.
Wava treats it as an **idempotency key**, scoped to `(store, order_key)`. Sending the same `order_key` again does **not** reliably create a new link — that is the anti-double-charge guarantee, not a side effect. Design your integration around it.
### When a link already exists for that `(store, order_key)`
Wava looks up the most recently created link of that store carrying that `order_key`, and applies this policy:
| State of the existing link | What `POST /v1/links` does | Response |
| - | - | - |
| The link is `inactive` (the merchant deactivated it) | Returns the existing link **as-is**. It is *not* reactivated and no new link is created — the merchant's decision is respected. | `200` with the existing link. A buyer who opens it gets `410 PAYMENT_LINK_INACTIVE` from checkout. |
| The link already has a **confirmed** (paid) order | Returns the existing link and enables **no new payment**. This is the anti-double-charge block. | `200` with the existing link. |
| Anything else (active link, no confirmed order) | Proceeds normally. | `200` with a link — but see the note below: a byte-identical request body still resolves to the same link. |
### Can you tell reuse from creation?
Not reliably, and you should not build on it.
Both cases answer `200`, and **no field says "reused"**. On `POST /v1/links` the two responses do happen to differ today — the reuse path returns only `link` (plus `hash`, when `hash` is returned at all) and omits `expires_at` and `wallet_id`, which a fresh creation always includes:
```json theme={null}
// Created by this request
{ "result": { "link": "https://checkout.wava.co/link/abc123", "hash": "abc123", "expires_at": null, "wallet_id": null } }
// Existing link returned for a repeated order_key
{ "result": { "link": "https://checkout.wava.co/link/abc123", "hash": "abc123" } }
```
**That difference is incidental, not a contract.** It is a by-product of how the two code paths build their response, it is not part of the documented API, and it can disappear without notice. Note also that both fields are frequently `null` on a genuine creation (no `ttl_minutes`, no wallet binding), so any check would have to test for *presence of the key* rather than for a value — which is exactly the kind of brittle inference you should not ship.
On `POST /links/tiendanube` there is **no difference at all**: reuse and creation return byte-identical bodies (`201 {"redirect_url": "..."}` on integration version `v2`, `{"result": {"link": "..."}}` on `v1`).
Design your integration so it does not need to know. Treat a `200` as "this `order_key` now has exactly one link, and here it is."
A dedicated `409 PAYMENT_LINK_ALREADY_EXISTS` for the reuse cases is planned behind the `LINKS_CONFLICT_409_ENABLED` feature flag. It is **not live today** — do not write code that depends on it.
### Use one `order_key` per buyer
For concurrent buyers, generate a **unique `order_key` per buyer, per order**. Never key it on a product, a SKU, or an event.
The mistake happens most often with a single store selling the same item at the same amount to many people at once — race registrations for the same distance are the canonical case:
```json theme={null}
// Wrong — every runner registering for the 10K sends the same key
{
"description": "10K registration",
"amount": 150000,
"currency": "COP",
"order_key": "race-10k"
}
```
The first runner creates the link. Once their payment is confirmed, every later request for `race-10k` gets **that same link** back instead of a new one — the runners behind them end up paying into an order that is already closed, or cannot pay at all.
```json theme={null}
// Right — one key per registration
{
"description": "10K registration",
"amount": 150000,
"currency": "COP",
"order_key": "race-10k-reg-84213"
}
```
Omitting `order_key` does **not** fix this. Links are stored keyed on the store plus a hash of the request body, so two buyers sending an identical body — same amount, same description, no `order_key` — resolve to the same link. Because `order_key` is part of the hashed body, a unique `order_key` per buyer is what makes each request produce its own link.
### What `order_key` does not guarantee
* **It is not the link's `hash`, and the `hash` cannot be derived from it.** The `hash` is computed from the stored request body as a whole — `order_key` is only one of the fields in it. Treat the `hash` and the returned `link` as opaque: never build, guess, or reconstruct a checkout URL from an `order_key`.
* **It is not enforced as unique by storage.** Uniqueness comes from the creation policy above, applied at request time.
* **It is scoped to a single store.** The same `order_key` under two different merchants refers to two unrelated links.
* **It is optional.** Omit it only for links genuinely meant to be paid by many different buyers (donations, tip jars) — and read the note above about identical bodies before you do.
## Link expiration (TTL)
Use `ttl_minutes` to create time-limited links:
```json theme={null}
{
"description": "Flash sale — 24h only",
"amount": 99000,
"currency": "COP",
"ttl_minutes": 1440
}
```
Once a link passes its `expires_at` timestamp:
* The Wava checkout page returns a `410` error with code `PAYMENT_LINK_EXPIRED`.
* Attempting to place an order on the link is blocked.
* The error response includes redirect URLs (same fields as `PAYMENT_LINK_INACTIVE`) so you can send buyers to a custom expiration page.
**In-flight payments are honored**: if a buyer starts payment before the link expires and the gateway confirms afterward, the payment is accepted. Expiration only blocks new payment attempts.
## Redirect URLs
After the buyer completes (or cancels) the payment, they are redirected to the URL you specified:
| Field | When used |
| - | - |
| `redirect_link` | Successful payment |
| `redirect_link_cancel` | Buyer cancels |
| `redirect_link_failure` | Payment fails |
All redirect fields are optional. If omitted, the buyer stays on the Wava checkout confirmation page.
## Routing payments to a wallet
A wallet is the merchant's balance account inside Wava — where their money sits until it is paid out. (Not to be confused with Nequi or Daviplata, which are also called digital wallets; those are how the *buyer* pays.)
Most merchants have one wallet per currency and can ignore this section entirely. Merchants who have multiple wallets enabled can route a link's payments to a specific one by passing `wallet_id` when creating it:
```json theme={null}
{
"description": "Invoice #1234",
"amount": 50000,
"currency": "COP",
"wallet_id": 4712
}
```
Behaviour:
* **Omitted, or `null`** — payments credit the merchant's default wallet for the link's currency. This is the default and what happens for every merchant with a single wallet.
* **A wallet the merchant does not own (or one that does not exist)** — rejected with `404 WALLET_NOT_FOUND`.
* **A wallet whose currency differs from the link's** — rejected with `400 CURRENCY_MISMATCH`.
* **A wallet that is suspended or closed** — rejected with `422 WALLET_NOT_ACTIVE`. Only active wallets can receive payments.
* **Passed by a merchant without multiple wallets enabled** — ignored silently, and the response echoes `wallet_id: null`. The payment credits their only wallet, so nothing is lost; check the response if you need to know whether the binding took effect.
The binding is durable: once a link is bound, every payment on it credits that wallet. The `link_paid` webhook reports it back as `wallet_id` — see [Webhook Events](/webhooks/events).
## Webhooks and payment links
Payment links created from the Wava merchant dashboard (app.wava.co) do not trigger webhook notifications. Webhooks are only sent for payment links created via the API. If you need webhook notifications for payments, create links programmatically using this endpoint.
## Error codes
| Code | HTTP | Description |
| - | - | - |
| `PAYMENT_LINK_INACTIVE` | 410 | Link was manually deactivated |
| `PAYMENT_LINK_EXPIRED` | 410 | Link passed its `expires_at` timestamp |
| `WALLET_NOT_FOUND` | 404 | `wallet_id` does not belong to this merchant |
| `CURRENCY_MISMATCH` | 400 | `wallet_id` is a wallet in a different currency than the link |
| `WALLET_NOT_ACTIVE` | 422 | `wallet_id` is a suspended or closed wallet |
# Creating Orders
Source: https://docs.wava.co/orders/creating-orders
Create payment orders using the Wava API with any available gateway.
# Creating Orders
Once you have discovered the available gateways via [Gateway Discovery](/orders/gateway-discovery), you can create an order to initiate a payment.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `amount` | number | Yes | Payment amount in the smallest currency unit (COP has no decimals). |
| `description` | string | Yes | Description shown to the buyer during payment. |
| `currency` | string | Yes | Currency code. Currently `COP` for Colombian gateways. |
| `shopper` | object | Yes, except Breb | Buyer data. **Optional for Breb**: see [Breb without shopper](#breb-orders-without-shopper). |
| `shopper.first_name` | string | No | Buyer's first name. |
| `shopper.last_name` | string | No | Buyer's last name. |
| `shopper.email` | string | Yes, except Breb | Buyer's email address. |
| `shopper.phone_number` | string | Yes, except Breb | Buyer's phone number. **Used as the payment identifier for Nequi.** |
| `shopper.country` | string | Yes, except Breb | ISO country code, e.g. `CO`. |
| `shopper.id_number` | string | Yes, except Breb | Buyer's national ID number (cedula). |
| `shopper.id_type` | integer | Yes, except Breb | Document type ID from [Document Types](/orders/document-types) endpoint. e.g. `1` for CC. |
| `payment_gateway.id_payment_gateway` | number | Yes | Gateway ID from [Gateway Discovery](/orders/gateway-discovery). |
| `order_key` | string | No | Your unique reference for this order (idempotency key, scoped to your store). See [Resending an order\_key](#resending-an-order-key). |
| `redirect_link` | string | No | URL to redirect buyer after successful payment. |
| `redirect_link_cancel` | string | No | URL to redirect buyer if payment is cancelled. |
| `redirect_link_failure` | string | No | URL to redirect buyer if payment fails. |
**Nequi and Daviplata require the shopper.** Their direct orders must include `email`, `phone_number`, `country`, `id_number` and `id_type`; omitting any of them returns a `422` validation error.
### Breb orders without shopper
For **Breb**, the `shopper` object is optional: the QR code and transfer key are generated from the order, not from the buyer's data. You can omit `shopper` entirely or send only the fields you have.
* Fields you do send are still validated for format (an invalid `email` returns `422`), but none of them is required.
* Who actually paid is reported by the payer's bank after the transfer is confirmed, and it is best-effort (see [Payer data](/payment-flows/breb#payer-data)).
* In webhooks and when reading the order, shopper fields you did not send arrive as `null` (the whole `shopper` object can be `null`). After confirmation, `first_name` and `last_name` may be filled with the payer's name. Email and phone number are never guaranteed for Breb.
The API also accepts `national_id_number` and `id_national_document_type` as aliases for `id_number` and `id_type` respectively. Both formats are interchangeable.
## Examples
The `id_payment_gateway` value in these examples is a placeholder. Always retrieve the correct ID for each gateway from the [Gateway Discovery](/orders/gateway-discovery) endpoint (`GET /v1/orders/paymentGateways`) — do not hardcode it.
Nequi uses the buyer's `phone_number` to send a push notification. National ID fields are still required for compliance.
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"description": "Premium subscription",
"currency": "COP",
"shopper": {
"first_name": "Juan",
"last_name": "Perez",
"email": "juan@email.com",
"phone_number": "+573001234567",
"country": "CO",
"id_number": "1234567890",
"id_type": 1
},
"payment_gateway": {
"id_payment_gateway": GATEWAY_ID
},
"order_key": "order-12345"
}'
```
**Response:**
```json theme={null}
{
"result": {
"id_order": 12345,
"status": "processing",
"payment_gateway": {
"nequi": {
"phone_number": "****567"
}
}
}
}
```
Daviplata uses the buyer's national ID to initiate the OTP flow. The buyer receives an OTP via SMS.
Daviplata only accepts the following document types: **CC** (Cédula de Ciudadanía), **CE** (Cédula de Extranjería), and **TI** (Tarjeta de Identidad).
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 75000,
"description": "Product purchase",
"currency": "COP",
"shopper": {
"first_name": "Maria",
"last_name": "Gonzalez",
"email": "maria@email.com",
"phone_number": "+573001234567",
"country": "CO",
"id_number": "12345678",
"id_type": 1
},
"payment_gateway": {
"id_payment_gateway": GATEWAY_ID
},
"order_key": "order-67890"
}'
```
**Response:**
```json theme={null}
{
"result": {
"id_order": 12346,
"status": "processing",
"daviplata_token_required": true,
"payment_gateway": {
"daviplata": {
"national_id_number": "****678",
"id_national_document_type": 1,
"id_type": "CC"
}
}
}
}
```
After receiving this response, you must submit the OTP. See [Daviplata flow](/payment-flows/daviplata) for the full OTP submission step.
Breb generates a QR code and transfer key from the order. It has no gateway-specific fields, and the `shopper` object is optional (see [Breb orders without shopper](#breb-orders-without-shopper)).
The payer's data (name and masked document) arrives after the payment is confirmed and is best-effort. Email and phone number are not guaranteed for Breb payers.
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" -H "merchant-key: YOUR_MERCHANT_KEY" -H "Content-Type: application/json" -d '{
"amount": 100000,
"description": "Invoice payment",
"currency": "COP",
"payment_gateway": {
"id_payment_gateway": GATEWAY_ID
},
"order_key": "invoice-11111"
}'
```
**Response:**
```json theme={null}
{
"result": {
"id_order": 12347,
"status": "processing",
"payment_gateway": {
"breb": {
"qr_code": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEA...",
"key": "BREB-TXN-2025-ABCDEF123456"
}
}
}
}
```
Display the QR code and key to the buyer. See [Breb flow](/payment-flows/breb) for display guidelines.
## Document types
The `POST /orders` response includes an `allowed_documents` array with the valid document types for the store's country. Use this field instead of calling `GET /v1/national-document-types/{countryCode}` separately.
```json theme={null}
"allowed_documents": [
{ "id_national_document_type": 1, "country_code": "CO", "type": "CC", "description": "Cédula de Ciudadanía" },
{ "id_national_document_type": 2, "country_code": "CO", "type": "CE", "description": "Cédula de Extranjería" },
{ "id_national_document_type": 3, "country_code": "CO", "type": "TI", "description": "Tarjeta de Identidad" }
]
```
The `id_type` field requires the integer ID (`id_national_document_type`) from this list. The legacy endpoint `GET /v1/national-document-types/{countryCode}` still works but is deprecated.
For Colombia, the most common types are:
| ID | Code | Description | Accepted by |
| - | - | - | - |
| 1 | CC | Cédula de Ciudadanía | All gateways |
| 2 | CE | Cédula de Extranjería | All gateways |
| 3 | TI | Tarjeta de Identidad | All gateways |
**Daviplata** only accepts CC, CE, and TI document types. Sending a different document type will result in a payment error.
## What happens next
After creating the order, the payment flow depends on the selected gateway:
Buyer receives a push notification and approves in the Nequi app.
Buyer receives an OTP via SMS and submits it to confirm.
Buyer completes a transfer using a QR code or transfer key.
Buyer is redirected to complete the card payment.
To track the order result, see [Order Status](/orders/order-status).
## Resending an order\_key
`order_key` identifies **one order** on your store. Use a unique `order_key` per order. Resending it is safe — it is how you retry after a timeout or a network error without creating a second order — and the response depends on the order that already exists for it.
A resend is the same order only if it has the **same `amount` and `currency`**. The rest of the body may change between attempts: a different `payment_gateway` (the buyer switches from Nequi to Daviplata), corrected `shopper` data or a new `description`.
| Existing order with that `order_key` | Response |
| - | - |
| None | A new order is created. |
| Different `amount` or `currency` | **`409 ORDER_KEY_CONFLICT`**. Nothing is created or changed. The body includes `existing_amount` and `existing_currency`. |
| Already paid (`confirmed` or `refunded`) | **`200`** with the existing order, `already_processed: true` and `api_code: "ORDER_ALREADY_PROCESSED"`. **No new payment is started**, and no payment data (push, OTP, QR) is returned. |
| `pending`, `processing` or `expired` | The payment is started again over **the same order**. A payment still in progress is cancelled first. |
| `cancelled` | Error `INVALID_ORDER_STATUS`: a cancelled order cannot be retried. Create a new order with a new `order_key`. |
```json Already paid theme={null}
{
"status": "success",
"result": {
"id_order": 159537,
"id_external": "order-12345",
"purchase_date": "2026-09-01 10:00:00",
"status": "confirmed",
"total_price": "50000.00",
"currency": "COP",
"wava_fee": null,
"net_to_merchant": null,
"already_processed": true,
"api_code": "ORDER_ALREADY_PROCESSED",
"message": "Order already processed",
"isDynamicLink": false
}
}
```
Do not reuse an `order_key` for a different payment — for example, several payments against the same invoice. A resend with another amount answers `409`, and one with the same amount resolves to the existing order. Use a distinct `order_key` per payment.
## Partners
If you are a partner creating orders on behalf of a store, the request body is identical. The only difference is the authentication headers. See [Partners — Creating Orders](/partners/creating-orders) for details.
# Document Types
Source: https://docs.wava.co/orders/document-types
Retrieve valid national document types for buyer identification.
# Document Types
**Deprecated.** This endpoint remains functional for backwards compatibility but is no longer the recommended approach. The `POST /orders` response now includes an `allowed_documents` field with the same data. See [Creating Orders](/orders/creating-orders) for details.
Nequi and Daviplata direct orders require a national ID number (`id_number`) and document type (`id_type`) for buyer identification and compliance (for Breb the shopper is optional). Use this endpoint to retrieve the valid document types for a given country.
## Request
```bash theme={null}
curl -X GET "https://api.wava.co/v1/national-document-types/CO" \
-H "merchant-key: YOUR_MERCHANT_KEY"
```
## Response
```json theme={null}
{
"result": [
{
"id_national_document_type": 1,
"country_code": "CO",
"type": "CC",
"description": "Cédula de Ciudadanía"
},
{
"id_national_document_type": 2,
"country_code": "CO",
"type": "CE",
"description": "Cédula de Extranjería"
},
{
"id_national_document_type": 3,
"country_code": "CO",
"type": "TI",
"description": "Tarjeta de Identidad"
}
]
}
```
## How to use
Pass the `id_national_document_type` value as the `id_type` field in the shopper object when [creating orders](/orders/creating-orders):
```json theme={null}
"shopper": {
"id_number": "12345678",
"id_type": 1
}
```
The `id_type` field currently accepts the integer ID from this endpoint. The API also accepts `id_national_document_type` as an alias for `id_type`.
## Gateway-specific restrictions
Not all gateways accept all document types. When creating an order, ensure the document type is supported by the selected gateway:
| Gateway | Accepted document types |
| - | - |
| Nequi | All Colombian document types (CC, CE, TI, etc.) |
| Daviplata | CC, CE, TI only |
| Breb | No gateway-specific restriction (Breb does not use the shopper's document to generate the QR code or key) |
Sending an unsupported document type for Daviplata will result in a payment error from the gateway.
## Supported countries
| Country code | Country |
| - | - |
| `CO` | Colombia |
Additional countries will be added as Wava expands across Latin America.
# Gateway Discovery
Source: https://docs.wava.co/orders/gateway-discovery
Discover which payment gateways are available for your store before creating orders.
# Gateway Discovery
Before creating any order, you need to know which payment gateways are active for your store and what fields each gateway requires from the buyer.
## Why this step matters
Each store has different payment gateways enabled depending on their configuration. The gateway discovery endpoint returns the exact list of available gateways, their IDs, and the required fields — so your integration adapts automatically without hardcoding gateway details.
## Request
```bash theme={null}
curl -X GET "https://api.wava.co/v1/orders/paymentGateways" \
-H "merchant-key: YOUR_MERCHANT_KEY"
```
## Response
The response includes an array of available gateways with their configuration:
```json theme={null}
{
"result": {
"store_currency": "COP",
"accepted_currencies": ["COP"],
"payment_gateways": [
{
"id_payment_gateway": 1,
"gateway_name": "Nequi",
"allowed_currencies": ["COP"],
"requires_phone": true,
"required_shopper_national_id": false,
"description": "Paga con tu celular usando Nequi",
"gateway_logo": "https://cdn.wava.co/logos/nequi.png",
"supports_direct_api": true
},
{
"id_payment_gateway": 2,
"gateway_name": "Daviplata",
"allowed_currencies": ["COP"],
"requires_phone": false,
"required_shopper_national_id": true,
"description": "Paga con Daviplata usando tu cédula",
"gateway_logo": "https://cdn.wava.co/logos/daviplata.png",
"supports_direct_api": true
},
{
"id_payment_gateway": 9,
"gateway_name": "Breb",
"allowed_currencies": ["COP"],
"requires_phone": false,
"required_shopper_national_id": false,
"description": "Transferencia interbancaria con QR o clave",
"gateway_logo": "https://cdn.wava.co/logos/breb.png",
"supports_direct_api": true
}
]
}
}
```
| Field | Meaning |
| - | - |
| `requires_phone` | The gateway uses `shopper.phone_number` to start the payment (Nequi push notification). |
| `required_shopper_national_id` | The gateway uses the shopper's document (`id_number` + `id_type`) to start the payment (Daviplata). |
| `supports_direct_api` | The gateway can be used with `POST /v1/orders` (Direct Orders). If `false`, it is only available through payment links or hosted checkout. |
## How to use the response
Make a `GET /v1/orders/paymentGateways` request with your `merchant-key` header.
Show the buyer only the gateways returned in the response. Use the provided `gateway_logo` URLs for gateway logos. For Direct Orders, only offer gateways with `supports_direct_api: true`.
Nequi and Daviplata direct orders need the shopper fields described in [Creating Orders](/orders/creating-orders); for Breb the `shopper` object is optional. On top of that, use the flags to know what the selected gateway uses: `requires_phone` (Nequi) or `required_shopper_national_id` (Daviplata).
Pass the `id_payment_gateway` and collected fields to `POST /v1/orders`. See [Creating Orders](/orders/creating-orders) for the full request format.
## Gateway-specific required fields
| Gateway | `id_payment_gateway` | Fields the gateway uses | Direct Orders |
| - | - | - | - |
| Nequi | Returned by endpoint | `phone_number` | Yes |
| Daviplata | Returned by endpoint | `id_number`, `id_type` | Yes |
| Breb | Returned by endpoint | None (QR code / key generated from the order; `shopper` is optional) | Yes |
| Stripe | Returned by endpoint | Configured via dashboard | No |
Do not hardcode `id_payment_gateway` values. Always use this endpoint to discover them dynamically — IDs may differ between environments and stores.
For gateway-specific payment flows (push notifications, OTP, QR codes), see [Payment Flows](/payment-flows/overview).
# Order Status
Source: https://docs.wava.co/orders/order-status
Track order status through its lifecycle using webhooks or polling.
# Order Status
After creating an order, you can track its progress through the payment lifecycle.
## Order lifecycle
All orders follow the same status progression regardless of the payment gateway:
```mermaid theme={null}
stateDiagram-v2
[*] --> pending: Order created
pending --> processing: Payment initiated
processing --> confirmed: Payment successful
processing --> cancelled: Rejected / Timeout
confirmed --> refunded: Refund processed
```
| Status | Description |
| - | - |
| `pending` | Order created but payment has not been initiated with the gateway yet. |
| `processing` | Payment is in progress — waiting for buyer action (push approval, OTP, transfer). |
| `confirmed` | Payment completed successfully. Funds will be settled to your wallet. Settlement timelines average 36 hours but vary by gateway and are dynamic. |
| `cancelled` | Order was cancelled — buyer rejected the payment, it timed out, or it was cancelled via API. |
| `refunded` | A refund has been processed for this order. |
## Checking order status
You have two options to track when a payment completes:
### Option 1: Webhooks (recommended)
Configure a webhook URL in your dashboard to receive real-time notifications when an order status changes. This is the most efficient approach — no polling required.
```json theme={null}
{
"event": "order_payment",
"timestamp": "2026-06-24T02:05:12.952Z",
"id_order": 12345,
"id_external": "order-12345",
"status": "confirmed",
"total_price": 50000,
"currency": "COP"
}
```
Payloads are a **flat JSON object** — every field is at the top level, with no nested `data` wrapper. The order total is `total_price` (not `amount`). The example above is abridged; see [Webhook Events](/webhooks/events) for the full `order_payment` payload and [Webhooks](/webhooks/overview) for setup instructions.
### Option 2: Polling
If you cannot receive webhooks, poll the order status endpoint:
```bash theme={null}
curl -X GET "https://api.wava.co/v1/orders/12345" \
-H "merchant-key: YOUR_MERCHANT_KEY"
```
Polling should be used as a fallback only. We recommend a polling interval of 3–5 seconds. Excessive polling may be rate-limited.
## Cancelling an order
You can cancel an order that is still in `pending` or `processing` status:
```bash theme={null}
curl -X PUT "https://api.wava.co/v1/orders/12345/cancel" \
-H "merchant-key: YOUR_MERCHANT_KEY"
```
Orders in `confirmed` status cannot be cancelled. To reverse a confirmed payment, use the refund flow.
## Timeout behavior
Each gateway has a different timeout window. If the buyer does not complete the payment within the timeout period, the order is automatically moved to `cancelled` status.
| Gateway | Approximate timeout |
| - | - |
| Nequi | \~5 minutes |
| Daviplata | \~5 minutes |
| Breb | \~15 minutes |
| Stripe | Session-based |
## Settlement timelines
Once an order reaches `confirmed` status, funds are settled to your Wava wallet. Settlement timelines average **36 hours** but vary by gateway and are subject to change. Exact per-gateway timelines will be visible in the Wava dashboard in an upcoming release.
## Next steps
* Set up [Webhooks](/webhooks/overview) for real-time notifications
* Review [Error Handling](/errors/overview) for failed payment scenarios
* See [Testing](/testing/sandbox) to simulate different order outcomes
# Partner Authentication
Source: https://docs.wava.co/partners/authentication
Which credentials authorize which partner request.
# Partner Authentication
Partners hold two different kinds of credentials, and they are **not** interchangeable:
| Credential | What it is | What it authorizes |
| - | - | - |
| `X-API-Key` + `X-API-Secret` | Your partner platform credentials | The `/v1/partners/*` endpoints — your merchant list, your webhook config, your analytics |
| `merchant-key` | A per-merchant key delivered to you in `store_onboarded` | Payment operations for **that** merchant — orders, links, order status |
The rule: **partner credentials manage your platform; merchant keys move money.**
## Partner platform requests
Send both partner headers, and nothing else:
```bash theme={null}
curl "https://api.wava.co/v1/partners/stores" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
Wava validates that the key/secret pair belongs to an active integration and that the store behind it is flagged as a partner. The store the credentials resolve to is your **own** partner store — that is what scopes the response to your merchants.
Failure modes:
| Response | Cause |
| - | - |
| `401` `INVALID_API_CREDENTIALS` | Key/secret pair not recognized |
| `403` `NOT_A_PARTNER` | The account behind the credentials is not a partner platform |
| `500` `PARTNER_NAME_NOT_FOUND` | Partner account exists but has no partner name configured — contact support |
## Requests on behalf of a merchant
Order and payment-link endpoints are authorized by the merchant's key. Send `merchant-key` alone:
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: MERCHANT_KEY_FROM_STORE_ONBOARDED" \
-H "Content-Type: application/json" \
-d '{ ... }'
```
Wava resolves the store from the merchant key, and the resulting order belongs to that merchant. Your partner relationship is what routes the order's webhooks back to you — it does not need to be re-asserted on the request.
You may also send your `X-API-Key` and `X-API-Secret` alongside the merchant key. They are treated as **attribution only** — they identify the request as coming from your platform and never change which store the operation lands on. The merchant key alone decides that.
Partner credentials on their own are not enough on these endpoints. A request carrying `X-API-Key`/`X-API-Secret` without a valid `merchant-key` is rejected with `403 UNAUTHORIZED_ACCESS` — the partner credentials never select a store for you.
## Which headers go where
| Endpoint | `X-API-Key` + `X-API-Secret` | `merchant-key` |
| - | - | - |
| `GET /v1/partners/stores` | Required | Do not send |
| `POST /v1/partners/stores/{storeId}/deactivate` | Required | Do not send |
| `GET` / `PATCH /v1/partners/webhook` | Required | Do not send |
| `POST /v1/partners/webhook/test` | Required | Do not send |
| `GET /v1/partners/events` | Required | Do not send |
| `GET /v1/partners/analytics/*` | Required | Do not send |
| `POST /v1/orders` | Optional (attribution) | Required |
| `POST /v1/links` | Optional (attribution) | Required |
| `GET /v1/orders/{orderId}` | Optional (attribution) | Required |
On the `/v1/partners/*` endpoints the partner credentials are what authorize the call, and no merchant key is involved. On the payment endpoints it is the reverse: the merchant key authorizes, and the partner credentials are optional metadata.
## Handling credentials
* Both partner values are issued once, when Wava creates your partner account, and cannot be retrieved afterwards.
* Keep them server-side. They authorize access to every merchant connected to your platform.
* Merchant keys are per-merchant secrets. Store them encrypted, scoped to the merchant they belong to, and never expose one merchant's key to another merchant's session.
* To rotate any of these, contact [soporte@wava.co](mailto:soporte@wava.co).
# Creating Links
Source: https://docs.wava.co/partners/creating-links
Create payment links for the merchants connected to your platform.
# Creating Payment Links as a Partner
Payment links work the same way orders do: authenticate with the merchant's `merchant_key` from the [`store_onboarded`](/partners/onboarding) webhook and call the standard `POST /v1/links` endpoint.
The merchant key is what authorizes this call and what decides which store the link belongs to. You may add your `X-API-Key`/`X-API-Secret` for attribution, but they never change the target store. See [Partner Authentication](/partners/authentication).
## Example
```bash theme={null}
curl -X POST "https://api.wava.co/v1/links" \
-H "merchant-key: MERCHANT_KEY_FROM_STORE_ONBOARDED" \
-H "Content-Type: application/json" \
-d '{
"amount": 80000,
"description": "Monthly service fee",
"currency": "COP",
"order_key": "partner-invoice-001"
}'
```
The response and behavior are identical to standard payment links. See [Payment Links](/links/overview) for fixed vs. dynamic links and redirect configuration.
## `order_key` is the order identifier, not a free reference
This is the field partners most often get wrong, so it is worth stating here as well as in [Order key semantics](/links/overview#order-key-semantics).
`order_key` identifies **the order on your platform**: one `order_key` = one order = one link = one payment. It is an idempotency key scoped to `(store, order_key)`, and resending it does **not** reliably create a new link — that is the anti-double-charge guarantee.
When a link already exists for that `(store, order_key)`:
| State of the existing link | What you get back |
| - | - |
| The link is `inactive` (the merchant deactivated it) | `200` with **that same link**, not reactivated and not replaced. Buyers who open it get `410 PAYMENT_LINK_INACTIVE`. |
| The link already has a **confirmed** (paid) order | `200` with **that same link**, and no new payment is enabled. |
No field in the response says "reused", and you should not try to infer it. On `POST /v1/links` the reuse path today happens to omit `expires_at` and `wallet_id`, which a fresh creation includes — but that is an incidental by-product of the code path, not a documented contract, and both fields are often `null` on a genuine creation anyway. On `POST /links/tiendanube` the two responses are byte-identical and there is no difference to detect at all. Build your integration so it does not need to know: a `200` means this `order_key` now has exactly one link. An explicit `409 PAYMENT_LINK_ALREADY_EXISTS` is planned behind the `LINKS_CONFLICT_409_ENABLED` flag and is **not live today** — do not depend on it.
Generate a **unique `order_key` per buyer**. Keying it on a product, a plan, or an event breaks as soon as two buyers of the same merchant pay the same amount at the same time — for example, a race sending `"order_key": "race-10k"` for every registration to the 10K distance. The first registration wins the link; once it is paid, every later request gets that same link back. Use `"race-10k-reg-84213"` instead: one key per registration.
Dropping `order_key` altogether is not a workaround. Links are keyed on the store plus a hash of the request body, so identical bodies collapse onto the same link.
`order_key` is **not** the link's `hash`, and the `hash` cannot be derived from it — the `hash` comes from the whole request body. Never construct a checkout URL from an `order_key`; use the `link` the API returns.
Links you create through the API have `source: "api"`. When one is paid, you receive `order_payment` **and** `link_paid` on your partner webhook URL. Links a merchant creates from the Wava dashboard have `source: "wava"` and emit no partner webhooks.
# Creating Orders
Source: https://docs.wava.co/partners/creating-orders
Create orders for the merchants connected to your platform.
# Creating Orders as a Partner
Once a merchant has connected to your platform you hold their `merchant_key`, delivered in the [`store_onboarded`](/partners/onboarding) webhook. That key is all you need: the request is byte-for-byte the same as a [standard order creation](/orders/creating-orders).
The merchant key is what authorizes this call and what decides which store the order belongs to. You may add your `X-API-Key`/`X-API-Secret` for attribution, but they never change the target store. See [Partner Authentication](/partners/authentication).
## Example
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: MERCHANT_KEY_FROM_STORE_ONBOARDED" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"description": "Product purchase",
"currency": "COP",
"shopper": {
"first_name": "Juan",
"last_name": "Perez",
"email": "juan@email.com",
"phone_number": "+573001234567",
"country": "CO"
},
"payment_gateway": {
"id_payment_gateway": 1
},
"order_key": "partner-order-001"
}'
```
The response is identical to a direct merchant order, and all payment flows (Nequi, Daviplata, Breb) behave identically.
## Tracking the order back to your system
* `order_key` is echoed back to you as `id_external` in webhooks. Use it to match the order to your own record.
* Available payment gateways differ per merchant. Call [gateway discovery](/orders/gateway-discovery) with that merchant's key rather than hardcoding `id_payment_gateway`.
* When the order is paid, `order_payment` is delivered to **your** partner webhook URL, not the merchant's. See [Partner Webhooks](/partners/webhooks).
For the full request body reference see [Creating Orders](/orders/creating-orders); for gateway-specific flows see [Payment Flows](/payment-flows/overview).
# Managing Merchants
Source: https://docs.wava.co/partners/managing-merchants
List, inspect and disconnect the merchants connected to your platform.
# Managing Merchants
These endpoints operate on your partner account as a whole. They are authenticated with `X-API-Key` + `X-API-Secret` only — no merchant key. See [Partner Authentication](/partners/authentication).
## List connected merchants
```bash theme={null}
curl "https://api.wava.co/v1/partners/stores" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
```json theme={null}
{
"stores": [
{
"id_store": 2841,
"store_name": "Café Bogotá",
"store_url": "https://cafebogota.co",
"country": "Colombia",
"currency": "COP",
"status": "active",
"integration_id": 5120,
"integration_status": "active",
"connected_at": "2026-08-06T14:22:30.000Z"
}
],
"total": 1
}
```
Only merchants with an **active** integration with your platform are returned. This is the authoritative answer to "who is connected to me" — use it to reconcile against the `store_onboarded` webhooks you have processed.
Merchant keys are not included here. They are delivered only in the `store_onboarded` webhook — see [Onboarding Merchants](/partners/onboarding).
## Disconnect a merchant
```bash theme={null}
curl -X POST "https://api.wava.co/v1/partners/stores/2841/deactivate" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
```json theme={null}
{
"success": true,
"message": "Integration deactivated for store 2841"
}
```
This deactivates your integration on that merchant's store. The merchant's Wava account is unaffected — only the link to your platform is removed. The store disappears from `GET /v1/partners/stores` and its events stop reaching your webhook URL.
| Response | Cause |
| - | - |
| `400` `INVALID_STORE_ID` | The path segment is not a positive integer |
| `404` `STORE_NOT_FOUND` | That store is not connected to your platform |
| `404` `INTEGRATION_NOT_FOUND` | The store is connected but has no active integration left to deactivate |
## Analytics
All analytics endpoints are scoped to the merchants connected to your platform. Every one of them accepts:
| Query param | Default | Notes |
| - | - | - |
| `date_from` | 30 days ago | `YYYY-MM-DD` |
| `date_to` | today | `YYYY-MM-DD`, must be on or after `date_from` |
| `currency` | `COP` | ISO 4217, exactly 3 characters |
| `timezone` | `-05:00` | Format `±HH:MM` |
| Endpoint | Returns |
| - | - |
| `GET /v1/partners/analytics/merchants` | Your merchants with their activity summary |
| `GET /v1/partners/analytics/summary` | Aggregated totals across all your merchants for the period |
| `GET /v1/partners/analytics/daily` | Day-by-day breakdown for the period |
| `GET /v1/partners/analytics/by-store` | Per-merchant performance. Extra param: `limit` |
| `GET /v1/partners/analytics/by-gateway` | Performance split by payment gateway, with gateway names resolved |
| `GET /v1/partners/analytics/orders` | Order list across your merchants. Extra params: `limit`, `offset`, `status` (`pending`, `confirmed`, `cancelled`, `refunded`), `gateway_id`, `store_id` |
| `GET /v1/partners/analytics/conversion-times` | Time-to-payment distribution |
```bash theme={null}
curl "https://api.wava.co/v1/partners/analytics/orders?date_from=2026-07-01&date_to=2026-07-31&status=confirmed&store_id=2841&limit=50" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
`store_id` narrows results to a single merchant on `/analytics/orders` only. The other analytics endpoints always aggregate across every merchant connected to you.
Invalid query parameters return `400` with a validation error listing the offending fields.
# Onboarding Merchants
Source: https://docs.wava.co/partners/onboarding
How merchants reach Wava through your link and how you receive them.
# Onboarding Merchants
This page walks the complete path a merchant takes from your platform into Wava, and what you receive at each point. It is the flow behind the `store_onboarded` webhook.
## 1. Your onboarding link
Wava issues you an onboarding link when your partner account is created, together with your production credentials. It is the Wava signup page carrying your partner name in the `platform` query parameter:
```
https://app.wava.co/signup?platform=YourPartnerName
```
`YourPartnerName` is the partner name registered on your account — the same name Wava uses to resolve your integration. It is fixed at account creation; you do not choose it per merchant.
This is the same mechanism the Tiendanube and WooCommerce integrations use, and it is what drives the automatic connection in step 2. There is nothing to configure on your side beyond putting the link in front of your merchants — in your onboarding emails, your dashboard, your setup wizard.
The link is per-partner, not per-merchant. You do not generate a new link per merchant and you do not pre-register merchants with Wava.
### Passing your own merchant ID
Add `eid` to carry your internal identifier for that merchant:
```
https://app.wava.co/signup?platform=YourPartnerName&eid=your-internal-merchant-id
```
It is stored on the Wava store as `external_store_id` and comes back to you as `external_store_id` in every webhook, store and order events alike, so you can match events to your own records without keeping a lookup table.
Generate the link per merchant from your own system if you want this. It is optional: without it the field arrives as `null` and you match on `id_store`, which is always present.
`eid` is a single reference string (up to 80 characters), not an arbitrary metadata object. Use it for your own merchant or account ID.
## 2. The merchant signs up — the integration is automatic
The merchant follows the link, creates their Wava account and completes onboarding (business details, documents, payout account).
As soon as onboarding finishes and the store becomes active, Wava reads the `platform` value carried from the link, resolves it to your partner account, and **creates the integration automatically**. The merchant does not have to find your platform in a settings screen.
The integration created on the merchant's store is:
* `platform`: your partner name (e.g. `Mercately`)
* `platform_type`: `partner`
* `allowed_scopes`: copied from your partner account, so the merchant is operated with exactly the permissions you were approved for
* `external_store_id`: the `eid` from the link, if you passed one
Under the hood this is a single call made on the merchant's behalf once their store leaves `draft`:
```http theme={null}
POST /v1/stores/{storeId}/integrations
x-auth-token:
Content-Type: application/json
{
"platform": "YourPartnerName",
"external_store_id": "your-internal-merchant-id"
}
```
Your `X-API-Key`/`X-API-Secret` are not involved here and cannot be used to connect a merchant to you. The link is the mechanism.
### Re-subscribing
A merchant can go through your link again — after you or they disconnected, or simply to refresh the connection. Every time they do, the integration is brought back to active, the `eid` on the link is written to the store, and you receive `store_onboarded` again.
`store_onboarded` is therefore **not** a once-per-merchant event. Key your handler on `id_store` and make it idempotent: treat a repeat as "this merchant is (still) connected, here is the current merchant key" rather than as a new signup.
## 3. You receive `store_onboarded`
The moment the integration is created, Wava POSTs `store_onboarded` to the webhook URL configured on your partner account. This is the event that hands you a merchant you can start charging for.
```json theme={null}
{
"event": "store_onboarded",
"timestamp": "2026-08-06T14:22:31.004Z",
"id_store": 2841,
"external_store_id": "your-internal-merchant-id",
"store_name": "Café Bogotá",
"store_url": "https://cafebogota.co",
"status": "active",
"merchant_key": "mk_live_9f2c7a1e4b8d6350a1c9",
"country": "Colombia",
"currency": "COP",
"referred_by": null
}
```
| Field | Meaning |
| - | - |
| `id_store` | Wava's ID for this merchant. Use it on `/v1/partners/*` endpoints. |
| `external_store_id` | The `eid` you put on the onboarding link. `null` if you did not pass one. |
| `store_name` / `store_url` | The merchant's business name and site. |
| `status` | Store status at the time of the event. |
| `merchant_key` | **The credential you store.** It authorizes order and link creation for this merchant. |
| `country` | Country name of the store (e.g. `Colombia`). |
| `currency` | The store's default currency (ISO 4217). |
| `referred_by` | Store ID from Wava's separate store-referral program, unrelated to the partner link. Normally `null` for merchants onboarded through your `platform` link. Your relationship with the merchant is established by the integration, not by this field. |
Request headers on that POST:
```
Content-Type: application/json
X-Wava-Event: store_onboarded
X-Wava-Timestamp: 2026-08-06T14:22:31.004Z
X-Wava-Signature: 6f1a9c... # present when signing is enabled
User-Agent: Wava-Webhooks/1.0
```
Verify the signature before trusting the payload — see [Webhook Security](/webhooks/security).
`store_onboarded` is only delivered if `store_onboarded` is present in your subscribed `events` list. Check it with `GET /v1/partners/webhook`. If your webhook URL is not configured, or the event is not subscribed, the merchant is still connected to you — you simply never hear about it. Reconcile with `GET /v1/partners/stores`.
## 4. You store the merchant key and start processing
Persist `id_store` and `merchant_key` against your own merchant record. From then on:
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: mk_live_9f2c7a1e4b8d6350a1c9" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "COP",
"description": "Product purchase",
"order_key": "your-order-001"
}'
```
See [Creating Orders](/partners/creating-orders) and [Creating Links](/partners/creating-links) for the full request bodies, and [Partner Authentication](/partners/authentication) for the header rules.
## 5. Payment events come back to you
Once an order for that merchant is paid, Wava sends `order_payment` to **your** webhook URL — the same URL that received `store_onboarded` — because the merchant's store carries your partner integration. Payment-link orders created via the API also emit `link_paid`.
You receive these for every merchant connected to you. Use `id_store` (or `external_store_id`) in the payload to route the event to the right merchant on your side.
See [Partner Webhooks](/partners/webhooks) for the payloads.
## 6. Disconnecting
Either side can end the relationship:
* **You:** `POST /v1/partners/stores/{storeId}/deactivate` deactivates your integration on that merchant's store.
* **The merchant:** deactivating your integration from their dashboard.
After deactivation the store stops appearing in `GET /v1/partners/stores`, and its events stop reaching your webhook URL. The merchant's Wava account and its merchant key continue to exist — they are simply no longer yours to operate.
The relationship can be re-established at any time by sending the merchant through your onboarding link again, which brings the integration back and re-delivers `store_onboarded`.
## Reconciling
`store_onboarded` is a notification, not a ledger. Treat `GET /v1/partners/stores` as the source of truth for who is connected to you, and reconcile against it periodically to catch merchants who connected while your endpoint was down.
`GET /v1/partners/stores` does **not** return merchant keys — `store_onboarded` is the only place a merchant key is delivered to you, so persist it when the webhook arrives. If you lose it, sending the merchant through your onboarding link again re-delivers it; otherwise contact [soporte@wava.co](mailto:soporte@wava.co).
# Partners API
Source: https://docs.wava.co/partners/overview
Onboard merchants onto Wava, receive them by webhook, and operate on their behalf.
# Partners API
A **partner** is a platform that brings its own merchants to Wava — a POS, a CRM, a marketplace, a vertical SaaS. Instead of every merchant integrating Wava separately, the partner integrates once and each merchant onboarded through the partner's link becomes a Wava store automatically linked to the partner.
Wava tells the partner about each of those merchants through the `store_onboarded` webhook, which carries the merchant's `merchant_key`. That key is what the partner uses to process payments for that merchant.
## The lifecycle at a glance
We register your partner name and issue your `X-API-Key`, `X-API-Secret`, webhook signing key and onboarding link.
The link carries your partner name: `https://app.wava.co/signup?platform=YourPartnerName`.
When the merchant finishes onboarding, Wava resolves the `platform` value from the link and creates a partner integration on their store pointing back at you — no manual step for the merchant.
Wava POSTs to your webhook URL with the new store's `id_store`, `store_name`, country, currency and — most importantly — its `merchant_key`.
Store the `merchant_key` and use it to create orders and payment links for that merchant. Payments emit `order_payment` to your webhook URL.
Full walkthrough with payloads: [Onboarding Merchants](/partners/onboarding).
## What you get as a partner
| Credential | Header | Used for |
| - | - | - |
| API Key | `X-API-Key` | Identifies your partner platform |
| API Secret | `X-API-Secret` | Authenticates your platform |
| Signing key | — | Verifying the `X-Wava-Signature` on webhooks we send you |
| Onboarding link | — | `https://app.wava.co/signup?platform=YourPartnerName` — share with merchants so they connect to you automatically |
The API secret and signing key are shown **once**, when your partner account is created. Store them securely — they cannot be retrieved again.
## Capabilities
### Partner platform endpoints
Authenticated with `X-API-Key` + `X-API-Secret` only. See [Managing Merchants](/partners/managing-merchants).
| Action | Endpoint |
| - | - |
| List your connected merchants | `GET /v1/partners/stores` |
| Disconnect a merchant | `POST /v1/partners/stores/{storeId}/deactivate` |
| Read your webhook config | `GET /v1/partners/webhook` |
| Update your webhook config | `PATCH /v1/partners/webhook` |
| Send a test webhook | `POST /v1/partners/webhook/test` |
| List subscribable events | `GET /v1/partners/events` |
| Analytics across your merchants | `GET /v1/partners/analytics/*` |
### Operating on behalf of a merchant
Authenticated with the merchant's `merchant-key`, which you receive in the `store_onboarded` webhook.
| Action | Endpoint |
| - | - |
| Create an order | `POST /v1/orders` |
| Create a payment link | `POST /v1/links` |
| Check order status | `GET /v1/orders/{orderId}` |
Payment endpoints are authorized by the **merchant key**, not by your partner credentials — the merchant key is what decides which store the order or link belongs to. See [Partner Authentication](/partners/authentication) for exactly which headers go on which request.
## Webhooks
Your webhook URL receives both merchant-lifecycle events (`store_onboarded`) and payment events (`order_payment`, `link_paid`) for every merchant connected to you — you do not configure a webhook per merchant. See [Partner Webhooks](/partners/webhooks).
## Becoming a partner
Partner access is granted case by case. Contact [sales@wava.co](mailto:sales@wava.co) with details about your platform and expected transaction volume.
# Partner Webhooks
Source: https://docs.wava.co/partners/webhooks
One endpoint that receives every event for every merchant connected to you.
# Partner Webhooks
As a partner you configure **one** webhook URL on your partner account. Every merchant that connects to your platform routes its events to that URL — you never configure a webhook per merchant.
Each delivery tells you which merchant it belongs to via `id_store` (and `external_store_id` when the merchant supplied your own identifier).
## Reading your configuration
```bash theme={null}
curl "https://api.wava.co/v1/partners/webhook" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
```json theme={null}
{
"url": "https://your-platform.com/webhooks/wava",
"events": ["store_onboarded", "order_payment", "integration_uninstalled"],
"signing_enabled": true,
"signing_key_fingerprint": "3f2a9c1b7e04"
}
```
The signing key is never returned. If you have lost it, contact [soporte@wava.co](mailto:soporte@wava.co).
`signing_key_fingerprint` is the first 12 characters of the SHA-256 of the key we sign with. If your signatures are not matching, compare it against the digest of the key you hold before anything else — see [Webhook Security](/webhooks/security#checking-which-key-signed-a-delivery).
## Updating your configuration
```bash theme={null}
curl -X PATCH "https://api.wava.co/v1/partners/webhook" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-platform.com/webhooks/wava",
"events": ["store_onboarded", "integration_uninstalled"]
}'
```
Both fields are optional; omitting one leaves it unchanged. The response reports how many integrations were updated — the change is applied across your partner account and all merchant integrations attached to it.
The URL must be `https://`. A plain `http://` URL is rejected with `400 INVALID_WEBHOOK_URL`.
`GET /v1/partners/events` lists the events this endpoint will accept:
```bash theme={null}
curl "https://api.wava.co/v1/partners/events" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
It currently returns the store and integration lifecycle events (`store_onboarded`, `store_updated`, `store_deleted`, `integration_installed`, `integration_uninstalled`). Order and payment-link events are delivered to partners as well, but are configured on your account by Wava rather than through this endpoint — ask [soporte@wava.co](mailto:soporte@wava.co) to add or remove them.
## Testing your endpoint
```bash theme={null}
curl -X POST "https://api.wava.co/v1/partners/webhook/test" \
-H "X-API-Key: YOUR_PARTNER_API_KEY" \
-H "X-API-Secret: YOUR_PARTNER_SECRET_KEY"
```
Wava POSTs a `test.ping` payload to your configured URL, signed the same way real events are, and returns the HTTP status and round-trip time it observed:
```json theme={null}
{
"success": true,
"test_sent": true,
"url": "https://your-platform.com/webhooks/wava",
"response_status": 200,
"response_time_ms": 342,
"error": null
}
```
## Events you receive
| Event | Fires when | Payload |
| - | - | - |
| `store_onboarded` | A merchant onboarded through your link finishes onboarding, or re-subscribes | Includes the merchant's `merchant_key` — see [Onboarding](/partners/onboarding) |
| `order_payment` | An order for one of your merchants is paid | Same shape as direct API integrations |
| `link_paid` | A payment-link order created via the API is paid (alongside `order_payment`) | See [Webhook Events](/webhooks/events) |
| `integration_uninstalled` | Your integration is removed from a merchant's store | Below |
`order_payment` reaches you for orders created through the API — yours or the merchant's own direct integration. Orders created from the Wava dashboard UI do **not** emit partner webhooks.
### `order_payment`
```json theme={null}
{
"event": "order_payment",
"timestamp": "2026-08-06T15:41:09.220Z",
"id_order": 55217,
"id_external": "your-order-001",
"status": "confirmed",
"total_price": 50000,
"currency": "COP",
"payment_method": {
"gateway": "nequi",
"status": "processed"
},
"totalItems": 1,
"shopper": {
"first_name": "Juan",
"last_name": "Perez",
"email": "juan@email.com"
},
"external_store_id": "your-internal-merchant-id"
}
```
Route it to the right merchant with `external_store_id`, and to the right transaction with `id_external` (the `order_key` you sent) or `id_order`.
### `integration_uninstalled`
```json theme={null}
{
"event": "integration_uninstalled",
"timestamp": "2026-08-06T16:02:44.881Z",
"id_store": 2841,
"external_store_id": "your-internal-merchant-id",
"store_name": "Café Bogotá"
}
```
Stop billing and stop using that merchant's key when you receive this.
`order_cancelled` and `order_refunded` can be enabled on a partner account, but their payload shape is not finalized and may change without notice. Do not build against them yet.
## Delivery semantics
* Each attempt has a **5 second** timeout. Respond `2xx` immediately and process asynchronously.
* Wava makes up to **3 attempts**, roughly one second apart.
* A `4xx` response is treated as a permanent rejection and is **not** retried. Only connection failures and `5xx` responses are retried.
* A `2xx` response whose JSON body contains `"error": true` is recorded as a **failure**, not a success. Do not return that shape from a handler that succeeded.
* Every attempt is logged with its status, response body and timing, and is visible to Wava support when you report a delivery problem.
Handlers must be idempotent — deduplicate on `id_order` / `id_external` for order events and on `id_store` for store events.
## Verifying deliveries
Partner accounts are created with HMAC signing enabled and a dedicated signing key. Every delivery carries `X-Wava-Signature`; verify it before acting on the payload. See [Webhook Security](/webhooks/security) for the algorithm and code samples.
# Breb
Source: https://docs.wava.co/payment-flows/breb
Breb QR code and bank transfer payment flow.
# Breb Payment Flow
Breb enables payments via bank transfer using a QR code or transfer key. The buyer scans the QR or enters the key in their banking app to complete the payment.
## How it works
```mermaid theme={null}
sequenceDiagram
participant Merchant
participant Wava API
participant Breb
participant Buyer
Merchant->>Wava API: POST /v1/orders
Wava API->>Breb: Initiate payment
Breb-->>Wava API: QR code + transfer key
Wava API-->>Merchant: 200 OK (qr_code, key)
Merchant->>Buyer: Display QR code / key
Buyer->>Breb: Complete transfer via banking app
Breb->>Wava API: Payment confirmed (+ payer data, best-effort)
Wava API->>Merchant: Webhook (status: confirmed)
```
Call `POST /v1/orders` with the amount and the Breb gateway ID. The `shopper` object is optional for Breb.
The response includes a `qr_code` (base64-encoded image) and a `key` (text string). Display both to the buyer — the QR code for scanning, and the key as a fallback for manual entry.
The buyer opens their banking app, scans the QR code or enters the transfer key, and confirms the payment.
Once the transfer is completed, the order status changes to `confirmed`. You receive a webhook notification, or you can poll for the status.
## Shopper data
The `shopper` object is **optional** for Breb. The QR code and key are generated from the order, not from the buyer's data, so you can create a Breb order without any shopper information.
* You can omit `shopper` entirely or send only the fields you have. Fields you do send are validated for format (an invalid `email` returns `422`), but none of them is required.
* Shopper fields you did not send arrive as `null` in webhooks and when reading the order; the whole `shopper` object can be `null`.
* Nequi and Daviplata are not affected: they still require the full shopper (see [Creating orders](/orders/creating-orders)).
## Payer data
Breb reports who actually paid only **after** the transfer is confirmed, and the data is **best-effort**:
* **Name** and **document (masked, last digits only)** of the payer, as reported by the payer's bank. When present, Wava stores them on the order after confirmation, so `shopper.first_name` and `shopper.last_name` may be filled even if you created the order without shopper.
* **Email and phone number are never guaranteed** for Breb payers.
* The payer is whoever completed the transfer, which may not be the shopper you sent when creating the order.
Do not depend on these fields to fulfil the order: the order status (`confirmed`) is what confirms the payment.
## Example request
The minimal Breb order, without shopper:
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 100000,
"description": "Invoice payment",
"currency": "COP",
"payment_gateway": {
"id_payment_gateway": 9
},
"order_key": "invoice-11111"
}'
```
## Example response
```json theme={null}
{
"result": {
"id_order": 12347,
"status": "processing",
"payment_gateway": {
"breb": {
"qr_code": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEA...",
"key": "BREB-TXN-2025-ABCDEF123456"
}
}
}
}
```
## Displaying the QR code
The `qr_code` field contains a base64-encoded PNG image. To display it in HTML:
```html theme={null}
```
Display the `key` value as text alongside the QR code so the buyer can copy and paste it if they prefer not to scan.
## Important notes
* The buyer completes payment asynchronously (like Nequi), so you must rely on webhooks or polling for confirmation.
* Breb does not use any shopper data to generate the QR code or key, so `shopper` is optional. Payer data arrives after confirmation and is best-effort (see [Payer data](#payer-data)).
* The QR code and key have a limited validity window. If expired, cancel the order and create a new one.
# Daviplata
Source: https://docs.wava.co/payment-flows/daviplata
Daviplata OTP verification payment flow.
# Daviplata Payment Flow
Daviplata is Davivienda's digital wallet. Payments require OTP (one-time password) verification — the buyer receives a code via SMS and you submit it to complete the payment.
## How it works
```mermaid theme={null}
sequenceDiagram
participant Merchant
participant Wava API
participant Daviplata
participant Buyer
Merchant->>Wava API: POST /v1/orders (national_id)
Wava API->>Daviplata: Initiate payment
Daviplata->>Buyer: SMS with OTP code
Wava API-->>Merchant: 200 OK (daviplata_token_required: true)
Buyer->>Merchant: Provides OTP
Merchant->>Wava API: POST /v1/orders/daviplata/{orderId} (OTP)
Wava API->>Daviplata: Validate OTP
Daviplata-->>Wava API: Payment confirmed
Wava API-->>Merchant: 200 OK (status: confirmed)
```
Call `POST /v1/orders` with the buyer's `id_number`, `id_type`, and the Daviplata gateway ID.
Daviplata sends a 6-digit OTP to the buyer via SMS. Your application should display a form for the buyer to enter this code.
Call `POST /v1/orders/daviplata/{orderId}` with the OTP the buyer entered.
If the OTP is valid, the payment completes immediately and the response includes `status: confirmed`.
## Customer experience
1. Customer receives push notification with an OTP on their phone
2. Opens Daviplata app to review payment details
3. Takes OTP provided by Daviplata app and pastes it in the Merchant's checkout
4. Receives payment confirmation in-app
## Suggested UX implementation
### Show these steps to the user (in Spanish)
Display the following instructions to the buyer while waiting for the OTP:
1. **Recibirás una notificación con un OTP de 6 dígitos**
2. **Copia el OTP e ingrésalo en el sistema**
3. **Tu compra será procesada de manera automática**
### Processing animation
Use the following GIF to give the buyer visual context during the OTP flow:
```
https://wava-assets.s3.us-east-1.amazonaws.com/payment-gateways/daviplata-flow-2025.gif
```
## Required fields
| Field | Required | Description |
| - | - | - |
| `shopper.id_number` | Yes | Buyer's national ID (cedula) |
| `shopper.id_type` | Yes | Document type ID from [Document Types](/orders/document-types) |
| `shopper.email` | Yes | Buyer's email |
| `shopper.phone_number` | Yes | Buyer's phone number |
| `shopper.country` | Yes | `CO` |
Daviplata only accepts the following document types: **CC** (Cédula de Ciudadanía, ID 1), **CE** (Cédula de Extranjería, ID 2), and **TI** (Tarjeta de Identidad, ID 3). Sending any other document type will result in a payment error.
## Example: Create order
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 75000,
"description": "Product purchase",
"currency": "COP",
"shopper": {
"first_name": "Maria",
"last_name": "Gonzalez",
"email": "maria@email.com",
"phone_number": "+573001234567",
"country": "CO",
"id_number": "12345678",
"id_type": 1
},
"payment_gateway": {
"id_payment_gateway": 2
},
"order_key": "order-67890"
}'
```
The response will include `daviplata_token_required: true`:
```json theme={null}
{
"result": {
"id_order": 12346,
"status": "processing",
"daviplata_token_required": true,
"payment_gateway": {
"daviplata": {
"national_id_number": "****678",
"id_national_document_type": 1,
"id_type": "CC"
}
}
}
}
```
## Example: Submit OTP
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders/daviplata/12346" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"daviplata_otp": "123456"
}'
```
## Document types
Use `GET /v1/national-document-types/CO` to retrieve valid document types. See [Document Types](/orders/document-types) for details.
For Daviplata, only these types are accepted:
| ID | Code | Description |
| - | - | - |
| 1 | CC | Cédula de Ciudadanía |
| 2 | CE | Cédula de Extranjería |
| 3 | TI | Tarjeta de Identidad |
## Development testing
In the sandbox, use the following OTP codes:
| OTP Code | Result |
| - | - |
| `123456` | Success |
| `000000` | Success |
| `111111` | Success |
| `999999` | Error: invalid OTP |
| Any other value | Error: invalid OTP |
See [Test Data](/testing/test-data) for more test values.
# Nequi
Source: https://docs.wava.co/payment-flows/nequi
Nequi push notification payment flow.
# Nequi Payment Flow
Nequi is Colombia's leading mobile wallet. Payments are completed via push notification — the buyer approves the payment directly in their Nequi app.
## How it works
```mermaid theme={null}
sequenceDiagram
participant Merchant
participant Wava API
participant Nequi
participant Buyer
Merchant->>Wava API: POST /v1/orders (phone_number)
Wava API->>Nequi: Initiate payment request
Nequi->>Buyer: Push notification
Wava API-->>Merchant: 200 OK (status: processing)
Buyer->>Nequi: Approve payment
Nequi->>Wava API: Payment confirmed
Wava API->>Merchant: Webhook (status: confirmed)
```
Call `POST /v1/orders` with the buyer's `phone_number`, `id_number`, `id_type`, and the `id_payment_gateway` for Nequi.
Nequi sends a push notification to the buyer's phone. The buyer opens the Nequi app and approves the payment.
Once the buyer approves, the order status changes to `confirmed`. You receive a webhook notification, or you can poll for the status.
## Customer experience
1. Customer receives push notification on their phone
2. Opens Nequi app to review payment details
3. Confirms or rejects the payment within the Nequi app
4. Receives confirmation within the app
## Suggested UX implementation
### Show these steps to the user (in Spanish)
Display the following instructions to the buyer while waiting for payment approval:
1. **Recibirás una notificación push en tu teléfono**
2. **Revisa los datos y acepta el cobro**
3. **Tu compra se procesará automáticamente**
### Processing animation
Use the following GIF to give the buyer visual context while waiting for them to approve the payment in Nequi:
```
https://wava-assets.s3.us-east-1.amazonaws.com/payment-gateways/nequi-flow-2025.gif
```
## Required fields
| Field | Required | Description |
| - | - | - |
| `shopper.phone_number` | Yes | The buyer's Nequi-registered phone number |
| `shopper.email` | Yes | Buyer's email |
| `shopper.country` | Yes | `CO` |
| `shopper.id_number` | Yes | Buyer's national ID number (required for compliance) |
| `shopper.id_type` | Yes | Document type ID from [Document Types](/orders/document-types) |
| `payment_gateway.id_payment_gateway` | Yes | Nequi gateway ID from `/v1/orders/paymentGateways` |
## Example request
```bash theme={null}
curl -X POST "https://api.wava.co/v1/orders" \
-H "merchant-key: YOUR_MERCHANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"description": "Premium subscription",
"currency": "COP",
"shopper": {
"first_name": "Juan",
"last_name": "Perez",
"email": "juan@email.com",
"phone_number": "+573001234567",
"country": "CO",
"id_number": "1234567890",
"id_type": 1
},
"payment_gateway": {
"id_payment_gateway": 1
},
"order_key": "order-12345"
}'
```
## Example response
```json theme={null}
{
"result": {
"id_order": 12345,
"status": "processing",
"payment_gateway": {
"nequi": {
"phone_number": "****567"
}
}
}
}
```
## Important notes
* The buyer has a limited time window to approve the push notification before it expires.
* If the notification expires, the order remains in `processing` status. You can cancel it and create a new one.
* While Nequi uses the phone number for the push notification, national ID fields (`id_number` and `id_type`) are also required on Nequi orders for compliance purposes.
## Development testing
In the sandbox, Nequi push notifications are simulated. To confirm an order, call `GET /v1/orders/{orderId}` with the order ID. This call will automatically confirm the order and trigger the webhook.
```bash theme={null}
# In development: this call auto-confirms the Nequi order
curl -X GET "https://api.dev.wava.co/v1/orders/12345" \
-H "merchant-key: YOUR_DEV_MERCHANT_KEY"
```
See [Test Data](/testing/test-data) for test phone numbers.
# Payment Flows
Source: https://docs.wava.co/payment-flows/overview
How each payment gateway works from order creation to confirmation.
# Payment Flows
Each payment gateway has a different flow for how the buyer completes their payment. This section explains the end-to-end process for each gateway.
## Available gateways
Push notification flow. Buyer approves payment in the Nequi app.
OTP verification flow. Buyer receives a code via SMS and submits it.
QR code / transfer key flow. Buyer completes a bank transfer.
Card payment flow. Configured through the Wava dashboard.
## Order lifecycle
All payment gateways follow the same order status progression:
```mermaid theme={null}
stateDiagram-v2
[*] --> pending: Order created
pending --> processing: Payment initiated
processing --> confirmed: Payment successful
processing --> cancelled: Rejected / Timeout
confirmed --> refunded: Refund processed
```
| Status | Description |
| - | - |
| `pending` | Order has been created but payment has not yet been initiated with the gateway. |
| `processing` | Payment is in progress — waiting for buyer action (push approval, OTP, transfer). |
| `confirmed` | Payment completed successfully. Funds will be included in your next payout. |
| `cancelled` | Order was cancelled — buyer rejected the payment, it timed out, or it was cancelled via API. |
| `refunded` | A refund has been processed for this order. |
## Gateway discovery
Before creating orders, call `GET /v1/orders/paymentGateways` to determine which gateways are active for your store and what fields each requires.
```bash theme={null}
curl -X GET "https://api.wava.co/v1/orders/paymentGateways" \
-H "merchant-key: YOUR_MERCHANT_KEY"
```
## Webhooks
For Nequi and Breb, the buyer completes payment asynchronously. Use [webhooks](/webhooks/overview) to receive real-time status updates, or poll `GET /v1/orders/{orderId}`.
## UX requirements
All implementations must be reviewed and approved by the Wava team before going live. You must display:
* **Payment gateway logos** using official assets provided by Wava
* **Processing animations** using official Wava GIFs showing step-by-step payment flows
* **User-facing instructions** in Spanish, guiding the buyer through each step
See each gateway's page below for specific UX assets and copy.
# Stripe
Source: https://docs.wava.co/payment-flows/stripe
Card payments through Stripe integration.
# Stripe Payment Flow
Stripe enables card payments through the Wava platform. Unlike other gateways, Stripe is not used via the direct API — it is configured through the Wava merchant dashboard and presented to buyers as a payment option in the Wava checkout.
## How it works
1. You enable Stripe in your Wava dashboard by connecting your Stripe account.
2. When a buyer opens a payment link or checkout, Stripe appears as an available payment method alongside digital wallets.
3. The buyer selects Stripe, enters their card details, and completes the payment.
4. The order status updates to `confirmed` and you receive a webhook notification.
## Setup
Stripe configuration is done entirely through the merchant dashboard. See [Stripe Integration](/integrations/stripe) for setup instructions.
Stripe payments are available through payment links and the Wava checkout page. They are not available through the direct `POST /v1/orders` API endpoint.
# Sandbox Environment
Source: https://docs.wava.co/testing/sandbox
Test your integration without processing real payments.
# Sandbox Environment
The development environment at `https://api.dev.wava.co/v1` is a full sandbox that simulates payment processing without moving real money.
## What is simulated
* Order creation and status transitions
* Payment gateway responses (push notifications, OTP codes, QR codes)
* Webhook delivery to your development endpoint
* All error scenarios
## Getting started
1. Create a development account at [app.dev.wava.co](https://app.dev.wava.co) (see [Authentication](/getting-started/authentication)).
2. Get your development `merchant-key` from **Settings > Integrations > API**.
3. Point your API calls to `https://api.dev.wava.co/v1`.
4. Use the [test data](/testing/test-data) values for phone numbers, national IDs, and OTP codes.
## Gateway-specific testing behavior
### Nequi (auto-confirm via GET)
In development, Nequi push notifications are simulated. To confirm a Nequi order, call the **get order endpoint** with the order ID. This call will automatically confirm the order and trigger the webhook.
```bash theme={null}
# Create a Nequi order
curl -X POST "https://api.dev.wava.co/v1/orders" \
-H "merchant-key: YOUR_DEV_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 10000,
"description": "Test payment",
"currency": "COP",
"shopper": {
"first_name": "Test",
"last_name": "User",
"email": "test@email.com",
"phone_number": "+573001234567",
"country": "CO"
},
"payment_gateway": { "id_payment_gateway": 1 },
"order_key": "test-nequi-001"
}'
# Then confirm it by calling GET /v1/orders/{orderId}
curl -X GET "https://api.dev.wava.co/v1/orders/12345" \
-H "merchant-key: YOUR_DEV_KEY"
# Response will show status: "confirmed" and webhook will be triggered
```
### Daviplata (test OTPs)
Daviplata orders work the same as production, but use test OTP codes:
| OTP Code | Result |
| - | - |
| `123456` | Success |
| `000000` | Success |
| `111111` | Success |
| `999999` | Error: invalid OTP |
| Any other value | Error: invalid OTP |
### Breb
Breb orders return simulated QR codes and transfer keys. The order can be confirmed via polling or webhook.
## Differences from production
| Aspect | Sandbox | Production |
| - | - | - |
| Base URL | `api.dev.wava.co/v1` | `api.wava.co/v1` |
| Real payments | No | Yes |
| Nequi confirmation | Auto-confirm via `GET /v1/orders/{id}` | Buyer approves in Nequi app |
| Daviplata OTP | Test codes only | Real SMS OTP |
| Webhook delivery | To dev URL | To prod URL |
| Rate limits | Relaxed | Standard |
| Merchant key | Development key | Production key |
# Test Data
Source: https://docs.wava.co/testing/test-data
Phone numbers, IDs, and OTP codes for sandbox testing.
# Test Data
Use these values when testing in the development environment (`https://api.dev.wava.co/v1`).
## Nequi test data
Use any valid Colombian phone number format for Nequi test payments. In the sandbox, push notifications are simulated.
**To confirm a Nequi order in development**, call `GET /v1/orders/{orderId}`. This automatically confirms the order and triggers the webhook.
| Field | Test Value | Notes |
| - | - | - |
| Phone number | `+573001234567` | Any valid Colombian format works |
```bash theme={null}
# 1. Create the Nequi order
curl -X POST "https://api.dev.wava.co/v1/orders" \
-H "merchant-key: YOUR_DEV_KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 10000, "description": "Test", "currency": "COP",
"shopper": { "first_name": "Test", "last_name": "User",
"email": "test@test.com", "phone_number": "+573001234567", "country": "CO" },
"payment_gateway": { "id_payment_gateway": 1 }, "order_key": "test-001" }'
# 2. Auto-confirm by calling GET
curl -X GET "https://api.dev.wava.co/v1/orders/{orderId}" \
-H "merchant-key: YOUR_DEV_KEY"
```
## Daviplata test OTPs
When testing Daviplata payments, use these OTP values:
| OTP Code | Result | Use case |
| - | - | - |
| `123456` | ✅ Success | Happy path testing |
| `000000` | ✅ Success | Alternative success test |
| `111111` | ✅ Success | Alternative success test |
| `999999` | ❌ Error: invalid OTP | Negative testing — triggers `DAVIPLATA_OTP_ERROR` (6105) |
| Any other value | ❌ Error: invalid OTP | Negative testing |
## Test document numbers
Use any numeric string as a national ID number in the sandbox. The document type should be a valid ID from `GET /v1/national-document-types/CO`.
| Field | Test Value | Notes |
| - | - | - |
| National ID | `12345678` | Any numeric string works |
| Document type | `1` (CC) | Cedula de Ciudadania |
| Document type | `2` (CE) | Cedula de Extranjeria |
| Document type | `3` (TI) | Tarjeta de Identidad |
## Negative testing
Use these approaches to test error handling in your integration:
| Scenario | How to trigger |
| - | - |
| Invalid OTP | Submit OTP `999999` for Daviplata |
| Invalid merchant key | Use an incorrect or expired merchant key |
| Missing required fields | Omit `amount`, `description`, or `payment_gateway` |
| Invalid email | Submit an invalid email format in `shopper.email` |
| Invalid phone | Submit non-numeric characters in `shopper.phone_number` |
| Order not found | Call `GET /v1/orders/99999999` with a non-existent order ID |
| Gateway not supported | Use an `id_payment_gateway` not enabled for your store |
Test all error scenarios before going live. Your integration should gracefully handle every error case and display appropriate feedback to the buyer.
# Webhook Events
Source: https://docs.wava.co/webhooks/events
List of webhook event types.
# Webhook Events
## Order events
These events are sent to both direct merchants and partners.
| Event | Description | When it fires | Status |
| - | - | - | - |
| `order_payment` | Payment completed successfully | After buyer completes payment | Active |
| `link_paid` | A payment-link order was paid | After payment on an **API-created** payment link (in addition to `order_payment`) | Active |
| `order_created` | Order was created | After order is created via API | Upcoming |
| `order_confirmed` | Order confirmed by gateway | After gateway confirms payment | Upcoming |
| `order_cancelled` | Order was cancelled | After cancellation via API or timeout | Upcoming |
| `order_refunded` | Refund processed | After refund is completed | Upcoming |
`order_payment` is the active event for payment notifications. For payment links created via the API, `link_paid` fires alongside it for the same order. The remaining order events are planned; their payloads are not yet finalized.
## Partner events
These events are sent only to partners, on the single webhook URL configured for the partner account. See [Partner Webhooks](/partners/webhooks) for setup, and [Onboarding Merchants](/partners/onboarding) for the flow they belong to.
| Event | Description | When it fires | Status |
| - | - | - | - |
| `store_onboarded` | A merchant onboarded through the partner's link | When the merchant finishes onboarding, and again whenever they re-subscribe through the link | Active |
| `integration_uninstalled` | The partner integration was removed from a store | After the merchant or the partner deactivates the integration | Active |
| `integration_installed` | Integration activated on a store | — | Upcoming |
| `store_updated` | Store configuration changed | After a store updates its settings | Upcoming |
| `store_deleted` | Store was removed | After a store is deactivated | Upcoming |
## Payload structure
All webhook payloads are a **flat JSON object** — fields are at the top level, not nested under a `data` key. Every payload includes the `event` name and an ISO 8601 `timestamp`.
### `order_payment`
```json theme={null}
{
"event": "order_payment",
"timestamp": "2026-06-24T02:05:12.952Z",
"id_order": 12345,
"id_external": "order-12345",
"status": "confirmed",
"total_price": 50000,
"currency": "COP",
"payment_method": {
"gateway": "nequi",
"status": "processed"
},
"totalItems": 1,
"shopper": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com"
},
"external_store_id": null
}
```
### `link_paid`
Fires for payment-link orders created via the API, in addition to `order_payment`.
```json theme={null}
{
"event": "link_paid",
"timestamp": "2026-06-24T03:19:39.159Z",
"id_payment_link": 17624,
"id_order": 22316,
"id_store": 2366,
"wallet_id": 4712,
"status": "confirmed",
"amount": 12312,
"currency": "COP",
"source": "api",
"id_external": null
}
```
The `id_external` field contains the `order_key` you provided when creating the order. Use it (or `id_order`) to match webhooks to your internal records and to deduplicate retries.
#### `wallet_id`
The Wava balance wallet this payment credits — the merchant's account inside Wava where the funds land, not the buyer's payment method. (Nequi and Daviplata are also called digital wallets; those appear under `payment_method` on `order_payment`, and are a different thing entirely.)
| Value | Meaning |
| - | - |
| An integer | The link was created bound to that specific wallet, via the `wallet_id` field on [Create payment link](/links/overview). The payment credited it. |
| `null` | The link is not bound to any particular wallet. The payment credited the merchant's **default** wallet for the order's currency. |
Most merchants hold a single wallet per currency, so `null` is the normal value and means "the merchant's only wallet". `wallet_id` is populated only for merchants who have multiple wallets enabled on their account; if you create a link with `wallet_id` for a merchant who does not, the field is ignored and the webhook reports `null`.
A `null` here means "this event does not name a wallet" — it is never a stand-in for a specific one. If you need to know which wallet a merchant's funds settled into, read the wallet bound to the link rather than inferring it from the event, since a merchant can re-bind a link after it was paid.
### `store_onboarded`
Partner event. Sent when a merchant who signed up through the partner's onboarding link finishes onboarding and the partner integration is created on their store, and again whenever that merchant re-subscribes through the link. This is the event that hands a partner a merchant it can charge for — `merchant_key` is the credential used for that merchant's orders and links.
Handlers must be idempotent on `id_store`: the same merchant can produce this event more than once.
```json theme={null}
{
"event": "store_onboarded",
"timestamp": "2026-08-06T14:22:31.004Z",
"id_store": 2841,
"external_store_id": "your-internal-merchant-id",
"store_name": "Café Bogotá",
"store_url": "https://cafebogota.co",
"status": "active",
"merchant_key": "mk_live_9f2c7a1e4b8d6350a1c9",
"country": "Colombia",
"currency": "COP",
"referred_by": null
}
```
| Field | Notes |
| - | - |
| `id_store` | Wava's ID for the merchant. |
| `external_store_id` | The partner's own identifier for the merchant (the same field order events carry), taken from the `eid` parameter on the onboarding link; `null` if none was passed. |
| `merchant_key` | The merchant's API key. Store it — it is not retrievable from the Partners API. |
| `country` | Country name (e.g. `Colombia`), not an ISO code. |
| `currency` | The store's default currency (ISO 4217). |
| `referred_by` | Store ID from Wava's separate store-referral program, unrelated to the partner relationship. Normally `null`. |
See [Onboarding Merchants](/partners/onboarding) for the full flow around this event.
### `integration_uninstalled`
Partner event. Sent when the partner integration is removed from a merchant's store, by either side.
```json theme={null}
{
"event": "integration_uninstalled",
"timestamp": "2026-08-06T16:02:44.881Z",
"id_store": 2841,
"external_store_id": "your-internal-merchant-id",
"store_name": "Café Bogotá"
}
```
# Webhooks
Source: https://docs.wava.co/webhooks/overview
Receive real-time notifications when order status changes.
# Webhooks
Webhooks notify your server when events occur in the Wava platform — most importantly, when a payment is confirmed. Instead of polling for status, your server receives an HTTP POST request with the event data.
## Setup
Configure your webhook URL in the [Wava Dashboard](https://app.wava.co) under **Settings > Integrations > API**. You can set different URLs for development and production environments.
Your endpoint must:
* Accept `POST` requests
* Respond with a `200` status code within 5 seconds
* Be accessible over HTTPS (production)
## How webhooks work
### Direct API integrations
When a merchant integrates directly with the Wava API, webhooks are sent to the webhook URL configured in their dashboard. The primary event is `order_payment` (sent when a payment is confirmed).
### Partner integrations
Partners configure **one** webhook URL on their partner account — not one per merchant — and receive events for every merchant connected to them:
* **`store_onboarded`**: sent when a merchant connects the partner's platform. Carries that merchant's `merchant_key`, which the partner then uses to create orders and links for them.
* **Order webhooks** (`order_payment`, `link_paid`): sent when an API-created order for one of the partner's merchants is paid — same payload as direct API webhooks.
* **`integration_uninstalled`**: sent when the partner integration is removed from a merchant's store.
Each delivery identifies the merchant through `id_store` (store events) and `external_store_id`, your own identifier for the merchant, present in store and order events alike. See [Partner Webhooks](/partners/webhooks) for configuration and [Onboarding Merchants](/partners/onboarding) for the end-to-end flow.
## Webhook payload
Payloads are a **flat JSON object** — every field is at the top level (there is no nested `data` wrapper). Every payload includes the `event` name and a top-level `timestamp` (ISO 8601). When a payment is confirmed, Wava sends:
```json theme={null}
{
"event": "order_payment",
"timestamp": "2026-06-24T02:05:12.952Z",
"id_order": 12345,
"id_external": "order-12345",
"status": "confirmed",
"total_price": 50000,
"currency": "COP",
"payment_method": {
"gateway": "nequi",
"status": "processed"
},
"totalItems": 1,
"shopper": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com"
},
"external_store_id": null
}
```
Field notes:
* `total_price` is the order total (not `amount`). `currency` is the ISO 4217 code (e.g. `COP`).
* `payment_method` is an **object**; the gateway is `payment_method.gateway` (e.g. `nequi`, `breb`, `daviplata`).
* `id_external` is the `order_key` you provided when creating the order (`null` if none). Use it — or `id_order` — to match webhooks to your records and to deduplicate retries.
* `external_store_id` is your external store reference (`null` if none).
Payment links **created via the API** (`source: "api"`) also emit a `link_paid` event in addition to `order_payment` for the same order. Payment links created from the Wava **dashboard UI** (`source: "wava"`) do **not** trigger webhooks. See [Webhook Events](/webhooks/events) for the `link_paid` payload.
## Retry policy
Each delivery attempt has a **5 second** timeout, and Wava makes up to **3 attempts** roughly a second apart.
* **Connection failures and `5xx` responses are retried.**
* **`4xx` responses are not retried** — they are treated as a permanent rejection, so do not return `4xx` for a problem on your side that a retry would fix.
* A `2xx` response whose JSON body contains `"error": true` is recorded as a **failure**, not a success.
Every attempt is logged with its status, response body and timing, and can be reviewed in the dashboard.
Always respond with `200` immediately, then process the webhook data asynchronously. This prevents timeouts and ensures reliable delivery.
# Webhook Security
Source: https://docs.wava.co/webhooks/security
Verifying webhook authenticity.
# Webhook Security
## Verifying webhooks
When you receive a webhook, verify it came from Wava before processing it. We recommend:
1. **Verify the signature** — API integrations with signing enabled receive an `X-Wava-Signature` header. See [Signature Verification](#signature-verification) below.
2. **Verify the order** — After receiving a webhook, call `GET /v1/orders/{orderId}` with your merchant key to confirm the order status matches what the webhook reported.
3. **Use HTTPS** — Always use an HTTPS endpoint for your webhook URL in production.
## Signature verification
API integrations with HMAC signing enabled receive a signature in the `X-Wava-Signature` header on every webhook request. Use it to confirm the payload has not been tampered with.
### How it works
Wava generates the signature by creating an HMAC-SHA256 hash of the JSON payload using your shared secret, then sends the hexadecimal result in the `X-Wava-Signature` header.
### Request headers
```
Content-Type: application/json
X-Wava-Event: order_payment
X-Wava-Timestamp: 2024-12-18T10:30:00.000Z
X-Wava-Signature: a1b2c3d4e5f6...
User-Agent: Wava-Webhooks/1.0
```
### Verification examples
```javascript theme={null}
const crypto = require('crypto');
function verifyWavaWebhook(req, res, next) {
const signature = req.headers['x-wava-signature'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
const payloadString = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', process.env.WAVA_WEBHOOK_SECRET)
.update(payloadString)
.digest('hex');
const received = Buffer.from(signature, 'hex');
const expected = Buffer.from(expectedSignature, 'hex');
// timingSafeEqual throws if the buffers differ in length — check first.
const isValid =
received.length === expected.length &&
crypto.timingSafeEqual(received, expected);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
```
```python theme={null}
import hmac
import hashlib
import json
import os
def verify_webhook_signature(payload, signature):
secret = os.environ['WAVA_WEBHOOK_SECRET']
payload_string = json.dumps(payload, separators=(',', ':'))
expected = hmac.new(
secret.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
```
```php theme={null}
function verifyWebhookSignature($payload, $signature) {
$secret = $_ENV['WAVA_WEBHOOK_SECRET'];
$payloadString = json_encode($payload, JSON_UNESCAPED_SLASHES);
$expected = hash_hmac('sha256', $payloadString, $secret);
return hash_equals($signature, $expected);
}
```
Always use a constant-time comparison function (`timingSafeEqual`, `compare_digest`, `hash_equals`) to prevent timing attacks. Never use `===` or `==` to compare signatures.
### Important notes
* The `X-Wava-Signature` is a plain hexadecimal string — no prefix like `sha256=`.
* Serialize the payload using `JSON.stringify()` with default options. Property order matters.
* Store your secret in an environment variable — never commit it to source control.
* The signature is computed over the exact JSON body Wava sends, using HMAC-SHA256 by default.
* **Use the signing key as a plain UTF-8 string.** It is 64 hexadecimal characters, but it is *not* hex-decoded before use — pass it to your HMAC function exactly as shown in your dashboard. Decoding it to 32 raw bytes first produces a valid-looking signature that will never match ours.
* **The webhook secret is the signing key (64 characters), not the API secret key (128 characters).** Partner accounts receive both in the same response at account creation; only the 64-character `signing_key` signs webhooks.
* **Partner accounts** are created with signing enabled and their own signing key, issued once at account creation. For direct API integrations signing is optional — contact [soporte@wava.co](mailto:soporte@wava.co) to enable it.
### Checking which key signed a delivery
If your signatures do not match, confirm you and Wava are using the same key before debugging the algorithm. `GET /v1/partners/webhook` returns `signing_key_fingerprint`: the first 12 characters of the SHA-256 of the key Wava signs with.
Compute the same digest over the key you have configured:
```javascript theme={null}
crypto.createHash('sha256').update(yourKey).digest('hex').slice(0, 12)
```
If the two fingerprints differ, you are holding a different key — regenerate it from the dashboard and reconfigure. If they match, the key is correct and the mismatch is in how the signature is computed: check the two notes above about key encoding and which secret to use.
## Idempotency
Your webhook handler should be idempotent — processing the same webhook multiple times should produce the same result. Wava may send the same webhook more than once in rare cases (retries, network issues).
Use the `id_order` or `id_external` field to deduplicate incoming webhooks.
Never trust webhook data alone for critical business logic (e.g., shipping an order). Always verify the order status via the API before taking action.