# Introduction
The Weav API lets your server accept payments through QR Ph, GCash, Maya, cards and over-the-counter partners, check your balance, and follow your withdrawals. You integrate once, and Weav handles each provider behind the scenes.
## Base URL
```http title="Base URL"
https://api.weavpayments.com/v1
```
> **Note:** Every request goes to `api.weavpayments.com` over HTTPS. Live and test keys use the same base URL. Questions go to `support@weavpayments.com`.
## OpenAPI, Postman and AI tools
The whole API is described in an OpenAPI 3.1 file. Import it into Postman (**Import → Link**), Insomnia, Swagger UI or a code generator to get every endpoint, field and example.
- [openapi.yaml](https://docs.weavpayments.com/openapi.yaml) or [openapi.json](https://docs.weavpayments.com/openapi.json)
- For AI assistants and coding agents: [llms.txt](https://docs.weavpayments.com/llms.txt) (a short index) and [llms-full.txt](https://docs.weavpayments.com/llms-full.txt) (the full reference as Markdown). Point your assistant at either link.
## Conventions
- Requests and responses are JSON. Send `Content-Type: application/json`.
- Field names are `snake_case`. Fixed values such as statuses are `UPPER_SNAKE_CASE`.
- Money is a JSON number in pesos, with up to two decimal places. Weav writes whole amounts with one decimal, such as `1250.0`, and others as they are, such as `31.25`. You may send `1250`, `1250.0` or `1250.00`. Never a string, and never formatted.
- Dates and times are ISO 8601 with the Manila offset, such as `2026-10-26T10:12:00+08:00`.
- Every field is always present. An empty value is `null` (or `[]` or `{}` for a list or an object), never left out.
- Every successful response has `code`, `success`, `message` and `data`. `data` is always an object.
- Decide on `code` or the HTTP status. Never parse `message`, because its wording can change.
## Response envelope
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"status": "COMPLETED"
}
}
}
```
## Quick start
If you're new to Weav, [Core concepts](https://docs.weavpayments.com/concepts) shows what each part of the API is for and how they connect. [How payments work](https://docs.weavpayments.com/how-payments-work) shows who does what, with a diagram for each kind of channel.
1. Sign in to the Weav dashboard and, under Developers, create a test API key (it starts with `wv_test_`) and a signing key.
2. Set your webhook URL with [PUT /v1/webhook-endpoint](https://docs.weavpayments.com/reference/webhooks/set-webhook-endpoint).
3. The quickest way: create a checkout with [POST /v1/checkouts](https://docs.weavpayments.com/reference/checkouts/create-checkout) and send your customer to its `checkout_url`. They choose how to pay on Weav's page. For full control, create a payment on one channel with [POST /v1/payments](https://docs.weavpayments.com/reference/payments/create-payment) instead.
4. Pay it as your customer would. With a payment of your own, show the QR code, send them to the checkout URL, or give them the payment code.
5. Finish the payment on the [test checkout page](https://docs.weavpayments.com/test-mode), then confirm your server received the signed webhook.
6. When everything works, switch to a `wv_live_` key.
---
# Core concepts
The things you work with in Weav, what each one is for, and how they connect. Read this first, then [How payments work](https://docs.weavpayments.com/how-payments-work) for who does what in a payment.
```mermaid
flowchart TD
CK[Checkout] -->|makes one or more| PY[Payment]
PM[Payment method
a saved card] -->|makes| PY
CH[Channel] -.->|each payment uses one| PY
PY -->|paid, less Weav's fee| BL[Balance]
RF[Refunds and chargebacks] -->|come off| BL
BL -->|you withdraw| WD[Withdrawal]
WD --> BK[Your bank account]
```
Every change to a checkout, payment, payment method or withdrawal also reaches your server as a [webhook](#webhooks).
## Payment
One try to collect money from your customer, on one channel. Its ID starts with `PY`.
- You create it with [POST /v1/payments](https://docs.weavpayments.com/reference/payments/create-payment), on a channel or with a saved card. Weav also creates one for each channel your customer tries in a checkout.
- It starts `PENDING`, then becomes `COMPLETED`, `FAILED` or `CANCELLED`. A paid payment can later be refunded or reversed. See [Statuses](https://docs.weavpayments.com/statuses#payment).
- Once it's paid, the amount goes to your balance, less Weav's fee.
- Your `external_reference_id` (your order number, for example) ties it to your system and makes retries safe. See [Idempotency](https://docs.weavpayments.com/idempotency).
## Checkout
One link for one order, where your customer chooses how to pay. Its ID starts with `CK`.
- You create it with [POST /v1/checkouts](https://docs.weavpayments.com/reference/checkouts/create-checkout) and send your customer to its `checkout_url`.
- Each channel your customer tries is a payment, with `checkout_reference_id` set. The first payment that's paid completes the checkout.
- Use a checkout when you don't want to choose the channel yourself. Create a payment when you do.
## Channel
How your customer pays: QR Ph, GCash, Maya, a card, or cash at Pay & Go, PalawanPay or Bayad. Each payment uses one.
- Each channel asks your customer for something different: a QR code to scan, a page to pay on, or a code to bring to a counter. That's the payment's `next_action`. See [Channels](https://docs.weavpayments.com/channels).
- [GET /v1/channels](https://docs.weavpayments.com/reference/channels/list-channels) shows which channels your account can use right now, and your payment limits.
## Payment method
A card your customer saved, so you can charge it later without them there. Its ID starts with `PM`.
- Your customer saves it on a Weav page, on its own or while they pay. Card numbers never reach your systems.
- Each charge is a new payment: send its `payment_method_id` to [POST /v1/payments](https://docs.weavpayments.com/reference/payments/create-payment).
- It's `PENDING` until their bank confirms it, then `ACTIVE`. See [Saved cards](https://docs.weavpayments.com/saved-cards) and [Subscriptions](https://docs.weavpayments.com/subscriptions).
## Balance
The money Weav holds for you.
- Paid payments add to it, less Weav's fee. Refunds, chargebacks and withdrawals take from it.
- [GET /v1/balance](https://docs.weavpayments.com/reference/balance/get-balance) gives `available` (what you can withdraw) and `on_hold` (withdrawals on their way to your bank).
- Every change is a [ledger entry](https://docs.weavpayments.com/reference/balance/list-ledger-entries), with the balance after it. An entry never changes: a correction is a new entry.
## Withdrawal
Money from your balance to your bank account, by InstaPay or PESONet. Its ID starts with `WD`.
- You withdraw in the dashboard, because each one needs a code from a person's authenticator app. Or turn on automatic withdrawals there: once a day, Weav withdraws your available balance.
- The amount leaves your balance when you ask. If the withdrawal fails, it comes back.
- The API reads withdrawals: [GET /v1/withdrawals](https://docs.weavpayments.com/reference/withdrawals/list-withdrawals). See [Statuses](https://docs.weavpayments.com/statuses#withdrawal).
## Webhooks
Weav tells your server when something changes, with a signed `POST` to your webhook URL.
- Set the URL with [PUT /v1/webhook-endpoint](https://docs.weavpayments.com/reference/webhooks/set-webhook-endpoint).
- Events are named after what changed: `PAYMENT_*`, `CHECKOUT_*`, `PAYMENT_METHOD_*` and `WITHDRAWAL_*`. See [Event types](https://docs.weavpayments.com/webhooks/event-types).
- Check each signature before you trust it. See [Verifying signatures](https://docs.weavpayments.com/webhooks/verifying-signatures).
- Act on webhooks, not on where your customer's browser lands: landing on your `success_url` doesn't prove they paid.
## Test and live
Everything above exists in test mode and in live mode, apart from each other. Your API key's prefix decides which one you see: `wv_test_` or `wv_live_`. See [Test mode](https://docs.weavpayments.com/test-mode).
---
# How payments work
Who does what in a payment, from checkout to the webhook, for each kind of channel. The numbers in each diagram are the order things happen in.
## Who’s involved
| Who | What they do |
| --- | --- |
| **Payer** | Your customer. Pays with a QR Ph app, an e-wallet, a card or cash at a counter. |
| **Your server** | Creates the payment with your API key, shows the payer what to do next, and fulfills the order when the webhook arrives. Your app or website talks to your server, never to Weav with your key. |
| **Weav** | Sets the payment up with the provider, tracks its status, credits your balance and sends you signed webhooks. |
| **Provider** | Where the money moves: the payer’s bank or e-wallet through QR Ph, GCash, Maya, the card network, or a Pay & Go, Palawan or Bayad counter. |
## Payment link (the default)
Send the payer to Weav’s payment page. Nothing is set up with the provider until they open it; then the page shows the QR code, the counter code or the way to the provider’s page, with how-to-pay steps and a live status.
```mermaid
sequenceDiagram
autonumber
actor Payer
participant Server as Your server
participant Weav
participant Provider
Payer->>Server: Checks out
Server->>Weav: POST /v1/payments
Weav-->>Server: 201 PENDING
next_action.checkout_url
Server-->>Payer: Sends them to checkout_url
Payer->>Weav: Opens Weav's payment page
Weav->>Provider: Sets up the payment
Weav-->>Payer: QR code, counter code or the
provider's page, with steps
Payer->>Provider: Pays
Provider->>Weav: Confirms the payment
Note over Weav: COMPLETED
your balance is credited
Weav-->>Payer: The page shows it as paid
Weav->>Server: PAYMENT_COMPLETED
webhook, signed
Server-->>Weav: 2xx within 10 seconds
Note over Server: Verify X-Signature,
then fulfill the order once
```
The link lasts 24 hours unopened. Once the payer opens it, `expires_at` becomes the provider’s window, for example 15 minutes for a QR Ph code.
## QR Ph, shown by you
With `"next_action_format": "DIRECT"` you get the raw QR string and show it yourself. Any QR Ph bank or e-wallet app can pay it.
```mermaid
sequenceDiagram
autonumber
actor Payer
participant Server as Your server
participant Weav
participant Provider as Payer's bank
or e-wallet
Payer->>Server: Checks out
Server->>Weav: POST /v1/payments
QR_PH, DIRECT
Weav->>Provider: Sets up the QR Ph code
Weav-->>Server: 201 PENDING, DISPLAY_QR
qr_string, instructions
Server-->>Payer: Shows the QR code and the steps
alt Paid before expires_at
Payer->>Provider: Scans the code
in their app and pays
Provider->>Weav: Confirms the payment
Weav->>Server: PAYMENT_COMPLETED
webhook
else Not paid in time
Weav->>Server: PAYMENT_FAILED webhook
failure.code EXPIRED
end
```
A payment that failed or was canceled can still complete if the provider confirms the money arrived late. Act on the latest status. See [Statuses](https://docs.weavpayments.com/statuses).
## Redirect: GCash, Maya and cards
The payer pays on the provider’s own page. With `"next_action_format": "DIRECT"`, `checkout_url` is that page, as below; with a payment link, Weav’s page sends them there. Where their browser lands afterward only decides what they see; it never confirms the payment.
```mermaid
sequenceDiagram
autonumber
actor Payer
participant Server as Your server
participant Weav
participant Provider as GCash, Maya
or card checkout
Payer->>Server: Checks out
Server->>Weav: POST /v1/payments
GCASH, MAYA or CARD, DIRECT,
success_url, cancel_url
Weav->>Provider: Creates the checkout
Weav-->>Server: 201 PENDING
REDIRECT: checkout_url
Server-->>Payer: Redirects to checkout_url
Payer->>Provider: Pays, or cancels
Provider-->>Payer: Sends them back through Weav
Payer->>Weav: Returns
Weav->>Provider: Checks the result
Weav-->>Payer: Redirects to
success_url or cancel_url
Note over Payer,Server: Landing on success_url
is not proof of payment
Provider->>Weav: Confirms the payment
Weav->>Server: PAYMENT_COMPLETED
webhook
```
If the provider is still confirming, the payer sees a Weav page that updates by itself before the redirect. The provider’s confirmation can come before or after the payer returns.
## Over the counter: Pay & Go, PalawanPay and Bayad
The payer gets a payment code and pays cash to **Eplayment** at a kiosk or counter. With `"next_action_format": "DIRECT"` you get the code and give it to them, as below; with a payment link, Weav’s page shows it.
```mermaid
sequenceDiagram
autonumber
actor Payer
participant Server as Your server
participant Weav
participant Provider as Kiosk or counter
Payer->>Server: Checks out
Server->>Weav: POST /v1/payments
PAY_AND_GO, PALAWANPAY or BAYAD,
DIRECT
Weav-->>Server: 201 PENDING
PAY_AT_COUNTER: payment_code
Server-->>Payer: Gives them the payment code
alt Paid within 48 hours
Payer->>Provider: Pays cash to Eplayment
with the code
Provider->>Weav: Confirms the payment
Weav->>Server: PAYMENT_COMPLETED
webhook
else Not paid in time
Weav->>Server: PAYMENT_FAILED webhook
failure.code EXPIRED
end
```
## Webhook delivery
Every status change reaches your server as a signed webhook. Answer within 10 seconds, do the work in a queue, and handle each event once: the same event can arrive twice, and events can arrive out of order.
```mermaid
sequenceDiagram
autonumber
participant Weav
participant Server as Your server
Weav->>Server: POST to your webhook URL (X-Signature)
alt 2xx within 10 seconds
Note over Weav: Delivered
else Anything else: 4xx, 5xx, timeout,
TLS error or redirect
loop Up to 18 attempts over about 7 days
Weav->>Server: Retries, freshly signed each time
end
Note over Weav: Still failing: waits in the
dashboard for you to repost
end
Server->>Weav: GET /v1/payments/{reference_id}
at any time
Weav-->>Server: The payment's latest status
```
See [Event types](https://docs.weavpayments.com/webhooks/event-types) for the retry schedule and [Verifying signatures](https://docs.weavpayments.com/webhooks/verifying-signatures) for the check.
---
# Authentication
Every request needs one of your secret API keys, sent as a Bearer token.
```http
GET /v1/balance
Host: api.weavpayments.com
Authorization: Bearer wv_test_••••••••••••••••
```
## Live and test keys
| Prefix | Mode | What it does |
| --- | --- | --- |
| `wv_live_` | Live | Real payments, real money, real webhooks. |
| `wv_test_` | Test | Simulated payments that you finish on the test checkout page. No money moves. |
The key's prefix decides the mode. Live keys never see test data, and test keys never see live data.
## Managing keys
- Create, rename, regenerate and delete keys under Developers in the dashboard.
- Weav shows a key in full only once, when you create it. Store it in your secrets manager right away.
- Regenerating a key stops the old one immediately.
- You can limit a key to your servers' addresses. See [IP allowlist](#ip-allowlist).
- Never put a key in a mobile app, a browser, or a public repository. Call Weav only from your server.
## IP allowlist
Limit a key to your servers' addresses, so a key that leaks can't be used from anywhere else. We recommend it for every live key.
- Set it per key under Developers in the dashboard, when you create or edit the key. An empty list allows any address.
- Up to 20 entries: IPv4 or IPv6 addresses, or ranges in CIDR notation, such as `203.0.113.10` or `203.0.113.0/24`. Separate them with commas, spaces or new lines.
- Use the public address your servers call out from: the one Weav sees. Behind a NAT gateway, load balancer or proxy, that's its public address, not your server's private one.
- A change to a live key's allowlist is emailed to your account's Owners and Developers.
- [Try it](https://docs.weavpayments.com/reference#try-it) on these docs calls Weav from your browser, so a key with an allowlist gets `403` there. Use a test key without one.
A request from any other address gets `403`:
```json title="403 Forbidden"
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
To call Weav, use the host name `api.weavpayments.com`, never an IP address: Weav's own addresses can change. Webhooks come to you from fixed addresses; see [IP addresses](https://docs.weavpayments.com/webhooks/ip-addresses).
## Errors
| Status | When |
| --- | --- |
| `401` | The key is missing, wrong, revoked or regenerated. |
| `403` | The key is valid, but the request came from an address outside its allowlist, a live key was sent from a browser (code `4030001`), your account isn’t active, or it’s a live key and live mode isn’t on for your account yet. |
---
# Test mode
Test mode is for building and testing your integration. It never uses a real provider sandbox. Weav simulates every channel and lets you decide how each payment ends.
## How it works
- Send requests with a `wv_test_` key. Everything else stays the same as live.
- Responses have the same shape as live. `next_action` still holds a QR string, a checkout URL or a payment code, depending on the channel.
- Every test payment has a `test_checkout_url`: the Weav test checkout page, where you pay, decline or let it expire. The QR string and the checkout URL lead there too. A counter payment's `payment_code` is a sample code that starts with `TP`; finish it at its `test_checkout_url`. In live mode, `test_checkout_url` is null.
- A test payment expires 15 minutes after you create it (48 hours for a counter channel), even as a payment link.
- Your balance, balance history and signed webhooks all behave as they do in live mode.
## The test checkout page
The page shows the payment's amount, channel and your reference, with three buttons.
| Button | Result | Webhook sent |
| --- | --- | --- |
| **Pay** | Status becomes `COMPLETED` and the amount is added to your test balance. | `PAYMENT_COMPLETED` |
| **Decline** | Status becomes `FAILED` with failure code `DECLINED`. | `PAYMENT_FAILED` |
| **Let it expire** | Status becomes `FAILED` with failure code `EXPIRED`. | `PAYMENT_FAILED` |
## Test saved cards
A test save link (`POST /v1/payment-methods`) opens Weav's test save page. Choose a test card, or make the bank's check fail. Then charge the saved card with `payment_method_id` on `POST /v1/payments`:
| Test card | When you charge it | Expires |
| --- | --- | --- |
| **Visa •••• 4242** | `COMPLETED`, at once | December, 3 years ahead |
| **Visa •••• 0002** | `FAILED` with failure code `DECLINED` | December, 3 years ahead |
| **Mastercard •••• 4444** | `COMPLETED`, at once | This month, so `expires_by` finds it |
A card saved while paying (`save_payment_method`) is Visa •••• 4242. See [Saved cards](https://docs.weavpayments.com/saved-cards) and [Subscriptions](https://docs.weavpayments.com/subscriptions).
## Test withdrawals
Withdrawals are created in the dashboard. In test mode, the dashboard also lets you move a withdrawal to `COMPLETED` or `FAILED`, so you can test the `WITHDRAWAL_COMPLETED` and `WITHDRAWAL_FAILED` webhooks.
> **Note:** The test checkout page only works for test payments. Opening it for a live payment returns 404.
---
# Build with AI
Give your AI assistant or coding agent Weav's docs, so it writes your integration from the real API, not from guesses.
## Connect the MCP server
The docs have a public MCP server at `https://docs.weavpayments.com/mcp`. MCP (Model Context Protocol) is how AI tools reach outside help. With it, your assistant can search these docs and read any page or endpoint while it works. It reads the docs only: it never sees your API keys, and it can't call Weav's API.
```shell title="Claude Code"
claude mcp add --transport http weav-docs https://docs.weavpayments.com/mcp
```
```json title="Cursor (.cursor/mcp.json)"
{
"mcpServers": {
"weav-docs": {
"url": "https://docs.weavpayments.com/mcp"
}
}
}
```
```json title="VS Code (.vscode/mcp.json)"
{
"servers": {
"weav-docs": {
"type": "http",
"url": "https://docs.weavpayments.com/mcp"
}
}
}
```
In apps that take a custom MCP connector, such as Claude, use the same URL.
What it can do:
| Tool | What it does |
| --- | --- |
| `search_docs` | Finds the pages and sections about a question, such as "verify a webhook signature". |
| `get_page` | Reads one page as Markdown, by its path, such as `/webhooks/verifying-signatures`. |
| `list_endpoints` | Lists every endpoint, grouped by resource. |
| `get_endpoint` | Reads one endpoint: its fields, responses, errors and code samples. |
| `get_openapi` | Returns the OpenAPI file, for generating a client. |
## Every page as Markdown
Every page has a Markdown copy: add `.md` to its address, such as [/authentication.md](https://docs.weavpayments.com/authentication.md). Under each page's title: **Copy for LLM**, **View as Markdown**, and **More actions**, which opens the page in ChatGPT, Claude or Grok, or connects Cursor, VS Code or Claude Code to the MCP server in one click.
For the whole site at once:
- [llms.txt](https://docs.weavpayments.com/llms.txt): what Weav is, and a link to every page's Markdown ([llmstxt.org](https://llmstxt.org)).
- [llms-full.txt](https://docs.weavpayments.com/llms-full.txt): every page, guides and reference, in one file.
- [openapi.yaml](https://docs.weavpayments.com/openapi.yaml): the API as OpenAPI 3.1.
## Keep your keys out of chats
> **Warning:** Never paste an API key or signing key into an AI chat, a prompt or code it writes. Keep keys in your server's environment (for example `WEAV_API_KEY`), and build and test with a test key (`wv_test_`) only.
A good start for your assistant:
```text title="Prompt"
Use the weav-docs MCP server (or https://docs.weavpayments.com/llms.txt).
Integrate Weav checkouts into this app: create a checkout on the server,
send the customer to checkout_url, and fulfill the order on the
CHECKOUT_COMPLETED webhook after verifying X-Signature. Read the API key
from the WEAV_API_KEY environment variable. Use a test key while building.
```
---
# Errors
Errors use standard HTTP status codes. An error response never includes `data`, so you can't read partial results from a failed request.
## Error shape
```json title="json · 422 Unprocessable Content"
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"amount": [
"The amount field must be greater than 0."
],
"success_url": [
"The success url field must be a valid URL."
]
}
}
```
## Fields
| Field | Type | Description |
| --- | --- | --- |
| `code` | `string` | A 7-digit code for the exact case, listed below. Branch on this, not on `message`. |
| `success` | `boolean` | Always `false` on an error. |
| `message` | `string` | What went wrong, for people. It can change; don’t parse it. |
| `errors` | `object` | Only on `422`: each field that failed, with its messages. |
```json title="json · 409 Conflict"
{
"code": "4090001",
"success": false,
"message": "This Idempotency-Key was already used with a different request."
}
```
## Status codes
| Status | Meaning |
| --- | --- |
| `200` | Read or action succeeded. |
| `201` | A resource was created. |
| `202` | Accepted: queued to run shortly, such as a webhook repost. |
| `400` | The request body could not be read. |
| `401` | Missing or invalid API key. |
| `403` | The key can’t make this request, for example from an address outside its allowlist. |
| `404` | The resource doesn’t exist, or it belongs to another merchant or mode. |
| `405` | This method isn’t allowed on this path. |
| `409` | Conflict, such as an `external_reference_id` or Idempotency-Key reused with a different request, or canceling a payment that is no longer pending. |
| `422` | Validation failed. A number sent as a string also fails here. |
| `429` | Too many requests. Wait, then retry. |
| `500` | Something went wrong on our side. Retry the same request. |
| `501` | That channel isn’t live through the API yet. |
## Error codes
| code | HTTP | When | What to do |
| --- | --- | --- | --- |
| `4000000` | 400 | The request body isn’t valid JSON. | Send a valid JSON body with `Content-Type: application/json`. |
| `4010000` | 401 | The API key is missing, malformed, deleted or regenerated. | Send a current key as `Authorization: Bearer wv_...`. |
| `4030000` | 403 | This key can’t make this request, for example from an address outside its IP allowlist. | Call from an allowed address, or update the key’s allowlist in the dashboard. |
| `4030001` | 403 | A live key was sent from a browser page. Live keys belong on your server. | Use a test key in the browser (Try It takes test keys only), and call Weav with live keys only from your server. |
| `4040000` | 404 | Not found in this mode for your account. | Check the reference and that you use a live or test key to match. |
| `4050000` | 405 | This method isn’t allowed on this path. | Check the method in the endpoint’s docs. |
| `4090001` | 409 | The `external_reference_id` (or Idempotency-Key) was already used with a different request. | Use a new reference for a new payment; never change a request and keep its reference. |
| `4090002` | 409 | The same request is still in progress. | Wait a moment, then retry it. |
| `4090101` | 409 | The payment can’t be canceled: it’s no longer pending. | Retrieve the payment for its final status. |
| `4090102` | 409 | The payment can’t be canceled at the provider right now. | Retry shortly. The payment stays pending until then. |
| `4090201` | 409 | The webhook is still being sent. | Repost it once the current delivery finishes. |
| `4090301` | 409 | The checkout can’t be canceled: it’s no longer open. | Retrieve the checkout for its status. |
| `4090302` | 409 | The checkout’s payment in progress can’t be canceled at the provider right now. | Retry shortly. The checkout stays open until then. |
| `4090401` | 409 | The saved card can’t be removed at the card processor right now. | Retry shortly. It stays chargeable until then. |
| `4220000` | 422 | Validation failed. `errors` lists each field, for example an amount outside your [payment limits](https://docs.weavpayments.com/reference/channels/list-channels), a reused `external_reference_id` or a private webhook URL. | Fix the fields named in `errors` and send again. |
| `4220101` | 422 | The channel is off for your account. | Turn it on under Channels in the dashboard, or contact support. |
| `4220102` | 422 | The channel doesn’t accept that currency. | Use a currency listed in `errors.currency`. |
| `4220103` | 422 | This payment method can’t be charged. | Charge only an `ACTIVE` one. Ask your customer to save their card again if it failed or was removed. |
| `4220401` | 422 | Saving cards is off for your account. | Contact support. Weav turns it on once it has your PCI DSS evidence. |
| `4290000` | 429 | Too many requests for your account. | Wait for `Retry-After` seconds, then retry. See [Rate limiting](https://docs.weavpayments.com/rate-limiting). |
| `4290201` | 429 | This payment’s webhook was reposted less than 30 seconds ago. | Wait for `Retry-After` seconds. To check the payment’s status, [retrieve the payment](https://docs.weavpayments.com/reference/payments/retrieve-payment) instead. |
| `4290202` | 429 | Your account reposted 20 webhooks in this mode in the last minute. | Wait for `Retry-After` seconds. Weav already retries every webhook for about 7 days, so there’s no need to repost in bulk. |
| `5000000` | 500 | Something went wrong on our side. | Retry the same request. If it keeps happening, contact support. |
| `5010100` | 501 | That channel isn’t live through the API yet. | Use a test key for now, or contact support. |
A code’s meaning never changes once published. New codes may be added, so treat an unknown one by its HTTP status.
---
# Pagination
List endpoints return results newest first, one page at a time, using a cursor.
## Parameters
| Field | Type | Description |
| --- | --- | --- |
| `per_page` | `integer` | Results per page. Default 25. Values above 100 are lowered to 100. |
| `cursor` | `string` | The `next_cursor` or `prev_cursor` from the previous page. Send it back exactly as you got it. |
## The pagination object
```json
"pagination": {
"per_page": 25,
"next_cursor": "eyJpZCI6MTM0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
"prev_cursor": null,
"is_last_page": false
}
```
- A cursor is opaque. Never decode or build one.
- A null cursor means there are no more pages in that direction.
- There is no total count. Keep fetching until `is_last_page` is `true`.
- An empty list returns `200` with an empty array, never `404`.
---
# Idempotency
Networks fail. Retry a create with the same request, and Weav returns the payment or checkout the first one made, never a second one. Your `external_reference_id` is the key, so there's nothing extra to send.
```http
POST /v1/payments
Authorization: Bearer wv_test_••••••••
Content-Type: application/json
{ "channel": "QR_PH", "currency": "PHP", "amount": 1250.00, "external_reference_id": "ORD-10541" }
```
## Rules
- On `POST /v1/payments` and `POST /v1/checkouts`. The rules work the same way for a checkout.
- **Same `external_reference_id` and same body:** you get the payment that request created, with the same `201` and the same fields, as it is now (so its `status` may have moved on), plus the header `Idempotent-Replayed: true`.
- **Same `external_reference_id` and a different body:** `409` with code `4090001`. A reference belongs to one payment: if a customer retries an order, send a new reference such as `ORD-10541-2`.
- **A reference that an earlier payment already has under another key:** `422` with code `4220000` (`errors` names `external_reference_id`). This happens when the first request sent an `Idempotency-Key` and the retry doesn't, or sent a different one. Retry exactly as you sent the first request, or use a new reference for a new payment.
- **Same request while the first one is still running:** `409` with code `4090002`. Wait and retry.
- A request that creates nothing (for example it fails validation) leaves the reference unused, so you can fix the cause and retry.
- Test and live mode are separate.
- A checkout's `external_reference_id` is optional. A checkout with no reference and no `Idempotency-Key` is never matched to an earlier one, so each request makes a new checkout.
```mermaid
flowchart LR
K{Reference
used before?} -->|No| N[201
created]
K -->|Yes| B{First request
still running?}
B -->|Yes| W[409, 4090002
wait, then retry]
B -->|No| S{Same body?}
S -->|Yes| RP[201, the same one
Idempotent-Replayed]
S -->|No| CF[409, 4090001
use a new reference]
```
## Your own key (optional)
To use your own key, send an `Idempotency-Key` header, such as a new UUID for each new payment and the same one for every retry of it (up to 255 characters). Weav then matches retries on that key instead, with the same rules. Use one or the other for a payment, not both: a retry without the key you sent first is a new request, and its reference is already taken (`422`).
```http
Idempotency-Key: 7d1f6c0e-2b4a-4c9e-9f3d-6a1b8e2c5f10
```
---
# Rate limiting
Each account can make up to 3,000 API requests a minute (50 a second), across all of its keys in live and test mode together. If you need more, contact support.
## Headers
| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests your account may make each minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `Retry-After` | On a `429` only: seconds to wait before trying again. |
## When you go over
You get `429` with code `4290000`, and nothing in the request is done. Wait for `Retry-After` seconds, then retry. Retrying a create with the same request is always safe (see [Idempotency](https://docs.weavpayments.com/idempotency)).
```json title="json · 429 Too Many Requests"
{
"code": "4290000",
"success": false,
"message": "Too many requests. Wait, then retry."
}
```
## Staying under the limit
- Use [webhooks](https://docs.weavpayments.com/webhooks/event-types) to learn when a payment finishes instead of polling `GET /v1/payments/{reference_id}`.
- List with `per_page=100` and the cursor rather than many small pages.
- Spread retries out with backoff, and honor `Retry-After`.
- Setting your webhook URL is limited to 10 changes a minute.
---
# Retries
Networks fail, both ways. Weav retries every webhook it sends you until your server takes it, and you can retry your own calls to Weav without ever doing something twice.
## Retrying your calls to Weav
When a call fails or times out, decide by what came back. Resend a create exactly as it was, with the same `external_reference_id` (or the same `Idempotency-Key`, if you send one) and the same body (see [Idempotency](https://docs.weavpayments.com/idempotency)).
| What came back | Retry? | How |
| --- | --- | --- |
| No response: a timeout or a dropped connection | **Yes** | The same request. If the first one went through, you get its stored response, with `Idempotent-Replayed: true`. |
| `500`, code `5000000` | **Yes** | The same request. Failed requests aren’t stored, so the retry runs fresh. If it keeps happening, contact support. |
| `429`, code `4290000` | **Yes** | After `Retry-After` seconds. Nothing in the request was done. See [Rate limiting](https://docs.weavpayments.com/rate-limiting). |
| `409`, code `4090002` | **Yes** | The first request with this key is still running. Wait a moment, then retry with the same key. |
| `409`, code `4090102` | **Yes** | The provider couldn’t cancel right now. Retry the cancel shortly; the payment stays pending until then. |
| `429`, code `4290201` or `4290202` | Not in a loop | Repost limits. Wait for `Retry-After`; to check a status, [retrieve the payment](https://docs.weavpayments.com/reference/payments/retrieve-payment) instead. |
| `409`, code `4090001` | No | The key was already used for a different request. A new request needs a new key. |
| `400`, `401`, `403`, `404`, `422` | No | The same request will fail the same way. Fix what `code` and `errors` say, then send it again. |
- **Back off.** Wait longer after each failed try (for example 1, 2, 4, then 8 seconds), with a little randomness so your retries don’t all land at once, and give up after a few tries. Always wait at least `Retry-After` when Weav sends one.
- **Decide on `code`** or the HTTP status, never on `message`. See [Response codes](https://docs.weavpayments.com/reference/response-codes).
- **A payment you gave up on can still finish.** If you stop retrying, read the payment with `GET /v1/payments/{reference_id}` or wait for its webhook before you tell your customer it failed.
```mermaid
sequenceDiagram
autonumber
participant Server as Your server
participant Weav
Server->>Weav: POST /v1/payments
external_reference_id: ORD-1
Note over Server,Weav: The connection drops before the response arrives
Server->>Weav: The same request
Weav-->>Server: The stored 201 response
Idempotent-Replayed: true
Note over Server: One payment, however many times you retried
```
## When Weav retries a webhook
A delivery counts only when your endpoint answers with a `2xx` within 10 seconds. When an attempt fails, Weav tries again on this schedule. Each wait is counted from the attempt before it.
| Attempt | Wait after the previous attempt | About this long after the first |
| --- | --- | --- |
| 1 | None | Right away |
| 2 | 2 minutes | 2 minutes |
| 3 | 10 minutes | 12 minutes |
| 4 | 10 minutes | 22 minutes |
| 5 | 1 hour | 1 hour 22 minutes |
| 6 | 2 hours | 3 hours 22 minutes |
| 7 | 6 hours | 9 hours 22 minutes |
| 8 to 18 | 15 hours each | Up to about 7 days 6 hours |
- **18 attempts in all:** the first one plus 17 retries, over about 7 days.
- **A failed attempt** is anything other than a `2xx` within 10 seconds: a `4xx` or `5xx`, a timeout, a connection or TLS error, or a redirect. Weav doesn't follow redirects, so point your URL at the final address.
- **Retries are spread out.** Each wait gets up to 10% extra at random, so retries for many events don't hit your server at the same moment.
- **Tell us when you're busy.** If you answer `429` or `503` with a `Retry-After` header in seconds, Weav waits at least that long before the next attempt, up to 15 hours.
- **Every attempt is freshly signed.** Each one carries a new `sent_at` and a new `X-Signature`, so a retry never fails your 5-minute check. The event and its `data` stay the same.
- **After the last attempt**, the delivery is marked failed and stays in the dashboard. You can repost it from there at any time. A repost continues the attempt count.
- **You can see every attempt** under Developers in the dashboard, with its status code, the first 2 KB of your response, and how long it took.
## When a payment’s result comes late
A provider’s confirmation can arrive late or not at all. Weav checks with providers regularly, so a payment the customer paid still completes, even after it expired or was canceled, and you get its webhook then. Always act on the latest status. See [Statuses](https://docs.weavpayments.com/statuses).
To have a payment’s latest webhook sent again, for example after fixing your endpoint, use [Repost a webhook](https://docs.weavpayments.com/reference/webhooks/repost-payment-webhook).
---
# Event types
Weav sends a signed POST whenever something you need to know about happens. Set where it goes with [PUT /v1/webhook-endpoint](https://docs.weavpayments.com/reference/webhooks/set-webhook-endpoint); see [Where each webhook goes](https://docs.weavpayments.com/reference/webhooks/set-webhook-endpoint#where-each-webhook-goes).
| Event | When | data key |
| --- | --- | --- |
| `PAYMENT_COMPLETED` | The customer paid. | `payment` |
| `PAYMENT_FAILED` | The payment expired or was declined. | `payment` |
| `PAYMENT_CANCELLED` | The payment was canceled. | `payment` |
| `PAYMENT_REVERSED` | A completed payment was reversed after a chargeback, and the amount came off your balance. | `payment`, `reversal` |
| `PAYMENT_REFUNDED` | Weav recorded a refund, partial or full. `payment.status` says which. | `payment`, `refund` |
| `CHECKOUT_COMPLETED` | A checkout was paid. `checkout.payment` is the payment that paid it. | `checkout` |
| `CHECKOUT_EXPIRED` | A checkout ran out without being paid. A payment still in progress keeps it open until that payment ends. | `checkout` |
| `PAYMENT_METHOD_SAVED` | Your customer saved their card and their bank confirmed it. It's `ACTIVE`: you can charge it. | `payment_method` |
| `PAYMENT_METHOD_FAILED` | The bank's check failed, or the save link ran out. | `payment_method` |
| `PAYMENT_METHOD_REMOVED` | A saved card was removed. It can't be charged again. | `payment_method` |
| `WITHDRAWAL_COMPLETED` | A withdrawal reached your bank. | `withdrawal` |
| `WITHDRAWAL_FAILED` | A withdrawal failed and the amount was returned to your balance. | `withdrawal` |
## Delivery
- Your URL must use HTTPS. Weav verifies your TLS certificate.
- Webhooks come from two fixed addresses, `18.136.62.102` and `18.139.176.153`. See [IP addresses](https://docs.weavpayments.com/webhooks/ip-addresses).
- A delivery succeeds only when your endpoint answers with a `2xx` within 10 seconds. Anything else is retried, for about 7 days. See [Retries](https://docs.weavpayments.com/retries#when-weav-retries-a-webhook).
- When retries run out, the delivery waits in the dashboard for you to repost it.
- The dashboard shows every attempt, with its status code, response and duration.
- **The same event can arrive twice.** Use the resource's `reference_id` to make sure you handle it only once.
- **Events can arrive out of order.** Trust the `status` in the body, or read the resource again.
> **Note:** Reply right away and do the work in a queue. A handler that runs longer than 10 seconds counts as failed, and you'll receive the same event again.
---
# Payload
Every webhook has the same shape: the event name, when it was sent, and the full resource.
```http title="http · request headers"
POST /webhooks/weav HTTP/1.1
Host: shop.example
Content-Type: application/json
X-Signature: 9b2f4c…c41e
```
```json title="json · body"
{
"event": "PAYMENT_COMPLETED",
"sent_at": "2026-10-26T10:15:42+08:00",
"data": {
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"external_reference_id": "ORD-10541",
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.0,
"fee": 31.25,
"total": 1281.25,
"amount_refunded": 0.0,
"refunds": [],
"status": "COMPLETED",
"completed_at": "2026-10-26T10:15:40+08:00",
"created_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
| Field | Type | Description |
| --- | --- | --- |
| `event` | `string` | One of the [event types](https://docs.weavpayments.com/webhooks/event-types). |
| `sent_at` | `string` | When Weav sent this delivery. It is inside the body so the signature covers it. |
| `data` | `object` | The full resource under its own key. The payment above is shortened. |
A webhook never contains your signing key, an API key, or any other credential.
---
# Verifying signatures
Check every webhook before you trust it. The `X-Signature` header is an HMAC-SHA512 of the raw request body, in lowercase hex, made with your signing key.
> **Note:** Weav sends webhooks from two fixed addresses, which you can allow on your firewall: see [IP addresses](https://docs.weavpayments.com/webhooks/ip-addresses). Verify the signature anyway: an address alone doesn't prove a webhook is Weav's.
## Steps
1. Read the **raw** body bytes. Never decode and re-encode the JSON first, because that changes the bytes.
2. Compute HMAC-SHA512 of the raw body with your signing key, as lowercase hex.
3. Compare it with `X-Signature` using a constant-time comparison.
4. Reject the delivery if `sent_at` is more than 5 minutes from your server's time.
5. Reply `2xx` right away, then process the event in a queue.
## PHP (Laravel)
```php
// Verify with the raw body, before parsing anything
$raw_payload = $request->getContent();
$signature = (string) $request->header('X-Signature');
$valid = false;
foreach ($signing_keys as $signing_key) { // one key, or two during a rotation
$expected = hash_hmac('sha512', $raw_payload, $signing_key);
if (hash_equals($expected, $signature)) {
$valid = true;
break;
}
}
if (! $valid) {
abort(401);
}
$payload = json_decode($raw_payload, true);
// Reject old or replayed deliveries
if (abs(now()->diffInSeconds($payload['sent_at'])) > 300) {
abort(401);
}
// Reply 2xx right away, then do the work in a queue.
// The same reference_id can arrive more than once.
HandleWeavWebhook::dispatch($payload);
return response()->noContent();
```
## Node.js (Express)
```js title="javascript"
const crypto = require('crypto');
const express = require('express');
const app = express();
function isValidSignature(rawBody, signature, signingKey) {
const expected = crypto.createHmac('sha512', signingKey).update(rawBody).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(String(signature || ''), 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw keeps req.body as the exact bytes Weav sent
app.post('/webhooks/weav', express.raw({ type: 'application/json' }), (req, res) => {
const keys = [process.env.WEAV_SIGNING_KEY, process.env.WEAV_SIGNING_KEY_NEW].filter(Boolean);
const signature = req.get('X-Signature');
if (!keys.some((key) => isValidSignature(req.body, signature, key))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
if (Math.abs(Date.now() - Date.parse(event.sent_at)) > 5 * 60 * 1000) {
return res.sendStatus(401);
}
res.sendStatus(200);
queue.add(event); // process later; the same reference_id can arrive twice
});
```
## Rotating your signing key
Weav signs every webhook with **one** key: your active key. A second key only exists during a rotation, so you can switch keys without failing a single delivery.
1. **Rotate** in the dashboard. Weav creates a **next** key and shows it once. Your active key keeps signing.
2. **Add the next key to your server** next to the active one, and deploy. Your server now accepts a signature made with either key.
3. **Retire the old key** in the dashboard. The next key becomes active, and every webhook from then on is signed with it.
4. **Remove the old key** from your server.
You don’t need to know which key signed a webhook. Compute the HMAC with each key you hold and accept the webhook if either one matches, as the examples above do. Only the key that signed it can produce that signature, so trying both is as safe as checking one.
```mermaid
sequenceDiagram
autonumber
participant You as You, in the dashboard
participant Weav
participant Server as Your server
Note over Weav: Active key A signs every webhook
You->>Weav: Rotate
Weav-->>You: Next key B, shown once
You->>Server: Add key B next to key A, deploy
Weav->>Server: Webhooks still signed with A
Note over Server: Accepts A or B
You->>Weav: Retire key A
Note over Weav: B is now the active key
Weav->>Server: Webhooks signed with B
You->>Server: Remove key A
```
---
# IP addresses
Weav sends every webhook from these two IP addresses. If your firewall only lets known addresses in, allow both.
| IP address | Version |
| --- | --- |
| `18.136.62.102` | IPv4 |
| `18.139.176.153` | IPv4 |
## Where they apply
- Every webhook Weav sends: to your webhook URL, your `withdrawals_url`, and a payment's own `webhook_url`.
- Live and test mode alike, first attempts and [retries](https://docs.weavpayments.com/retries).
- Webhooks come only from these addresses. Your calls to Weav are a different direction: send them to `api.weavpayments.com` by name, never by IP.
## Allow both
Add both addresses to your firewall, load balancer or web application firewall rules for your webhook URL. Weav may send from either one, so allowing only one makes some deliveries fail. A webhook your server doesn't answer with `2xx` is retried (see [Retries](https://docs.weavpayments.com/retries)).
> **Warning:** An address isn't proof that a webhook came from Weav. Always [verify the signature](https://docs.weavpayments.com/webhooks/verifying-signatures) too: the allowlist keeps others out of your endpoint, and the signature proves the payload is Weav's and unchanged.
## Changes
Changes to these addresses are announced in the [changelog](https://docs.weavpayments.com/changelog).
This list is for webhooks Weav sends you. To limit where your own API calls come from, use your key's [IP allowlist](https://docs.weavpayments.com/authentication#ip-allowlist).
---
# Channels
What each channel asks of your customer, and what you get back.
| Value | Name | The customer… | next_action | Expires after |
| --- | --- | --- | --- | --- |
| `QR_PH` | QR Ph | Scans a one-time QR with any QR Ph bank or e-wallet app. | `DISPLAY_QR` | 15 or 60 minutes |
| `GCASH` | GCash | Pays in GCash on the GCash checkout page. | `REDIRECT` | 10 minutes |
| `MAYA` | Pay with Maya | Pays with a Maya wallet on Maya’s checkout page. | `REDIRECT` | 15 minutes |
| `CARD` | Credit or debit card | Pays by card on the card provider’s checkout page. Each payment is screened for fraud. | `REDIRECT` | 60 minutes |
| `PAY_AND_GO` | Pay & Go | Pays cash at a Pay & Go kiosk: chooses Eplayment and enters the payment code. | `PAY_AT_COUNTER` | 48 hours |
| `PALAWANPAY` | PalawanPay | Pays cash at a Palawan counter: pays to Eplayment with the payment code. | `PAY_AT_COUNTER` | 48 hours |
| `BAYAD` | Bayad | Pays cash at a Bayad counter: pays to Eplayment with the payment code. | `PAY_AT_COUNTER` | 48 hours |
> **Note:** With a payment link (the default), these times start when your customer opens the link. An unopened link lasts 24 hours. In test mode, every payment lasts 15 minutes from when you create it (48 hours for a counter channel).
## Showing a QR Ph code
With a payment link (the default), Weav's page shows the code for you. If you ask for the raw string (`"next_action_format": "DIRECT"`), you render `next_action.qr_string` as a QR code yourself. QR Ph codes carry the QR Ph symbol in the center, laid out exactly like this. Bank and e-wallet apps expect it, so a code that's off may not scan:
(Figure: a sample QR Ph code. 29 × 29 modules of 8 px, a white quiet zone of 4 modules on every side, and the full-color QR Ph symbol in a white box at the center, 23.81% of the code's width.)
- Error correction level Q (M at minimum), black modules on white, whole pixels per module (no blurring), and a white quiet zone of at least 4 modules on every side.
- A white box in the exact center, 23.81% of the code's width (50 ÷ 210, measured without the quiet zone), with the full-color QR Ph symbol filling about 80% of the box. Don't recolor, invert, stretch or add effects to the symbol.
- Logo size = code width × 0.2381, rounded to a whole pixel; nudge it by 1 px if needed so (code width − logo size) is even, then offset it by half the difference on both axes. For example, a 210 px code gets a 50 px box at 80, 80.
- Scan-test on major e-wallet and banking apps, on iOS and Android, before you go live.
---
# Statuses
## Payment
```mermaid
flowchart TD
P[PENDING] --> F[FAILED]
P --> C[COMPLETED]
P --> X[CANCELLED]
F -. paid late .-> C
X -. paid late .-> C
C --> R[REFUNDED]
C --> V[REVERSED]
class P warn
class C ok
class F,V error
class X,R neutral
```
| Status | Meaning | Final |
| --- | --- | --- |
| `PENDING` | Waiting for the customer to pay. | No |
| `COMPLETED` | Paid. Partial refunds keep this status. | Can still move to reversed or refunded |
| `FAILED` | Expired or declined. See `failure.code`. | Almost: see below |
| `CANCELLED` | Canceled before the customer paid. | Almost: see below |
| `REVERSED` | Reversed after completion, for example a chargeback. | Yes |
| `REFUNDED` | Fully refunded. | Yes |
> **Note:** **A late payment still counts.** Sometimes a customer pays just as a payment expires or is canceled, or the provider's confirmation reaches Weav late. When the provider confirms the money arrived, a `FAILED` or `CANCELLED` payment becomes `COMPLETED`, its `failure` clears, the amount is credited to your balance, and you get a `PAYMENT_COMPLETED` webhook. Always act on the latest status, and be ready to fulfill (or refund) an order you had given up on. Weav checks with providers regularly, so this happens without anyone asking.
## Payment failure codes
| Code | Meaning |
| --- | --- |
| `EXPIRED` | The customer didn’t pay before `expires_at`. |
| `DECLINED` | The provider or card issuer declined the payment. |
| `PROVIDER_ERROR` | The provider couldn’t process the payment. |
## Saved card
```mermaid
flowchart LR
P[PENDING] --> A[ACTIVE]
P --> F[FAILED]
A --> R[REMOVED]
P -. removed .-> R
class P warn
class A ok
class F error
class R neutral
```
| Status | Meaning |
| --- | --- |
| `PENDING` | Waiting for your customer to save the card and their bank to confirm it. |
| `ACTIVE` | Saved. You can charge it. |
| `FAILED` | The bank's check failed, the save link ran out, or the payment it was saved with wasn't paid. |
| `REMOVED` | Removed, by you (`DELETE /v1/payment-methods/{id}`) or by Weav. It can't be charged again. |
See [Saved cards](https://docs.weavpayments.com/saved-cards).
## Withdrawal
```mermaid
flowchart LR
P[PENDING] --> S[FOR_SETTLEMENT]
S --> D[COMPLETED]
P --> F[FAILED]
S --> F
class P warn
class S info
class D ok
class F error
```
| Status | Meaning |
| --- | --- |
| `PENDING` | Requested and held from your balance. |
| `FOR_SETTLEMENT` | Accepted by the bank and on its way. |
| `COMPLETED` | Arrived in your bank account. |
| `FAILED` | Rejected. The amount is back in your balance. |
---
# Saved cards
Save your customer's card once, then charge it whenever you need to, with no customer present: subscriptions, top-ups, auto-debit. Your customer types their card on a Weav page, so card numbers never reach your systems.
> **Note:** Saving cards is off until Weav turns it on for your account. Weav asks for your PCI DSS evidence first. In test mode it always works, so you can build your integration now.
## 1. Save the card
Save it while your customer pays, or on its own.
### While they pay
Add `save_payment_method` and `customer.id` to a `CARD` payment:
```http
POST /v1/payments
{ "channel": "CARD", "currency": "PHP", "amount": 499.00, "external_reference_id": "SUB-USER-123-2026-10", "save_payment_method": true, "customer": { "id": "USER-123", "name": "Juan dela Cruz" }, "success_url": "https://shop.example/account/plan" }
```
Send your customer to `next_action.checkout_url`, Weav's payment page, as for any payment. There they type their card, agree that you may charge it for future payments, and confirm the payment with their bank. That one check also confirms the card: there's no ₱10.00 check.
```mermaid
sequenceDiagram
autonumber
actor Customer as Your customer
participant Server as Your server
participant Weav
participant Bank as Their bank
Server->>Weav: POST /v1/payments
CARD, save_payment_method, customer.id
Weav-->>Server: 201 PENDING, payment_method_id
next_action.checkout_url
Server-->>Customer: Sends them to checkout_url
Customer->>Weav: Types their card, agrees to future charges
Note over Customer,Weav: The card number never reaches your server
Weav->>Bank: Charges the card
Bank-->>Customer: Asks them to confirm it's them
Customer->>Bank: Confirms
alt Paid
Weav-->>Customer: Redirects to success_url
Weav->>Server: PAYMENT_COMPLETED webhook
Weav->>Server: PAYMENT_METHOD_SAVED webhook
the card is ACTIVE
Note over Server: Keep the card's id (PM…)
with your customer
else Not paid
Weav->>Server: PAYMENT_FAILED, then
PAYMENT_METHOD_FAILED webhooks
Note over Weav: The card isn't kept
end
```
The response's `payment_method_id` is the new saved card, `PENDING` until the payment is paid. Then you get two webhooks:
- `PAYMENT_COMPLETED`, then `PAYMENT_METHOD_SAVED`: the card is `ACTIVE`. Charge it next time with its `id` (step 2).
- If the payment isn't paid, the card isn't kept: `PAYMENT_FAILED`, then `PAYMENT_METHOD_FAILED`.
A [checkout](https://docs.weavpayments.com/reference/checkouts/create-checkout) takes the same `save_payment_method` and `customer.id`. If your customer pays by card, the card is saved the same way. Another way to pay saves nothing, so send `"channels": ["CARD"]` to take cards only.
### On its own
```http
POST /v1/payment-methods
{ "type": "CARD", "customer": { "id": "USER-123", "name": "Juan dela Cruz", "email": "juan@example.com" }, "success_url": "https://shop.example/account/cards" }
```
`customer.id` is your own ID for your customer. You get a payment method with `status: PENDING` and `next_action.checkout_url`. Send your customer there. On Weav's save page they:
1. Give their name, if you didn't send it.
2. Type their card and agree that you may charge it for future payments.
3. Confirm it with their bank. Their bank may show a ₱10.00 check that is canceled right after.
```mermaid
sequenceDiagram
autonumber
actor Customer as Your customer
participant Server as Your server
participant Weav
participant Bank as Their bank
Server->>Weav: POST /v1/payment-methods
CARD, customer.id
Weav-->>Server: 201 PENDING
next_action.checkout_url
Server-->>Customer: Sends them to checkout_url
Customer->>Weav: Their name if you didn't send it,
their card, and their agreement
Weav->>Bank: Checks the card
Bank-->>Customer: Asks them to confirm
(may show a ₱10.00 check)
Customer->>Bank: Confirms
alt The bank confirms it
Weav-->>Customer: Redirects to success_url
Weav->>Server: PAYMENT_METHOD_SAVED webhook
the card is ACTIVE
else The check fails, or the link isn't used in 24 hours
Weav->>Server: PAYMENT_METHOD_FAILED webhook
end
```
Then they go back to your `success_url` (or `cancel_url`), and you get a webhook:
- `PAYMENT_METHOD_SAVED`: it's `ACTIVE`. Keep its `id` (`PM…`) with your customer. Show `label` ("Visa •••• 1381") so they know which card it is.
- `PAYMENT_METHOD_FAILED`: the bank's check failed, or the link wasn't used within 24 hours.
## 2. Charge it
```http
POST /v1/payments
{ "payment_method_id": "PM8K2Q7X9KBAT4MZ3N", "currency": "PHP", "amount": 499.00, "external_reference_id": "SUB-USER-123-2026-11" }
```
No `channel`, no `customer`, no URLs. A saved card is usually paid at once: the response says `COMPLETED`, and you get `PAYMENT_COMPLETED`. If the card can't be charged, it's `FAILED`. Sometimes the bank asks your customer to confirm again: the payment stays `PENDING` with `next_action.type` `REDIRECT`, so send your customer to `checkout_url`.
```mermaid
sequenceDiagram
autonumber
participant Server as Your server
participant Weav
actor Customer as Your customer
Server->>Weav: POST /v1/payments
payment_method_id, amount, external_reference_id
alt Paid, as most charges are
Weav-->>Server: 201 COMPLETED
Weav->>Server: PAYMENT_COMPLETED webhook
else The bank wants your customer to confirm
Weav-->>Server: 201 PENDING
next_action.checkout_url
Server-->>Customer: Sends them the link
Customer->>Weav: Confirms with their bank
Weav->>Server: PAYMENT_COMPLETED webhook
else The card can't be charged
Weav-->>Server: 201 FAILED
Weav->>Server: PAYMENT_FAILED webhook
end
```
Retries are safe: send the same request with the same `external_reference_id` (see [Idempotency](https://docs.weavpayments.com/idempotency)).
## 3. List and remove
- `GET /v1/payment-methods?customer_id=USER-123`: your customer's saved cards.
- `GET /v1/payment-methods?expires_by=2026-12`: cards that expire by then. Each card has `exp_month` and `exp_year`, so you can ask for a new card in time.
- `GET /v1/payments?payment_method_id=PM8K2Q7X9KBAT4MZ3N`: every charge on a saved card.
- `DELETE /v1/payment-methods/{id}`: removes it, at the card processor too, so nobody can charge it again (`PAYMENT_METHOD_REMOVED`). Remove a card whenever your customer asks.
## Good to know
- **Your customer's agreement.** The save page records that your customer let you charge the card. Charge only what you told them you would (amounts and timing).
- **In a mobile app,** open the save page in the system browser or an in-app browser tab (`SFSafariViewController` on iOS, Custom Tabs on Android), not a WebView your app controls.
- **Test mode** has a test save page with three test cards: one that's paid, one that's declined, and one that expires this month. See [Test mode](https://docs.weavpayments.com/test-mode#test-saved-cards).
- **Subscriptions:** charge a saved card on your own schedule. See [Subscriptions](https://docs.weavpayments.com/subscriptions).
- Only cards for now (Visa, Mastercard, JCB).
---
# Subscriptions
Charge your customers every week, month or year with a saved card. You keep the plan, the price and the schedule in your own system. Weav saves the card and charges it whenever you ask, with no customer present.
> **Note:** Saving cards is off until Weav turns it on for your account. Weav asks for your PCI DSS evidence first. In test mode it always works, so you can build your integration now.
## 1. Save the card at sign-up
Charge the first period and save the card in one step. Add `save_payment_method` and `customer.id` (your own ID for the customer) to a `CARD` payment:
```http title="Request"
POST /v1/payments
{ "channel": "CARD", "currency": "PHP", "amount": 499.00, "external_reference_id": "SUB-USER-123-2026-11", "save_payment_method": true, "customer": { "id": "USER-123", "name": "Juan dela Cruz" }, "success_url": "https://shop.example.com/account/plan" }
```
A [checkout](https://docs.weavpayments.com/reference/checkouts/create-checkout) works too: send `"channels": ["CARD"]` so your customer can only pay by card. Your customer agrees to future charges on Weav's page and confirms the payment with their bank.
Then you get `PAYMENT_COMPLETED` and `PAYMENT_METHOD_SAVED`. Keep the saved card's `id` (`PM…`) with the subscription. To save a card without charging it, see [Saved cards](https://docs.weavpayments.com/saved-cards).
## 2. Charge on your schedule
Run your own job, for example once a day. For each subscription that's due, charge its card:
```http title="Request"
POST /v1/payments
{ "payment_method_id": "PM8K2Q7X9KBAT4MZ3N", "currency": "PHP", "amount": 499.00, "external_reference_id": "SUB-USER-123-2026-12", "description": "December plan" }
```
- **Use one `external_reference_id` per period.** If your job crashes and sends the same request again, Weav returns the same payment instead of charging twice (see [Idempotency](https://docs.weavpayments.com/idempotency)).
- **`COMPLETED`:** paid. Extend the subscription.
- **`PENDING` with `next_action.checkout_url`:** your customer's bank wants them to confirm this charge. Send them the link by email or in your app.
- **`FAILED`:** see step 3.
The same result comes as a webhook (`PAYMENT_COMPLETED` or `PAYMENT_FAILED`), so your job doesn't have to wait for it.
```mermaid
flowchart LR
C[Charge the
saved card] --> S{Status}
S -->|COMPLETED| OK[Extend the
subscription]
S -->|PENDING| L[Send your customer
checkout_url]
L -. paid .-> OK
S -->|FAILED| T{Tried a
few times?}
T -->|No| R[Try again a day later,
with a new reference]
R --> C
T -->|Yes| N[Ask for
another card]
```
## 3. When a charge fails
- **Try again later.** A new try is a new payment, so give it a new reference, such as `SUB-USER-123-2026-12-2`. The same reference returns the same failed payment. Leave at least a day between tries, and stop after a few.
- **Ask for another card.** Send your customer a payment with `save_payment_method` (step 1) for the amount due. It charges the new card and saves it. Then remove the old one.
## 4. Before a card expires
Saved cards carry their expiry (`card.exp_month`, `card.exp_year`). Once a month, list the cards that expire soon and ask those customers for a new card before a charge fails:
```http title="Request"
GET /v1/payment-methods?expires_by=2026-12
```
## 5. Keep track
- `GET /v1/payments?payment_method_id=PM8K2Q7X9KBAT4MZ3N`: every charge on a saved card, such as a subscription's history.
- `GET /v1/payment-methods?customer_id=USER-123`: a customer's saved cards.
## 6. When your customer cancels
Stop charging their card in your system. If they ask you to remove the card, `DELETE /v1/payment-methods/{id}` removes it at the card processor too, so nobody can charge it again.
## Good to know
- **Charge only what you told your customer.** They agreed to future charges when they saved the card. Tell them the amount and when you'll charge it, and tell them how to cancel.
- **Weav doesn't run the schedule.** There are no plans or invoices in Weav: you decide what to charge and when.
- **Test mode:** save Visa •••• 0002 to try a declined charge, and Mastercard •••• 4444 to try an expiring card. See [Test mode](https://docs.weavpayments.com/test-mode#test-saved-cards).
---
# Refunds
Refunds go through Weav support. There's no refund endpoint: you see each refund on the payment and get a webhook.
1. Email [support@weavpayments.com](https://www.weavpayments.com/support) with the payment's `reference_id` and the amount to return.
2. Weav returns the money to your customer through the payment's channel and records the refund.
3. The refunded amount comes off your available balance, and you get a `PAYMENT_REFUNDED` webhook.
## On the payment
| Field | Type | Description |
| --- | --- | --- |
| `amount_refunded` | `number` | Pesos refunded so far. |
| `refunds` | `array` | Each refund, oldest first: `reference_id` (`RF` plus 12 characters), `amount` in pesos, `provider_reference_id` (the channel’s refund reference, or null) and `refunded_at`. |
- Refunds can be partial. A partial refund keeps the payment at `COMPLETED`; once all of `amount_charged` (what your customer paid) is refunded, it moves to `REFUNDED`.
- Only a completed payment can be refunded, up to `amount_charged` less earlier refunds. A pending payment is canceled instead.
- Weav's fee for the payment isn't returned.
- Each refund also appears in your [ledger entries](https://docs.weavpayments.com/reference/balance/list-ledger-entries) as a `REFUND` entry.
---
# Chargebacks
A chargeback is when the customer's bank or card provider takes a payment back, for example after the customer disputes it. Weav records it once the provider has taken the money back.
- The payment moves to `REVERSED`, with `reversed_at` set.
- Weav takes back what wasn't refunded already. It comes off your available balance, and Weav's fee isn't returned.
- You get a `PAYMENT_REVERSED` webhook, and your Owners and Finance get an email.
- If your balance can't cover it, Weav takes what's there and your balance goes below zero by the rest. Withdrawals wait until incoming payments bring it back above zero.
## In the webhook
| Field | Type | Description |
| --- | --- | --- |
| `payment` | `object` | The payment, now `REVERSED`. |
| `reversal` | `object` | `reference_id` (`RV` plus 12 characters), `amount` in pesos, `provider_reference_id` (the provider’s chargeback reference, or null) and `reversed_at`. |
If you think a chargeback is wrong, email [support@weavpayments.com](https://www.weavpayments.com/support) with the payment's `reference_id` and any proof of delivery.
---
# API reference
Every endpoint, with its parameters, responses and code samples. Requests go to `https://api.weavpayments.com` with your API key as a Bearer token. See [Authentication](https://docs.weavpayments.com/authentication). Example values are samples, fees included: your fees are in your agreement with Weav.
## Checkouts
- [List checkouts](https://docs.weavpayments.com/reference/checkouts/list-checkouts): `GET /v1/checkouts`
- [Create a checkout](https://docs.weavpayments.com/reference/checkouts/create-checkout): `POST /v1/checkouts`
- [Retrieve a checkout](https://docs.weavpayments.com/reference/checkouts/retrieve-checkout): `GET /v1/checkouts/{reference_id}`
- [Cancel a checkout](https://docs.weavpayments.com/reference/checkouts/cancel-checkout): `POST /v1/checkouts/{reference_id}/cancel`
## Payments
- [List payments](https://docs.weavpayments.com/reference/payments/list-payments): `GET /v1/payments`
- [Create a payment](https://docs.weavpayments.com/reference/payments/create-payment): `POST /v1/payments`
- [Retrieve a payment](https://docs.weavpayments.com/reference/payments/retrieve-payment): `GET /v1/payments/{reference_id}`
- [Cancel a payment](https://docs.weavpayments.com/reference/payments/cancel-payment): `POST /v1/payments/{reference_id}/cancel`
## Payment methods
- [List saved payment methods](https://docs.weavpayments.com/reference/payment-methods/list-payment-methods): `GET /v1/payment-methods`
- [Save a payment method](https://docs.weavpayments.com/reference/payment-methods/create-payment-method): `POST /v1/payment-methods`
- [Retrieve a payment method](https://docs.weavpayments.com/reference/payment-methods/retrieve-payment-method): `GET /v1/payment-methods/{id}`
- [Remove a payment method](https://docs.weavpayments.com/reference/payment-methods/remove-payment-method): `DELETE /v1/payment-methods/{id}`
## Balance
- [Get your balance](https://docs.weavpayments.com/reference/balance/get-balance): `GET /v1/balance`
- [List ledger entries](https://docs.weavpayments.com/reference/balance/list-ledger-entries): `GET /v1/ledger-entries`
## Withdrawals
- [List withdrawals](https://docs.weavpayments.com/reference/withdrawals/list-withdrawals): `GET /v1/withdrawals`
- [Retrieve a withdrawal](https://docs.weavpayments.com/reference/withdrawals/retrieve-withdrawal): `GET /v1/withdrawals/{reference_id}`
## Channels
- [List channels](https://docs.weavpayments.com/reference/channels/list-channels): `GET /v1/channels`
## Webhooks
- [Repost a payment's latest webhook](https://docs.weavpayments.com/reference/webhooks/repost-payment-webhook): `POST /v1/payments/{reference_id}/webhooks/repost`
- [Get your webhook URL](https://docs.weavpayments.com/reference/webhooks/get-webhook-endpoint): `GET /v1/webhook-endpoint`
- [Set your webhook URL](https://docs.weavpayments.com/reference/webhooks/set-webhook-endpoint): `PUT /v1/webhook-endpoint`
- [Remove your webhook URL](https://docs.weavpayments.com/reference/webhooks/remove-webhook-endpoint): `DELETE /v1/webhook-endpoint`
## Try it
Each endpoint's page has **Try it**, next to the endpoint's path. It opens a playground: fill in the request on the left, and see it as cURL and Weav's answer on the right, with the status, time, size and headers. Press ⌘↵ (Ctrl+Enter) to send.
- **Test keys only.** It takes a `wv_test_` key and refuses a live one, and so does the API: a live key sent from a browser gets `403` with code `4030001`.
- **Your key stays in this tab.** It's kept in memory while you move between pages here, never saved, and gone when you reload or close the tab. **Forget it** clears it sooner. No cookies go with the request.
- **Copy it as cURL.** The cURL view uses `$WEAV_API_KEY`, never your key. Paste it into a terminal, or import it into Postman.
- **It's a real test-mode request.** A payment you create is a real test payment in your account, and its webhooks go to your test webhook URL.
- A key limited to an IP allowlist gets `403` here: your browser's address isn't on the list.
## For your tools
This reference is made from Weav's OpenAPI 3.1 file. Import [openapi.yaml](https://docs.weavpayments.com/openapi.yaml) or [openapi.json](https://docs.weavpayments.com/openapi.json) into Postman (**Import → Link**), Insomnia, Swagger UI or a code generator. For AI assistants: [llms.txt](https://docs.weavpayments.com/llms.txt) and [llms-full.txt](https://docs.weavpayments.com/llms-full.txt).
---
# Response codes
`code` is a 7-digit string. The first three digits match the HTTP status, the next two name the service, and the last two name the case.
## Success
| Code | HTTP | Meaning |
| --- | --- | --- |
| `2000000` | 200 | OK |
| `2010100` | 201 | Payment created |
| `2000101` | 200 | Payment canceled |
| `2010300` | 201 | Checkout created |
| `2000301` | 200 | Checkout canceled |
| `2010400` | 201 | Payment method created |
| `2000401` | 200 | Payment method removed |
| `2020200` | 202 | Webhook queued for repost |
## Errors
| Code | HTTP | Meaning |
| --- | --- | --- |
| `4000000` | 400 | The request body isn’t valid JSON. |
| `4010000` | 401 | The API key is missing, malformed, deleted or regenerated. |
| `4030000` | 403 | This key can’t make this request, for example from an address outside its IP allowlist. |
| `4030001` | 403 | A live key was sent from a browser page. Live keys belong on your server. |
| `4040000` | 404 | Not found in this mode for your account. |
| `4050000` | 405 | This method isn’t allowed on this path. |
| `4090001` | 409 | The `external_reference_id` (or Idempotency-Key) was already used with a different request. |
| `4090002` | 409 | The same request is still in progress. |
| `4090101` | 409 | The payment can’t be canceled: it’s no longer pending. |
| `4090102` | 409 | The payment can’t be canceled at the provider right now. |
| `4090201` | 409 | The webhook is still being sent. |
| `4090301` | 409 | The checkout can’t be canceled: it’s no longer open. |
| `4090302` | 409 | The checkout’s payment in progress can’t be canceled at the provider right now. |
| `4090401` | 409 | The saved card can’t be removed at the card processor right now. |
| `4220000` | 422 | Validation failed. `errors` lists each field, for example an amount outside your [payment limits](https://docs.weavpayments.com/reference/channels/list-channels), a reused `external_reference_id` or a private webhook URL. |
| `4220101` | 422 | The channel is off for your account. |
| `4220102` | 422 | The channel doesn’t accept that currency. |
| `4220103` | 422 | This payment method can’t be charged: it isn’t `ACTIVE`, or saving cards is off for your account. |
| `4220401` | 422 | Saving cards is off for your account. |
| `4290000` | 429 | Too many requests for your account. |
| `4290201` | 429 | This payment’s webhook was reposted less than 30 seconds ago. |
| `4290202` | 429 | Your account reposted 20 webhooks in this mode in the last minute. |
| `5000000` | 500 | Something went wrong on our side. |
| `5010100` | 501 | That channel isn’t live through the API yet. |
What to do for each: see [Errors](https://docs.weavpayments.com/errors#error-codes).
Service `00` is generic. Service `01` is payments. Service `02` is webhooks. Service `03` is checkouts. Once published, a code's meaning never changes.
---
# List checkouts
`GET https://api.weavpayments.com/v1/checkouts`
Your checkouts in this mode, newest first, with [cursor pagination](https://docs.weavpayments.com/pagination).
## Query parameters
- `per_page` (integer): Items per page, 25 by default; values above 100 become 100. At least 1. Default: 25.
- `cursor` (string): The `next_cursor` or `prev_cursor` from the previous page. Up to 1024 characters.
- `date_from` (string (date)): Start date (Asia/Manila), inclusive. Send with `date_to`.
- `date_to` (string (date)): End date (Asia/Manila), inclusive.
- `status` (string): Only checkouts with this status. `OPEN`, `COMPLETED`, `EXPIRED` or `CANCELLED`. One of: `OPEN`, `COMPLETED`, `EXPIRED`, `CANCELLED`.
- `external_reference_id` (string): Up to 64 characters.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Your checkouts in this mode, newest first.
What `data` holds:
- `checkouts` (array of objects)
- `reference_id` (string)
- `external_reference_id` (string | null)
- `type` (string): One of: `ORDER`, `AMOUNT`.
- `status` (string): One of: `OPEN`, `COMPLETED`, `EXPIRED`, `CANCELLED`.
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number): The total, in pesos.
- `description` (string | null)
- `items` (array of objects)
- `name` (string, required): Up to 255 characters.
- `quantity` (integer, required): At least 1. At most 9999.
- `unit_amount` (number, required): Pesos, as a JSON number.
- `amount` (number): `quantity` × `unit_amount`.
- `channels` (array of strings | null): One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `qr_ph_apps` (array of strings | null)
- `customer` (object)
- `id` (string | null): Your ID for the customer, with `save_payment_method`.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `save_payment_method` (boolean): A card payment also saves the card (the payment's `payment_method_id`).
- `checkout_url` (string (uri))
- `success_url` (string | null)
- `cancel_url` (string | null)
- `webhook_url` (string | null)
- `payment` (any): The payment that paid it.
- `expires_at` (string (date-time))
- `completed_at` (string (date-time) | null)
- `expired_at` (string (date-time) | null)
- `cancelled_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
- `pagination` (object)
- `per_page` (integer, required)
- `next_cursor` (string | null, required)
- `prev_cursor` (string | null, required)
- `is_last_page` (boolean, required)
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"checkouts": [
{
"reference_id": "CK4T8M2Q7X9KBA",
"external_reference_id": "ORD-10541",
"type": "ORDER",
"status": "COMPLETED",
"currency": "PHP",
"amount": 1250.0,
"description": null,
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.0,
"amount": 1000.0
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.0,
"amount": 250.0
}
],
"channels": null,
"qr_ph_apps": null,
"customer": {
"id": null,
"name": null,
"email": null,
"mobile_no": null
},
"save_payment_method": false,
"checkout_url": "https://pay.weavpayments.com/checkout/CK4T8M2Q7X9KBA",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart",
"webhook_url": null,
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"checkout_reference_id": "CK4T8M2Q7X9KBA",
"status": "COMPLETED",
"…": "…"
},
"expires_at": "2026-10-27T10:12:00+08:00",
"completed_at": "2026-10-26T10:15:40+08:00",
"expired_at": null,
"cancelled_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:15:41+08:00"
}
],
"pagination": {
"per_page": 25,
"next_cursor": "eyJpZCI6MTM0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
"prev_cursor": null,
"is_last_page": false
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/checkouts \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/checkouts';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/checkouts',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/checkouts');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/checkouts",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/checkouts", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/checkouts")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/checkouts";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Create a checkout
`POST https://api.weavpayments.com/v1/checkouts`
One call, one link. Send your customer to `checkout_url`: they choose how to pay (QR Ph, e-wallets, cards or cash at a counter), give their name if you didn't, and pay. Paid once. Send `items` (Weav adds them up) or one `amount`, never both.
Each channel your customer starts is a payment with `checkout_reference_id` set (its PAYMENT_* webhooks still come). The first payment paid completes the checkout (`CHECKOUT_COMPLETED`). Never treat a redirect to `success_url` as proof of payment.
## How it works
- Your customer chooses a channel. Weav starts a payment on it for the checkout’s total, the same as a payment you’d create yourself, with `checkout_reference_id` set. You get its `PAYMENT_*` webhooks as usual.
- QR Ph also shows as each bank and e-wallet app that pays it (UnionBank, BDO Pay, GrabPay and more), because many customers know their app better than QR Ph. Choosing one makes a QR Ph payment with `qr_ph_app` set: the page shows that app’s logo, colors and steps. Any QR Ph app can still pay it.
- They can change their mind: choosing another channel cancels the payment in progress (you get `PAYMENT_CANCELLED`) and starts a new one.
- When you pay the fee, every channel shows the same total. When your customer pays it (fees on top), each channel shows its own total, fee included.
- The first payment paid completes the checkout: `status` becomes `COMPLETED`, `payment` is that payment, and you get `CHECKOUT_COMPLETED`.
- Rarely, a customer pays a QR code and then pays again another way. The second payment is real money: it’s credited like any payment, and Weav refunds it with you.
- A checkout that runs out unpaid becomes `EXPIRED` (`CHECKOUT_EXPIRED`). A payment still in progress, such as a counter code good for 48 hours, keeps it open until that payment ends. If it’s paid, the checkout completes.
```mermaid
sequenceDiagram
autonumber
actor Customer as Your customer
participant Server as Your server
participant Weav
Server->>Weav: POST /v1/checkouts
Weav-->>Server: 201 OPEN, checkout_url
Server-->>Customer: Sends them to checkout_url
Customer->>Weav: Chooses a channel
Note over Weav: Starts a payment on it,
with checkout_reference_id
opt They choose another channel
Weav->>Server: PAYMENT_CANCELLED webhook
for the first payment
Note over Weav: Starts a new payment
end
Customer->>Weav: Pays
Weav->>Server: PAYMENT_COMPLETED and
CHECKOUT_COMPLETED webhooks
```
## Statuses
```mermaid
flowchart LR
O[OPEN] --> C[COMPLETED]
O --> E[EXPIRED]
O --> X[CANCELLED]
E -. paid late .-> C
class O warn
class C ok
class E error
class X neutral
```
| Status | Meaning |
| --- | --- |
| `OPEN` | Waiting for your customer to pay. |
| `COMPLETED` | Paid. Final. |
| `EXPIRED` | Ran out unpaid. A late payment still completes it. |
| `CANCELLED` | You canceled it. Final. |
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
- `Idempotency-Key` (string): Optional. A unique value per new checkout, such as a UUID. Retrying with the same key and body returns the stored response; a different body returns 409. Without it, your `external_reference_id` is the key (a checkout with neither is never matched to an earlier one). Up to 255 characters.
## Body
JSON, with `Content-Type: application/json`.
- `items` (array of objects | null): An order, up to 50 items. Each has `name` (up to 255 characters), `quantity` (1 to 9,999) and `unit_amount` (pesos, a JSON number). Weav multiplies and adds them up; the total must be within your payment limits. Up to 50 items.
- `name` (string, required): Up to 255 characters.
- `quantity` (integer, required): At least 1. At most 9999.
- `unit_amount` (number, required): Pesos, as a JSON number.
- `amount` (number): `quantity` × `unit_amount`.
- `amount` (number | null): Pesos. Within your payment limits.
- `currency` (string | null): One of: `PHP`.
- `description` (string | null): What it's for, up to 255 characters. Your customer sees it. Up to 255 characters.
- `external_reference_id` (string | null): Unique across your checkouts in this mode when given. Up to 64 characters.
- `channels` (array of strings | null): Only these channels. Default every channel that's on. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `qr_ph_apps` (array of strings | null): QR Ph only as these bank or e-wallet apps, for example a UnionBank link. Needs QR Ph among the channels. One of: `BDO`, `CHINABANK`, `GOTYME`, `LANDBANK`, `METROBANK`, `PNB`, `RCBC`, `ROBINSONS_BANK`, `SECURITY_BANK`, `UNIONBANK`, `GRABPAY`, `SHOPEEPAY`.
- `save_payment_method` (boolean | null): If your customer pays by card, also saves the card for later charges (auto-debit). Send `customer.id`. They agree to future charges on Weav's payment page, and their bank's check for this payment also confirms the card. Another way to pay saves nothing: send `channels: [CARD]` to take cards only. The payment's `payment_method_id` is the saved card: you get `PAYMENT_COMPLETED`, then `PAYMENT_METHOD_SAVED`. If that payment isn't paid, the card isn't kept (`PAYMENT_METHOD_FAILED`). Off until Weav turns on saving cards for your account (422 `4220401`).
- `customer` (object | null): All optional. With a name, your customer isn't asked for one; with an email, they get a receipt.
- `id` (string): With `save_payment_method` only, and required then: your own ID for this customer. Their saved cards are listed by it (`GET /v1/payment-methods?customer_id=`). Up to 64 characters. Pattern: ^[A-Za-z0-9._:@-]+$.
- `name` (string | null): Up to 255 characters.
- `email` (string (email) | null)
- `mobile_no` (string | null)
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null): HTTPS. Where your customer goes if they leave, or the checkout expires or is canceled. Up to 2048 characters.
- `webhook_url` (string (uri) | null)
- `expires_in_minutes` (integer | null): How long it stays open, 5 to 10,080 (7 days). Default 1,440 (24 hours). At least 5. At most 10080. Default: 1440.
## Responses
### 201 Created
Checkout created (code 2010300).
What `data` holds:
- `checkout` (object)
- `reference_id` (string)
- `external_reference_id` (string | null)
- `type` (string): One of: `ORDER`, `AMOUNT`.
- `status` (string): One of: `OPEN`, `COMPLETED`, `EXPIRED`, `CANCELLED`.
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number): The total, in pesos.
- `description` (string | null)
- `items` (array of objects)
- `name` (string, required): Up to 255 characters.
- `quantity` (integer, required): At least 1. At most 9999.
- `unit_amount` (number, required): Pesos, as a JSON number.
- `amount` (number): `quantity` × `unit_amount`.
- `channels` (array of strings | null): One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `qr_ph_apps` (array of strings | null)
- `customer` (object)
- `id` (string | null): Your ID for the customer, with `save_payment_method`.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `save_payment_method` (boolean): A card payment also saves the card (the payment's `payment_method_id`).
- `checkout_url` (string (uri))
- `success_url` (string | null)
- `cancel_url` (string | null)
- `webhook_url` (string | null)
- `payment` (any): The payment that paid it.
- `expires_at` (string (date-time))
- `completed_at` (string (date-time) | null)
- `expired_at` (string (date-time) | null)
- `cancelled_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
```json
{
"code": "2010300",
"success": true,
"message": "Checkout created",
"data": {
"checkout": {
"reference_id": "CK4T8M2Q7X9KBA",
"external_reference_id": "ORD-10541",
"type": "ORDER",
"status": "OPEN",
"currency": "PHP",
"amount": 1250.0,
"description": null,
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.0,
"amount": 1000.0
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.0,
"amount": 250.0
}
],
"channels": null,
"qr_ph_apps": null,
"customer": {
"id": null,
"name": null,
"email": null,
"mobile_no": null
},
"save_payment_method": false,
"checkout_url": "https://pay.weavpayments.com/checkout/CK4T8M2Q7X9KBA",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart",
"webhook_url": null,
"payment": null,
"expires_at": "2026-10-27T10:12:00+08:00",
"completed_at": null,
"expired_at": null,
"cancelled_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 409 Conflict
The request conflicts with the current state, e.g. an Idempotency-Key reused with a different body (4090001), a request still in progress (4090002), a payment that can't be canceled (4090101) or can't be canceled at the provider yet (4090102, retry shortly), or a webhook still being sent (4090201).
```json
{
"code": "4090001",
"success": false,
"message": "This Idempotency-Key was already used with a different request."
}
```
### 422 Unprocessable Content
One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).
```json
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"channel": [
"The channel field is required."
]
}
}
```
### 429 Too Many Requests
Rate limited (code 4290000). Wait for `Retry-After` seconds.
```json
{
"code": "4290000",
"success": false,
"message": "Too many requests. Wait, then retry."
}
```
## Examples
### An order
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/checkouts \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.00
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.00
}
],
"external_reference_id": "ORD-10541",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart"
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/checkouts';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
name: 'Canvas tote bag',
quantity: 1,
unit_amount: 1000.0,
},
{
name: 'Canvas Strap',
quantity: 2,
unit_amount: 125.0,
},
],
external_reference_id: 'ORD-10541',
success_url: 'https://shop.example.com/orders/ORD-10541/thanks',
cancel_url: 'https://shop.example.com/cart',
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/checkouts',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
items: [
{
name: 'Canvas tote bag',
quantity: 1,
unit_amount: 1000.0,
},
{
name: 'Canvas Strap',
quantity: 2,
unit_amount: 125.0,
},
],
external_reference_id: 'ORD-10541',
success_url: 'https://shop.example.com/orders/ORD-10541/thanks',
cancel_url: 'https://shop.example.com/cart',
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/checkouts', [
'items' => [
[
'name' => 'Canvas tote bag',
'quantity' => 1,
'unit_amount' => 1000.00,
],
[
'name' => 'Canvas Strap',
'quantity' => 2,
'unit_amount' => 125.00,
],
],
'external_reference_id' => 'ORD-10541',
'success_url' => 'https://shop.example.com/orders/ORD-10541/thanks',
'cancel_url' => 'https://shop.example.com/cart',
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/checkouts",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.00,
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.00,
},
],
"external_reference_id": "ORD-10541",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.00
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.00
}
],
"external_reference_id": "ORD-10541",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart"
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/checkouts", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.00
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.00
}
],
"external_reference_id": "ORD-10541",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart"
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/checkouts")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/checkouts";
String body =
"""
{
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.00
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.00
}
],
"external_reference_id": "ORD-10541",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart"
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
Response (201):
```json
{
"code": "2010300",
"success": true,
"message": "Checkout created",
"data": {
"checkout": {
"reference_id": "CK4T8M2Q7X9KBA",
"external_reference_id": "ORD-10541",
"type": "ORDER",
"status": "OPEN",
"currency": "PHP",
"amount": 1250.0,
"description": null,
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.0,
"amount": 1000.0
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.0,
"amount": 250.0
}
],
"channels": null,
"qr_ph_apps": null,
"customer": {
"id": null,
"name": null,
"email": null,
"mobile_no": null
},
"save_payment_method": false,
"checkout_url": "https://pay.weavpayments.com/checkout/CK4T8M2Q7X9KBA",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart",
"webhook_url": null,
"payment": null,
"expires_at": "2026-10-27T10:12:00+08:00",
"completed_at": null,
"expired_at": null,
"cancelled_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
### A plan, saving the card
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/checkouts \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"name": "Monthly plan",
"quantity": 1,
"unit_amount": 499.00
}
],
"external_reference_id": "SUB-USER-123-START",
"channels": [
"CARD"
],
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz"
},
"success_url": "https://shop.example.com/account/plan"
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/checkouts';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
name: 'Monthly plan',
quantity: 1,
unit_amount: 499.0,
},
],
external_reference_id: 'SUB-USER-123-START',
channels: ['CARD'],
save_payment_method: true,
customer: {
id: 'USER-123',
name: 'Juan dela Cruz',
},
success_url: 'https://shop.example.com/account/plan',
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/checkouts',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
items: [
{
name: 'Monthly plan',
quantity: 1,
unit_amount: 499.0,
},
],
external_reference_id: 'SUB-USER-123-START',
channels: ['CARD'],
save_payment_method: true,
customer: {
id: 'USER-123',
name: 'Juan dela Cruz',
},
success_url: 'https://shop.example.com/account/plan',
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/checkouts', [
'items' => [
[
'name' => 'Monthly plan',
'quantity' => 1,
'unit_amount' => 499.00,
],
],
'external_reference_id' => 'SUB-USER-123-START',
'channels' => [
'CARD',
],
'save_payment_method' => true,
'customer' => [
'id' => 'USER-123',
'name' => 'Juan dela Cruz',
],
'success_url' => 'https://shop.example.com/account/plan',
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/checkouts",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"items": [
{
"name": "Monthly plan",
"quantity": 1,
"unit_amount": 499.00,
},
],
"external_reference_id": "SUB-USER-123-START",
"channels": [
"CARD",
],
"save_payment_method": True,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
},
"success_url": "https://shop.example.com/account/plan",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"items": [
{
"name": "Monthly plan",
"quantity": 1,
"unit_amount": 499.00
}
],
"external_reference_id": "SUB-USER-123-START",
"channels": [
"CARD"
],
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz"
},
"success_url": "https://shop.example.com/account/plan"
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/checkouts", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"items": [
{
"name": "Monthly plan",
"quantity": 1,
"unit_amount": 499.00
}
],
"external_reference_id": "SUB-USER-123-START",
"channels": [
"CARD"
],
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz"
},
"success_url": "https://shop.example.com/account/plan"
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/checkouts")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/checkouts";
String body =
"""
{
"items": [
{
"name": "Monthly plan",
"quantity": 1,
"unit_amount": 499.00
}
],
"external_reference_id": "SUB-USER-123-START",
"channels": [
"CARD"
],
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz"
},
"success_url": "https://shop.example.com/account/plan"
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Retrieve a checkout
`GET https://api.weavpayments.com/v1/checkouts/{reference_id}`
Reads one checkout. Use it to confirm it was paid, for example when your customer lands on your `success_url`.
A checkout that belongs to another merchant, or to the other mode, returns `404`.
## Path parameters
- `reference_id` (string, required)
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
The checkout.
What `data` holds:
- `checkout` (object)
- `reference_id` (string)
- `external_reference_id` (string | null)
- `type` (string): One of: `ORDER`, `AMOUNT`.
- `status` (string): One of: `OPEN`, `COMPLETED`, `EXPIRED`, `CANCELLED`.
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number): The total, in pesos.
- `description` (string | null)
- `items` (array of objects)
- `name` (string, required): Up to 255 characters.
- `quantity` (integer, required): At least 1. At most 9999.
- `unit_amount` (number, required): Pesos, as a JSON number.
- `amount` (number): `quantity` × `unit_amount`.
- `channels` (array of strings | null): One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `qr_ph_apps` (array of strings | null)
- `customer` (object)
- `id` (string | null): Your ID for the customer, with `save_payment_method`.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `save_payment_method` (boolean): A card payment also saves the card (the payment's `payment_method_id`).
- `checkout_url` (string (uri))
- `success_url` (string | null)
- `cancel_url` (string | null)
- `webhook_url` (string | null)
- `payment` (any): The payment that paid it.
- `expires_at` (string (date-time))
- `completed_at` (string (date-time) | null)
- `expired_at` (string (date-time) | null)
- `cancelled_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"checkout": {
"reference_id": "CK4T8M2Q7X9KBA",
"external_reference_id": "ORD-10541",
"type": "ORDER",
"status": "COMPLETED",
"currency": "PHP",
"amount": 1250.0,
"description": null,
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.0,
"amount": 1000.0
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.0,
"amount": 250.0
}
],
"channels": null,
"qr_ph_apps": null,
"customer": {
"id": null,
"name": null,
"email": null,
"mobile_no": null
},
"save_payment_method": false,
"checkout_url": "https://pay.weavpayments.com/checkout/CK4T8M2Q7X9KBA",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart",
"webhook_url": null,
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"checkout_reference_id": "CK4T8M2Q7X9KBA",
"status": "COMPLETED",
"…": "…"
},
"expires_at": "2026-10-27T10:12:00+08:00",
"completed_at": "2026-10-26T10:15:40+08:00",
"expired_at": null,
"cancelled_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:15:41+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Cancel a checkout
`POST https://api.weavpayments.com/v1/checkouts/{reference_id}/cancel`
Cancels an `OPEN` checkout, and the payment in progress with it. 409 `4090301` when it isn't open; 409 `4090302` when the provider can't cancel the payment in progress yet (retry shortly).
## Path parameters
- `reference_id` (string, required)
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Checkout canceled (code 2000301).
What `data` holds:
- `checkout` (object)
- `reference_id` (string)
- `external_reference_id` (string | null)
- `type` (string): One of: `ORDER`, `AMOUNT`.
- `status` (string): One of: `OPEN`, `COMPLETED`, `EXPIRED`, `CANCELLED`.
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number): The total, in pesos.
- `description` (string | null)
- `items` (array of objects)
- `name` (string, required): Up to 255 characters.
- `quantity` (integer, required): At least 1. At most 9999.
- `unit_amount` (number, required): Pesos, as a JSON number.
- `amount` (number): `quantity` × `unit_amount`.
- `channels` (array of strings | null): One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `qr_ph_apps` (array of strings | null)
- `customer` (object)
- `id` (string | null): Your ID for the customer, with `save_payment_method`.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `save_payment_method` (boolean): A card payment also saves the card (the payment's `payment_method_id`).
- `checkout_url` (string (uri))
- `success_url` (string | null)
- `cancel_url` (string | null)
- `webhook_url` (string | null)
- `payment` (any): The payment that paid it.
- `expires_at` (string (date-time))
- `completed_at` (string (date-time) | null)
- `expired_at` (string (date-time) | null)
- `cancelled_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
```json
{
"code": "2000301",
"success": true,
"message": "Checkout canceled",
"data": {
"checkout": {
"reference_id": "CK4T8M2Q7X9KBA",
"external_reference_id": "ORD-10541",
"type": "ORDER",
"status": "CANCELLED",
"currency": "PHP",
"amount": 1250.0,
"description": null,
"items": [
{
"name": "Canvas tote bag",
"quantity": 1,
"unit_amount": 1000.0,
"amount": 1000.0
},
{
"name": "Canvas Strap",
"quantity": 2,
"unit_amount": 125.0,
"amount": 250.0
}
],
"channels": null,
"qr_ph_apps": null,
"customer": {
"id": null,
"name": null,
"email": null,
"mobile_no": null
},
"save_payment_method": false,
"checkout_url": "https://pay.weavpayments.com/checkout/CK4T8M2Q7X9KBA",
"success_url": "https://shop.example.com/orders/ORD-10541/thanks",
"cancel_url": "https://shop.example.com/cart",
"webhook_url": null,
"payment": null,
"expires_at": "2026-10-27T10:12:00+08:00",
"completed_at": null,
"expired_at": null,
"cancelled_at": "2026-10-26T11:02:00+08:00",
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T11:02:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
### 409 Conflict
The request conflicts with the current state, e.g. an Idempotency-Key reused with a different body (4090001), a request still in progress (4090002), a payment that can't be canceled (4090101) or can't be canceled at the provider yet (4090102, retry shortly), or a webhook still being sent (4090201).
```json
{
"code": "4090001",
"success": false,
"message": "This Idempotency-Key was already used with a different request."
}
```
## Examples
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/checkouts/CK4T8M2Q7X9KBA/cancel";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# List payments
`GET https://api.weavpayments.com/v1/payments`
Newest first. Filter by status, channel, your reference or creation date.
Uses [cursor pagination](https://docs.weavpayments.com/pagination). Each item is a full payment object; the example response leaves some fields out to keep it short.
## Query parameters
- `per_page` (integer): Items per page, 25 by default; values above 100 become 100. At least 1. Default: 25.
- `cursor` (string): The `next_cursor` or `prev_cursor` from the previous page. Up to 1024 characters.
- `date_from` (string (date)): Start date (Asia/Manila), inclusive. Send with `date_to`.
- `date_to` (string (date)): End date (Asia/Manila), inclusive.
- `status` (string): Only payments with this status. One of: `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED`, `REFUNDED`.
- `channel` (string): Only payments on this channel. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `external_reference_id` (string): Your own order or invoice reference, exact match. Up to 64 characters.
- `reference` (string): Exact match on `reference_id`, `provider_reference_id` or `channel_reference_id`, or a number in references (for example the QR Ph invoice number your customer gives you). A number in references can match more than one payment. Up to 64 characters.
- `payment_method_id` (string): Only the payments charged to this saved payment method, or saving it. A subscription's history. Up to 32 characters.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
A page of payments.
What `data` holds:
- `payments` (array of objects, required)
- `reference_id` (string)
- `external_reference_id` (string | null): Yours. For a checkout's payment, the checkout's (null when it has none).
- `checkout_reference_id` (string | null): The checkout this payment was started from, if any.
- `payment_method_id` (string | null): The saved payment method it was charged to, or the one it saves the card to (`save_payment_method`), if any.
- `provider_reference_id` (string | null): The provider's transaction ID, once the provider has one.
- `channel_reference_id` (string | null): A reference from the channel that is unique to this payment, once the payment completes. Null for channels whose numbers can repeat; those are in references.
- `references` (object): The provider's other numbers for this payment, keyed by type: `invoice_number` (the 6-digit QR Ph invoice number), `receipt_number` (Maya) or `transaction_id` (GCash). Saved once the provider confirms the payment; {} until then. They can repeat across payments: search with them, never identify a payment by one alone.
- `invoice_number` (string)
- `receipt_number` (string)
- `transaction_id` (string)
- `channel` (string): One of `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `customer` (object): The person paying, as you sent it. The email and mobile number turn null once Weav's retention period ends.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number)
- `fee` (number | null): Null until a rate applies.
- `total` (number | null): `amount` plus `fee`, calculated by Weav. Null while `fee` is null.
- `fee_bearer` (string): `MERCHANT`, the fee comes out of `amount` (default). `PAYER` (fees on top), the customer is charged `amount` + `fee` and you receive the full `amount`. One of: `MERCHANT`, `PAYER`.
- `amount_charged` (number): What the customer is charged. `amount`, or `total` when `fee_bearer` is `PAYER`.
- `amount_refunded` (number): The total refunded so far. The payment turns `REFUNDED` once it equals `amount`.
- `refunds` (array of objects): Refunds Weav recorded for this payment, oldest first. Refunds are made by Weav support on request.
- `reference_id` (string)
- `amount` (number)
- `provider_reference_id` (string | null)
- `refunded_at` (string (date-time))
- `status` (string): `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED` or `REFUNDED`. See [Statuses](https://docs.weavpayments.com/statuses). One of: `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED`, `REFUNDED`.
- `description` (string | null): As you sent it.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null): Where this payment's webhooks go, when not your webhook URL.
- `next_action` (object): What your customer does next. `type` is `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`, or null once the payment is final. `qr_string`, `checkout_url` and `payment_code` are each null unless they apply. With `HOSTED` (the default), `type` is `REDIRECT` and `checkout_url` is Weav's payment page (the test checkout with a test key), for every channel. For QR Ph, `qr_ph_app` is the app you chose, and `instructions` lists the how-to-pay steps (`title`, `description`) when you asked for `DIRECT`.
- `type` (string | null): One of: `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`.
- `qr_string` (string | null): QR Ph payload to render as a QR code: error correction Q (M at minimum), a white quiet zone of at least 4 modules, and the full-color QR Ph symbol on a white box 23.81% of the code width (quiet zone excluded) in the exact center.
- `checkout_url` (string (uri) | null)
- `payment_code` (string | null): For over-the-counter channels.
- `qr_ph_app` (string | null): QR Ph only. The bank or e-wallet app the steps are tailored to.
- `instructions` (array of objects | null): QR Ph with `next_action_format` `DIRECT` only. How-to-pay steps to show the customer.
- `title` (string)
- `description` (string)
- `test_checkout_url` (string (uri) | null): Test mode only: the test checkout page, where you pay, decline or let this payment expire, for every channel (a counter payment with `DIRECT` has no other way to finish). Anyone with the link can open it. Null in live mode.
- `failure` (object | null): `code` and `message` when the payment failed. Codes: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `code` (string): One of: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `message` (string)
- `expires_at` (string (date-time)): When an unpaid payment fails (`failure.code` `EXPIRED`).
- `completed_at` (string (date-time) | null): When the customer paid, as the provider reports it.
- `failed_at` (string (date-time) | null): When the payment failed.
- `cancelled_at` (string (date-time) | null): When the payment was canceled.
- `reversed_at` (string (date-time) | null): When the payment was reversed.
- `created_at` (string (date-time)): When Weav created the payment.
- `updated_at` (string (date-time)): When the payment last changed.
- `pagination` (object, required)
- `per_page` (integer, required)
- `next_cursor` (string | null, required)
- `prev_cursor` (string | null, required)
- `is_last_page` (boolean, required)
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"payments": [
{
"reference_id": "PY7K3Q9M2X4TBA",
"external_reference_id": "ORD-10541",
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.0,
"status": "COMPLETED",
"completed_at": "2026-10-26T10:15:40+08:00",
"created_at": "2026-10-26T10:12:00+08:00"
}
],
"pagination": {
"per_page": 25,
"next_cursor": "eyJpZCI6MTM0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
"prev_cursor": null,
"is_last_page": false
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 422 Unprocessable Content
One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).
```json
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"channel": [
"The channel field is required."
]
}
}
```
### 429 Too Many Requests
Rate limited (code 4290000). Wait for `Retry-After` seconds.
```json
{
"code": "4290000",
"success": false,
"message": "Too many requests. Wait, then retry."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/payments \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/payments',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/payments');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/payments", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/payments")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Create a payment
`POST https://api.weavpayments.com/v1/payments`
Creates a payment on one channel and returns what the customer does next (`next_action`): scan a QR code, go to a checkout page, or pay at a counter with a payment code.
With a test key, the checkout page is Weav's test checkout, where you pay, decline or expire the payment. Live payments run through the providers.
> **Warning:** Never treat a redirect to your `success_url` as proof of payment. Wait for the `PAYMENT_COMPLETED` webhook, or read the payment with `GET`.
## What your customer does next
### Payment link (the default)
By default every payment comes back as a **payment link**: send your customer to `next_action.checkout_url`, Weav's payment page. Weav sets the payment up with the provider when your customer opens it, so `expires_at` starts as the link's lifetime (24 hours) and becomes the provider's window (for example 15 minutes for a QR Ph code) from that moment. For QR Ph the page shows the code and how-to-pay steps for their bank or e-wallet app (set `qr_ph_app` to start with one). The QR Ph and Pay & Go examples ask for the raw QR string or payment code instead, with `"next_action_format": "DIRECT"`.
**Your customer's name.** Weav needs it on every payment, for anti-money-laundering and fraud checks. Send it in `customer.name` if you have it. If you don't, leave `customer` out: Weav's payment page asks your customer before anything is set up. With `DIRECT` there's no Weav page, so `customer.name` is required.
**Webhooks for this payment.** They go to your webhook URL. To send one payment's webhooks somewhere else, set `webhook_url` on it.
### QR Ph, shown by you
With the raw string, render `qr_string` as a QR code and show the steps in `next_action.instructions`. Any QR Ph bank or e-wallet app can scan it. A QR Ph code expires 15 or 60 minutes after it's made, depending on your account's QR Ph provider; `expires_at` has the exact time.
Lay it out the QR Ph way, with the QR Ph symbol in the center: bank and e-wallet apps expect it, so a code that's off may not scan. See [Showing a QR Ph code](https://docs.weavpayments.com/channels#showing-a-qr-ph-code) for the exact layout and a sample.
### Redirect: GCash, Maya and cards
Send your customer to `next_action.checkout_url`. After they pay or cancel there, they come back through Weav first: Weav checks the result with the provider right away, then sends them to your `success_url` (paid) or `cancel_url` (failed, expired or canceled). Both are optional: without them, your customer stays on Weav's result page. If the provider is still confirming, they see a Weav page that updates by itself. Always rely on the webhook or `GET /v1/payments/{reference_id}` for the final status, not on which URL they land on.
A GCash payment expires 10 minutes after it's created, Pay with Maya after 15 minutes and a card payment after 60.
> **Note:** **Mobile apps.** The GCash page has an "Open in GCash" button that uses a `gcash://` link. If you load `checkout_url` in a WebView, hand `gcash://` links to the operating system. On Android, use `shouldOverrideUrlLoading`. On iOS, use `decidePolicyForNavigationAction`. Otherwise the customer sees an unknown-scheme error.
### Over the counter
Give the customer the `payment_code`. At a Pay & Go kiosk they choose **Eplayment** on the screen, enter the code and pay in cash. With `PALAWANPAY` and `BAYAD` they go to a Palawan or Bayad counter, pay to **Eplayment** and show the code. Counter payments expire after 48 hours.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
- `Idempotency-Key` (string): Optional. A unique value per new payment, such as a UUID. Retrying with the same key and body returns the stored response; a different body returns 409. Without it, your `external_reference_id` is the key. Up to 255 characters.
## Body
JSON, with `Content-Type: application/json`.
- `channel` (string): Required, unless you send `payment_method_id`. One of `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`. The channel must be on for your account, or the response is 422 (4220101). One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `payment_method_id` (string | null): Charge a saved payment method instead (auto-debit), with no payer present. Leave out `channel` and `customer`. Only an `ACTIVE` one, or 422 `4220103`. A card is usually paid at once (`COMPLETED`); if the bank asks your customer to confirm again, it's `PENDING` with `next_action.type` `REDIRECT`.
- `save_payment_method` (boolean | null): `CARD` only. Also saves your customer's card for later charges (auto-debit), so you don't need `POST /v1/payment-methods` first. Send `customer.id`. On Weav's payment page, your customer agrees to future charges and pays; their bank's check (3DS) for this payment also confirms the card. The response's `payment_method_id` is the new method, `PENDING` until the payment is paid. You get `PAYMENT_COMPLETED`, then `PAYMENT_METHOD_SAVED`. If the payment isn't paid, the card isn't kept (`PAYMENT_METHOD_FAILED`). Off until Weav turns on saving cards for your account (422 `4220401`).
- `currency` (string, required): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number, required): Major unit, up to the currency's decimals (2 for PHP). Must be within your payment limits (GET /v1/channels; by default ₱0.01 to ₱50,000.00), or the response is 422. More than 0.
- `external_reference_id` (string, required): Your order or invoice reference. Unique across your payments in this mode (test and live are separate); reusing one returns 422. Up to 64 characters.
- `description` (string | null): Up to 255 characters. Your customer sees it on their emailed receipt (sent when you give their email). Up to 255 characters.
- `next_action_format` (string | null): `HOSTED` (default, every channel) returns `next_action.checkout_url`, Weav's payment page; the payment is set up with the provider when the customer opens it. `DIRECT` sets it up right away and returns the raw `qr_string` (with instructions), the provider's `checkout_url` or the `payment_code`. One of: `HOSTED`, `DIRECT`.
- `qr_ph_app` (string | null): QR Ph only. Tailors the how-to-pay steps to one bank or e-wallet app; any QR Ph app can still pay. One of: `BDO`, `CHINABANK`, `GOTYME`, `LANDBANK`, `METROBANK`, `PNB`, `RCBC`, `ROBINSONS_BANK`, `SECURITY_BANK`, `UNIONBANK`, `GRABPAY`, `SHOPEEPAY`.
- `customer` (object | null): The person paying. Stored encrypted; with an email, Weav emails them a receipt once the payment is paid. Weav needs the name for every payment (anti-money-laundering and fraud checks). Leave it out with `HOSTED` and Weav's payment page asks your customer before they pay; with `DIRECT` there's no Weav page, so `name` is required.
- `id` (string): With `save_payment_method` only, and required then: your own ID for this customer. Their saved cards are listed by it (`GET /v1/payment-methods?customer_id=`). Up to 64 characters. Pattern: ^[A-Za-z0-9._:@-]+$.
- `name` (string): One field; not split into first and last name. Up to 255 characters.
- `email` (string (email) | null): Up to 255 characters.
- `mobile_no` (string | null): A Philippine mobile number. Up to 20 characters.
- `webhook_url` (string | null): Optional. All of this payment's webhooks go here instead of your webhook URL, whatever its events list says. Public HTTPS only, signed with your signing key. Up to 2048 characters.
- `success_url` (string (uri) | null): Optional, HTTPS. Where your customer goes once the payment is paid. Without it, they stay on Weav's result page. Never treat this redirect as proof of payment. Up to 2048 characters.
- `cancel_url` (string (uri) | null): Optional, HTTPS. Where your customer goes if they leave without paying (the payment failed, expired or was canceled). Without it, they stay on Weav's result page. Up to 2048 characters.
## Responses
### 201 Created
Payment created (code 2010100).
What `data` holds:
- `payment` (object)
- `reference_id` (string)
- `external_reference_id` (string | null): Yours. For a checkout's payment, the checkout's (null when it has none).
- `checkout_reference_id` (string | null): The checkout this payment was started from, if any.
- `payment_method_id` (string | null): The saved payment method it was charged to, or the one it saves the card to (`save_payment_method`), if any.
- `provider_reference_id` (string | null): The provider's transaction ID, once the provider has one.
- `channel_reference_id` (string | null): A reference from the channel that is unique to this payment, once the payment completes. Null for channels whose numbers can repeat; those are in references.
- `references` (object): The provider's other numbers for this payment, keyed by type: `invoice_number` (the 6-digit QR Ph invoice number), `receipt_number` (Maya) or `transaction_id` (GCash). Saved once the provider confirms the payment; {} until then. They can repeat across payments: search with them, never identify a payment by one alone.
- `invoice_number` (string)
- `receipt_number` (string)
- `transaction_id` (string)
- `channel` (string): One of `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `customer` (object): The person paying, as you sent it. The email and mobile number turn null once Weav's retention period ends.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number)
- `fee` (number | null): Null until a rate applies.
- `total` (number | null): `amount` plus `fee`, calculated by Weav. Null while `fee` is null.
- `fee_bearer` (string): `MERCHANT`, the fee comes out of `amount` (default). `PAYER` (fees on top), the customer is charged `amount` + `fee` and you receive the full `amount`. One of: `MERCHANT`, `PAYER`.
- `amount_charged` (number): What the customer is charged. `amount`, or `total` when `fee_bearer` is `PAYER`.
- `amount_refunded` (number): The total refunded so far. The payment turns `REFUNDED` once it equals `amount`.
- `refunds` (array of objects): Refunds Weav recorded for this payment, oldest first. Refunds are made by Weav support on request.
- `reference_id` (string)
- `amount` (number)
- `provider_reference_id` (string | null)
- `refunded_at` (string (date-time))
- `status` (string): `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED` or `REFUNDED`. See [Statuses](https://docs.weavpayments.com/statuses). One of: `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED`, `REFUNDED`.
- `description` (string | null): As you sent it.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null): Where this payment's webhooks go, when not your webhook URL.
- `next_action` (object): What your customer does next. `type` is `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`, or null once the payment is final. `qr_string`, `checkout_url` and `payment_code` are each null unless they apply. With `HOSTED` (the default), `type` is `REDIRECT` and `checkout_url` is Weav's payment page (the test checkout with a test key), for every channel. For QR Ph, `qr_ph_app` is the app you chose, and `instructions` lists the how-to-pay steps (`title`, `description`) when you asked for `DIRECT`.
- `type` (string | null): One of: `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`.
- `qr_string` (string | null): QR Ph payload to render as a QR code: error correction Q (M at minimum), a white quiet zone of at least 4 modules, and the full-color QR Ph symbol on a white box 23.81% of the code width (quiet zone excluded) in the exact center.
- `checkout_url` (string (uri) | null)
- `payment_code` (string | null): For over-the-counter channels.
- `qr_ph_app` (string | null): QR Ph only. The bank or e-wallet app the steps are tailored to.
- `instructions` (array of objects | null): QR Ph with `next_action_format` `DIRECT` only. How-to-pay steps to show the customer.
- `title` (string)
- `description` (string)
- `test_checkout_url` (string (uri) | null): Test mode only: the test checkout page, where you pay, decline or let this payment expire, for every channel (a counter payment with `DIRECT` has no other way to finish). Anyone with the link can open it. Null in live mode.
- `failure` (object | null): `code` and `message` when the payment failed. Codes: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `code` (string): One of: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `message` (string)
- `expires_at` (string (date-time)): When an unpaid payment fails (`failure.code` `EXPIRED`).
- `completed_at` (string (date-time) | null): When the customer paid, as the provider reports it.
- `failed_at` (string (date-time) | null): When the payment failed.
- `cancelled_at` (string (date-time) | null): When the payment was canceled.
- `reversed_at` (string (date-time) | null): When the payment was reversed.
- `created_at` (string (date-time)): When Weav created the payment.
- `updated_at` (string (date-time)): When the payment last changed.
```json
{
"code": "2010100",
"success": true,
"message": "Payment created",
"data": {
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"external_reference_id": "ORD-10541",
"checkout_reference_id": null,
"payment_method_id": null,
"provider_reference_id": null,
"channel_reference_id": null,
"references": {},
"channel": "QR_PH",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"currency": "PHP",
"amount": 1250.0,
"fee": 31.25,
"total": 1281.25,
"fee_bearer": "MERCHANT",
"amount_charged": 1250.0,
"amount_refunded": 0.0,
"refunds": [],
"status": "PENDING",
"description": "Order ORD-10541",
"success_url": null,
"cancel_url": null,
"webhook_url": null,
"next_action": {
"type": "DISPLAY_QR",
"qr_string": "00020101021228…6304A1B2",
"checkout_url": null,
"payment_code": null,
"qr_ph_app": null,
"instructions": [
{
"title": "Open your bank or e-wallet app",
"description": "Any app that supports QR Ph works."
},
{
"title": "Scan the QR code",
"description": "Use the app's QR scanner on the code on your screen, or upload a saved copy."
},
{
"title": "Confirm the payment",
"description": "Check the amount and the merchant's name, then confirm."
}
]
},
"test_checkout_url": null,
"failure": null,
"expires_at": "2026-10-26T10:27:00+08:00",
"completed_at": null,
"failed_at": null,
"cancelled_at": null,
"reversed_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 409 Conflict
The request conflicts with the current state, e.g. an Idempotency-Key reused with a different body (4090001), a request still in progress (4090002), a payment that can't be canceled (4090101) or can't be canceled at the provider yet (4090102, retry shortly), or a webhook still being sent (4090201).
```json
{
"code": "4090001",
"success": false,
"message": "This Idempotency-Key was already used with a different request."
}
```
### 422 Unprocessable Content
One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).
```json
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"channel": [
"The channel field is required."
]
}
}
```
### 429 Too Many Requests
Rate limited (code 4290000). Wait for `Retry-After` seconds.
```json
{
"code": "4290000",
"success": false,
"message": "Too many requests. Wait, then retry."
}
```
### 501 Not Implemented
This channel isn't live for your account yet (code 5010100); use a test key.
```json
{
"code": "5010100",
"success": false,
"message": "Live payments aren't available through the API yet. Use a test key, or contact support@weavpayments.com."
}
```
## Examples
### QR Ph
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10541",
"description": "Order ORD-10541",
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: 'QR_PH',
currency: 'PHP',
amount: 1250.0,
external_reference_id: 'ORD-10541',
description: 'Order ORD-10541',
next_action_format: 'DIRECT',
customer: {
name: 'Juan dela Cruz',
email: null,
mobile_no: null,
},
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
channel: 'QR_PH',
currency: 'PHP',
amount: 1250.0,
external_reference_id: 'ORD-10541',
description: 'Order ORD-10541',
next_action_format: 'DIRECT',
customer: {
name: 'Juan dela Cruz',
email: null,
mobile_no: null,
},
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments', [
'channel' => 'QR_PH',
'currency' => 'PHP',
'amount' => 1250.00,
'external_reference_id' => 'ORD-10541',
'description' => 'Order ORD-10541',
'next_action_format' => 'DIRECT',
'customer' => [
'name' => 'Juan dela Cruz',
'email' => null,
'mobile_no' => null,
],
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10541",
"description": "Order ORD-10541",
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": None,
"mobile_no": None,
},
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10541",
"description": "Order ORD-10541",
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10541",
"description": "Order ORD-10541",
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments";
String body =
"""
{
"channel": "QR_PH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10541",
"description": "Order ORD-10541",
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
Response (201):
```json
{
"code": "2010100",
"success": true,
"message": "Payment created",
"data": {
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"external_reference_id": "ORD-10541",
"checkout_reference_id": null,
"payment_method_id": null,
"provider_reference_id": null,
"channel_reference_id": null,
"references": {},
"channel": "QR_PH",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"currency": "PHP",
"amount": 1250.0,
"fee": 31.25,
"total": 1281.25,
"fee_bearer": "MERCHANT",
"amount_charged": 1250.0,
"amount_refunded": 0.0,
"refunds": [],
"status": "PENDING",
"description": "Order ORD-10541",
"success_url": null,
"cancel_url": null,
"webhook_url": null,
"next_action": {
"type": "DISPLAY_QR",
"qr_string": "00020101021228…6304A1B2",
"checkout_url": null,
"payment_code": null,
"qr_ph_app": null,
"instructions": [
{
"title": "Open your bank or e-wallet app",
"description": "Any app that supports QR Ph works."
},
{
"title": "Scan the QR code",
"description": "Use the app's QR scanner on the code on your screen, or upload a saved copy."
},
{
"title": "Confirm the payment",
"description": "Check the amount and the merchant's name, then confirm."
}
]
},
"test_checkout_url": null,
"failure": null,
"expires_at": "2026-10-26T10:27:00+08:00",
"completed_at": null,
"failed_at": null,
"cancelled_at": null,
"reversed_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
### GCash
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "GCASH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10542",
"description": "Order ORD-10542",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"success_url": "https://shop.example/orders/10542/done",
"cancel_url": "https://shop.example/orders/10542/retry"
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: 'GCASH',
currency: 'PHP',
amount: 1250.0,
external_reference_id: 'ORD-10542',
description: 'Order ORD-10542',
customer: {
name: 'Juan dela Cruz',
email: null,
mobile_no: null,
},
success_url: 'https://shop.example/orders/10542/done',
cancel_url: 'https://shop.example/orders/10542/retry',
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
channel: 'GCASH',
currency: 'PHP',
amount: 1250.0,
external_reference_id: 'ORD-10542',
description: 'Order ORD-10542',
customer: {
name: 'Juan dela Cruz',
email: null,
mobile_no: null,
},
success_url: 'https://shop.example/orders/10542/done',
cancel_url: 'https://shop.example/orders/10542/retry',
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments', [
'channel' => 'GCASH',
'currency' => 'PHP',
'amount' => 1250.00,
'external_reference_id' => 'ORD-10542',
'description' => 'Order ORD-10542',
'customer' => [
'name' => 'Juan dela Cruz',
'email' => null,
'mobile_no' => null,
],
'success_url' => 'https://shop.example/orders/10542/done',
'cancel_url' => 'https://shop.example/orders/10542/retry',
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"channel": "GCASH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10542",
"description": "Order ORD-10542",
"customer": {
"name": "Juan dela Cruz",
"email": None,
"mobile_no": None,
},
"success_url": "https://shop.example/orders/10542/done",
"cancel_url": "https://shop.example/orders/10542/retry",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"channel": "GCASH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10542",
"description": "Order ORD-10542",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"success_url": "https://shop.example/orders/10542/done",
"cancel_url": "https://shop.example/orders/10542/retry"
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"channel": "GCASH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10542",
"description": "Order ORD-10542",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"success_url": "https://shop.example/orders/10542/done",
"cancel_url": "https://shop.example/orders/10542/retry"
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments";
String body =
"""
{
"channel": "GCASH",
"currency": "PHP",
"amount": 1250.00,
"external_reference_id": "ORD-10542",
"description": "Order ORD-10542",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"success_url": "https://shop.example/orders/10542/done",
"cancel_url": "https://shop.example/orders/10542/retry"
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
Response (201):
```json
{
"code": "2010100",
"success": true,
"message": "Payment created",
"data": {
"payment": {
"reference_id": "PY3T8N1W6R2QCD",
"external_reference_id": "ORD-10542",
"checkout_reference_id": null,
"payment_method_id": null,
"provider_reference_id": null,
"channel_reference_id": null,
"references": {},
"channel": "GCASH",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"currency": "PHP",
"amount": 1250.0,
"fee": 31.25,
"total": 1281.25,
"fee_bearer": "MERCHANT",
"amount_charged": 1250.0,
"amount_refunded": 0.0,
"refunds": [],
"status": "PENDING",
"description": "Order ORD-10542",
"success_url": "https://shop.example/orders/10542/done",
"cancel_url": "https://shop.example/orders/10542/retry",
"webhook_url": null,
"next_action": {
"type": "REDIRECT",
"qr_string": null,
"checkout_url": "https://pay.weavpayments.com/pay/PY3T8N1W6R2QCD",
"payment_code": null,
"qr_ph_app": null,
"instructions": null
},
"test_checkout_url": null,
"failure": null,
"expires_at": "2026-10-27T10:12:00+08:00",
"completed_at": null,
"failed_at": null,
"cancelled_at": null,
"reversed_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
### A saved card (auto-debit)
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payment_method_id": "PM8K2Q7X9KBAT4MZ3N",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-11",
"description": "November plan"
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
payment_method_id: 'PM8K2Q7X9KBAT4MZ3N',
currency: 'PHP',
amount: 499.0,
external_reference_id: 'SUB-USER-123-2026-11',
description: 'November plan',
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
payment_method_id: 'PM8K2Q7X9KBAT4MZ3N',
currency: 'PHP',
amount: 499.0,
external_reference_id: 'SUB-USER-123-2026-11',
description: 'November plan',
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments', [
'payment_method_id' => 'PM8K2Q7X9KBAT4MZ3N',
'currency' => 'PHP',
'amount' => 499.00,
'external_reference_id' => 'SUB-USER-123-2026-11',
'description' => 'November plan',
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"payment_method_id": "PM8K2Q7X9KBAT4MZ3N",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-11",
"description": "November plan",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"payment_method_id": "PM8K2Q7X9KBAT4MZ3N",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-11",
"description": "November plan"
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"payment_method_id": "PM8K2Q7X9KBAT4MZ3N",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-11",
"description": "November plan"
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments";
String body =
"""
{
"payment_method_id": "PM8K2Q7X9KBAT4MZ3N",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-11",
"description": "November plan"
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
### Pay by card and save it
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "CARD",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-10",
"description": "October plan",
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/plan",
"cancel_url": "https://shop.example/account/plan"
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: 'CARD',
currency: 'PHP',
amount: 499.0,
external_reference_id: 'SUB-USER-123-2026-10',
description: 'October plan',
save_payment_method: true,
customer: {
id: 'USER-123',
name: 'Juan dela Cruz',
email: 'juan@example.com',
},
success_url: 'https://shop.example/account/plan',
cancel_url: 'https://shop.example/account/plan',
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
channel: 'CARD',
currency: 'PHP',
amount: 499.0,
external_reference_id: 'SUB-USER-123-2026-10',
description: 'October plan',
save_payment_method: true,
customer: {
id: 'USER-123',
name: 'Juan dela Cruz',
email: 'juan@example.com',
},
success_url: 'https://shop.example/account/plan',
cancel_url: 'https://shop.example/account/plan',
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments', [
'channel' => 'CARD',
'currency' => 'PHP',
'amount' => 499.00,
'external_reference_id' => 'SUB-USER-123-2026-10',
'description' => 'October plan',
'save_payment_method' => true,
'customer' => [
'id' => 'USER-123',
'name' => 'Juan dela Cruz',
'email' => 'juan@example.com',
],
'success_url' => 'https://shop.example/account/plan',
'cancel_url' => 'https://shop.example/account/plan',
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"channel": "CARD",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-10",
"description": "October plan",
"save_payment_method": True,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com",
},
"success_url": "https://shop.example/account/plan",
"cancel_url": "https://shop.example/account/plan",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"channel": "CARD",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-10",
"description": "October plan",
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/plan",
"cancel_url": "https://shop.example/account/plan"
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"channel": "CARD",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-10",
"description": "October plan",
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/plan",
"cancel_url": "https://shop.example/account/plan"
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments";
String body =
"""
{
"channel": "CARD",
"currency": "PHP",
"amount": 499.00,
"external_reference_id": "SUB-USER-123-2026-10",
"description": "October plan",
"save_payment_method": true,
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/plan",
"cancel_url": "https://shop.example/account/plan"
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
### Pay & Go
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "PAY_AND_GO",
"currency": "PHP",
"amount": 3500.00,
"external_reference_id": "ORD-10543",
"description": null,
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: 'PAY_AND_GO',
currency: 'PHP',
amount: 3500.0,
external_reference_id: 'ORD-10543',
description: null,
next_action_format: 'DIRECT',
customer: {
name: 'Juan dela Cruz',
email: null,
mobile_no: null,
},
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
channel: 'PAY_AND_GO',
currency: 'PHP',
amount: 3500.0,
external_reference_id: 'ORD-10543',
description: null,
next_action_format: 'DIRECT',
customer: {
name: 'Juan dela Cruz',
email: null,
mobile_no: null,
},
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments', [
'channel' => 'PAY_AND_GO',
'currency' => 'PHP',
'amount' => 3500.00,
'external_reference_id' => 'ORD-10543',
'description' => null,
'next_action_format' => 'DIRECT',
'customer' => [
'name' => 'Juan dela Cruz',
'email' => null,
'mobile_no' => null,
],
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"channel": "PAY_AND_GO",
"currency": "PHP",
"amount": 3500.00,
"external_reference_id": "ORD-10543",
"description": None,
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": None,
"mobile_no": None,
},
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"channel": "PAY_AND_GO",
"currency": "PHP",
"amount": 3500.00,
"external_reference_id": "ORD-10543",
"description": null,
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"channel": "PAY_AND_GO",
"currency": "PHP",
"amount": 3500.00,
"external_reference_id": "ORD-10543",
"description": null,
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments";
String body =
"""
{
"channel": "PAY_AND_GO",
"currency": "PHP",
"amount": 3500.00,
"external_reference_id": "ORD-10543",
"description": null,
"next_action_format": "DIRECT",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
}
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
Response (201):
```json
{
"code": "2010100",
"success": true,
"message": "Payment created",
"data": {
"payment": {
"reference_id": "PY9M4C2X7K1VHE",
"external_reference_id": "ORD-10543",
"checkout_reference_id": null,
"payment_method_id": null,
"provider_reference_id": null,
"channel_reference_id": null,
"references": {},
"channel": "PAY_AND_GO",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"currency": "PHP",
"amount": 3500.0,
"fee": 87.5,
"total": 3587.5,
"fee_bearer": "MERCHANT",
"amount_charged": 3500.0,
"amount_refunded": 0.0,
"refunds": [],
"status": "PENDING",
"description": null,
"success_url": null,
"cancel_url": null,
"webhook_url": null,
"next_action": {
"type": "PAY_AT_COUNTER",
"qr_string": null,
"checkout_url": null,
"payment_code": "EP7K2M4Q9X1TB3A",
"qr_ph_app": null,
"instructions": null
},
"test_checkout_url": null,
"failure": null,
"expires_at": "2026-10-28T10:12:00+08:00",
"completed_at": null,
"failed_at": null,
"cancelled_at": null,
"reversed_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:12:00+08:00"
}
}
}
```
---
# Retrieve a payment
`GET https://api.weavpayments.com/v1/payments/{reference_id}`
Reads one payment. Use it to confirm a status, for example after your customer returns from a checkout page.
A payment that belongs to another merchant, or to the other mode, returns `404`.
## Path parameters
- `reference_id` (string, required): Weav's payment reference. Pattern: ^PY[0-9A-Z]{12}$.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
The payment.
What `data` holds:
- `payment` (object)
- `reference_id` (string)
- `external_reference_id` (string | null): Yours. For a checkout's payment, the checkout's (null when it has none).
- `checkout_reference_id` (string | null): The checkout this payment was started from, if any.
- `payment_method_id` (string | null): The saved payment method it was charged to, or the one it saves the card to (`save_payment_method`), if any.
- `provider_reference_id` (string | null): The provider's transaction ID, once the provider has one.
- `channel_reference_id` (string | null): A reference from the channel that is unique to this payment, once the payment completes. Null for channels whose numbers can repeat; those are in references.
- `references` (object): The provider's other numbers for this payment, keyed by type: `invoice_number` (the 6-digit QR Ph invoice number), `receipt_number` (Maya) or `transaction_id` (GCash). Saved once the provider confirms the payment; {} until then. They can repeat across payments: search with them, never identify a payment by one alone.
- `invoice_number` (string)
- `receipt_number` (string)
- `transaction_id` (string)
- `channel` (string): One of `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `customer` (object): The person paying, as you sent it. The email and mobile number turn null once Weav's retention period ends.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number)
- `fee` (number | null): Null until a rate applies.
- `total` (number | null): `amount` plus `fee`, calculated by Weav. Null while `fee` is null.
- `fee_bearer` (string): `MERCHANT`, the fee comes out of `amount` (default). `PAYER` (fees on top), the customer is charged `amount` + `fee` and you receive the full `amount`. One of: `MERCHANT`, `PAYER`.
- `amount_charged` (number): What the customer is charged. `amount`, or `total` when `fee_bearer` is `PAYER`.
- `amount_refunded` (number): The total refunded so far. The payment turns `REFUNDED` once it equals `amount`.
- `refunds` (array of objects): Refunds Weav recorded for this payment, oldest first. Refunds are made by Weav support on request.
- `reference_id` (string)
- `amount` (number)
- `provider_reference_id` (string | null)
- `refunded_at` (string (date-time))
- `status` (string): `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED` or `REFUNDED`. See [Statuses](https://docs.weavpayments.com/statuses). One of: `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED`, `REFUNDED`.
- `description` (string | null): As you sent it.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null): Where this payment's webhooks go, when not your webhook URL.
- `next_action` (object): What your customer does next. `type` is `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`, or null once the payment is final. `qr_string`, `checkout_url` and `payment_code` are each null unless they apply. With `HOSTED` (the default), `type` is `REDIRECT` and `checkout_url` is Weav's payment page (the test checkout with a test key), for every channel. For QR Ph, `qr_ph_app` is the app you chose, and `instructions` lists the how-to-pay steps (`title`, `description`) when you asked for `DIRECT`.
- `type` (string | null): One of: `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`.
- `qr_string` (string | null): QR Ph payload to render as a QR code: error correction Q (M at minimum), a white quiet zone of at least 4 modules, and the full-color QR Ph symbol on a white box 23.81% of the code width (quiet zone excluded) in the exact center.
- `checkout_url` (string (uri) | null)
- `payment_code` (string | null): For over-the-counter channels.
- `qr_ph_app` (string | null): QR Ph only. The bank or e-wallet app the steps are tailored to.
- `instructions` (array of objects | null): QR Ph with `next_action_format` `DIRECT` only. How-to-pay steps to show the customer.
- `title` (string)
- `description` (string)
- `test_checkout_url` (string (uri) | null): Test mode only: the test checkout page, where you pay, decline or let this payment expire, for every channel (a counter payment with `DIRECT` has no other way to finish). Anyone with the link can open it. Null in live mode.
- `failure` (object | null): `code` and `message` when the payment failed. Codes: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `code` (string): One of: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `message` (string)
- `expires_at` (string (date-time)): When an unpaid payment fails (`failure.code` `EXPIRED`).
- `completed_at` (string (date-time) | null): When the customer paid, as the provider reports it.
- `failed_at` (string (date-time) | null): When the payment failed.
- `cancelled_at` (string (date-time) | null): When the payment was canceled.
- `reversed_at` (string (date-time) | null): When the payment was reversed.
- `created_at` (string (date-time)): When Weav created the payment.
- `updated_at` (string (date-time)): When the payment last changed.
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"payment": {
"reference_id": "PY7K3Q9M2X4TBA",
"external_reference_id": "ORD-10541",
"checkout_reference_id": null,
"payment_method_id": null,
"provider_reference_id": null,
"channel_reference_id": null,
"references": {},
"channel": "QR_PH",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"currency": "PHP",
"amount": 1250.0,
"fee": 31.25,
"total": 1281.25,
"fee_bearer": "MERCHANT",
"amount_charged": 1250.0,
"amount_refunded": 0.0,
"refunds": [],
"status": "COMPLETED",
"description": "Order ORD-10541",
"success_url": null,
"cancel_url": null,
"webhook_url": null,
"next_action": {
"type": null,
"qr_string": null,
"checkout_url": null,
"payment_code": null,
"qr_ph_app": null,
"instructions": null
},
"test_checkout_url": null,
"failure": null,
"expires_at": "2026-10-26T10:27:00+08:00",
"completed_at": "2026-10-26T10:15:40+08:00",
"failed_at": null,
"cancelled_at": null,
"reversed_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T10:15:41+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Cancel a payment
`POST https://api.weavpayments.com/v1/payments/{reference_id}/cancel`
Cancels a payment that's still `PENDING`, so the customer can no longer pay it. Proposed.
Any status other than `PENDING` returns `409` with code `4090101`. A completed payment needs a [refund](https://docs.weavpayments.com/refunds) instead.
## Path parameters
- `reference_id` (string, required): Weav's payment reference. Pattern: ^PY[0-9A-Z]{12}$.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Payment canceled (code 2000101).
What `data` holds:
- `payment` (object)
- `reference_id` (string)
- `external_reference_id` (string | null): Yours. For a checkout's payment, the checkout's (null when it has none).
- `checkout_reference_id` (string | null): The checkout this payment was started from, if any.
- `payment_method_id` (string | null): The saved payment method it was charged to, or the one it saves the card to (`save_payment_method`), if any.
- `provider_reference_id` (string | null): The provider's transaction ID, once the provider has one.
- `channel_reference_id` (string | null): A reference from the channel that is unique to this payment, once the payment completes. Null for channels whose numbers can repeat; those are in references.
- `references` (object): The provider's other numbers for this payment, keyed by type: `invoice_number` (the 6-digit QR Ph invoice number), `receipt_number` (Maya) or `transaction_id` (GCash). Saved once the provider confirms the payment; {} until then. They can repeat across payments: search with them, never identify a payment by one alone.
- `invoice_number` (string)
- `receipt_number` (string)
- `transaction_id` (string)
- `channel` (string): One of `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `customer` (object): The person paying, as you sent it. The email and mobile number turn null once Weav's retention period ends.
- `name` (string | null)
- `email` (string | null)
- `mobile_no` (string | null)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number)
- `fee` (number | null): Null until a rate applies.
- `total` (number | null): `amount` plus `fee`, calculated by Weav. Null while `fee` is null.
- `fee_bearer` (string): `MERCHANT`, the fee comes out of `amount` (default). `PAYER` (fees on top), the customer is charged `amount` + `fee` and you receive the full `amount`. One of: `MERCHANT`, `PAYER`.
- `amount_charged` (number): What the customer is charged. `amount`, or `total` when `fee_bearer` is `PAYER`.
- `amount_refunded` (number): The total refunded so far. The payment turns `REFUNDED` once it equals `amount`.
- `refunds` (array of objects): Refunds Weav recorded for this payment, oldest first. Refunds are made by Weav support on request.
- `reference_id` (string)
- `amount` (number)
- `provider_reference_id` (string | null)
- `refunded_at` (string (date-time))
- `status` (string): `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED` or `REFUNDED`. See [Statuses](https://docs.weavpayments.com/statuses). One of: `PENDING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REVERSED`, `REFUNDED`.
- `description` (string | null): As you sent it.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null): Where this payment's webhooks go, when not your webhook URL.
- `next_action` (object): What your customer does next. `type` is `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`, or null once the payment is final. `qr_string`, `checkout_url` and `payment_code` are each null unless they apply. With `HOSTED` (the default), `type` is `REDIRECT` and `checkout_url` is Weav's payment page (the test checkout with a test key), for every channel. For QR Ph, `qr_ph_app` is the app you chose, and `instructions` lists the how-to-pay steps (`title`, `description`) when you asked for `DIRECT`.
- `type` (string | null): One of: `DISPLAY_QR`, `REDIRECT`, `PAY_AT_COUNTER`.
- `qr_string` (string | null): QR Ph payload to render as a QR code: error correction Q (M at minimum), a white quiet zone of at least 4 modules, and the full-color QR Ph symbol on a white box 23.81% of the code width (quiet zone excluded) in the exact center.
- `checkout_url` (string (uri) | null)
- `payment_code` (string | null): For over-the-counter channels.
- `qr_ph_app` (string | null): QR Ph only. The bank or e-wallet app the steps are tailored to.
- `instructions` (array of objects | null): QR Ph with `next_action_format` `DIRECT` only. How-to-pay steps to show the customer.
- `title` (string)
- `description` (string)
- `test_checkout_url` (string (uri) | null): Test mode only: the test checkout page, where you pay, decline or let this payment expire, for every channel (a counter payment with `DIRECT` has no other way to finish). Anyone with the link can open it. Null in live mode.
- `failure` (object | null): `code` and `message` when the payment failed. Codes: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `code` (string): One of: `EXPIRED`, `DECLINED`, `PROVIDER_ERROR`.
- `message` (string)
- `expires_at` (string (date-time)): When an unpaid payment fails (`failure.code` `EXPIRED`).
- `completed_at` (string (date-time) | null): When the customer paid, as the provider reports it.
- `failed_at` (string (date-time) | null): When the payment failed.
- `cancelled_at` (string (date-time) | null): When the payment was canceled.
- `reversed_at` (string (date-time) | null): When the payment was reversed.
- `created_at` (string (date-time)): When Weav created the payment.
- `updated_at` (string (date-time)): When the payment last changed.
```json
{
"code": "2000101",
"success": true,
"message": "Payment canceled",
"data": {
"payment": {
"reference_id": "PY9M4C2X7K1VHE",
"external_reference_id": "ORD-10543",
"checkout_reference_id": null,
"payment_method_id": null,
"provider_reference_id": null,
"channel_reference_id": null,
"references": {},
"channel": "PAY_AND_GO",
"customer": {
"name": "Juan dela Cruz",
"email": null,
"mobile_no": null
},
"currency": "PHP",
"amount": 3500.0,
"fee": 87.5,
"total": 3587.5,
"fee_bearer": "MERCHANT",
"amount_charged": 3500.0,
"amount_refunded": 0.0,
"refunds": [],
"status": "CANCELLED",
"description": null,
"success_url": null,
"cancel_url": null,
"webhook_url": null,
"next_action": {
"type": null,
"qr_string": null,
"checkout_url": null,
"payment_code": null,
"qr_ph_app": null,
"instructions": null
},
"test_checkout_url": null,
"failure": null,
"expires_at": "2026-10-28T10:12:00+08:00",
"completed_at": null,
"failed_at": null,
"cancelled_at": "2026-10-26T11:02:00+08:00",
"reversed_at": null,
"created_at": "2026-10-26T10:12:00+08:00",
"updated_at": "2026-10-26T11:02:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
### 409 Conflict
The payment isn't pending, so it can't be canceled (code 4090101), or the provider can't cancel it yet (code 4090102, retry shortly).
```json
{
"code": "4090101",
"success": false,
"message": "Only a pending payment can be canceled."
}
```
### 501 Not Implemented
This channel isn't live for your account yet (code 5010100); use a test key.
```json
{
"code": "5010100",
"success": false,
"message": "Live payments aren't available through the API yet. Use a test key, or contact support@weavpayments.com."
}
```
## Examples
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/cancel";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# List saved payment methods
`GET https://api.weavpayments.com/v1/payment-methods`
Newest first. Filter by your customer's ID to show their saved cards.
## Query parameters
- `per_page` (integer): Items per page, 25 by default; values above 100 become 100. At least 1. Default: 25.
- `cursor` (string): The `next_cursor` or `prev_cursor` from the previous page. Up to 1024 characters.
- `date_from` (string (date)): Start date (Asia/Manila), inclusive. Send with `date_to`.
- `date_to` (string (date)): End date (Asia/Manila), inclusive.
- `customer_id` (string): Your own ID for the customer, exact match. Up to 64 characters.
- `status` (string): `PENDING` until your customer saves it and their bank confirms it; `ACTIVE` (chargeable); `FAILED` when that never happens; `REMOVED`. One of: `PENDING`, `ACTIVE`, `FAILED`, `REMOVED`.
- `expires_by` (string): Only cards that expire in this month or earlier (`YYYY-MM`). Run it each month to ask those customers for a new card. Pattern: ^\d{4}-\d{2}$.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
A page of saved payment methods.
What `data` holds:
- `payment_methods` (array of objects)
- `id` (string)
- `type` (string): One of: `CARD`.
- `status` (string): `PENDING` until your customer saves it and their bank confirms it; `ACTIVE` (chargeable); `FAILED` when that never happens; `REMOVED`. One of: `PENDING`, `ACTIVE`, `FAILED`, `REMOVED`.
- `customer` (object)
- `id` (string)
- `name` (string | null)
- `email` (string | null)
- `card` (object | null): Once saved. Never the card number.
- `brand` (string): From the card processor. One of: `VISA`, `MASTERCARD`, `JCB`.
- `last4` (string)
- `exp_month` (integer | null): The expiry month your customer typed. Ask them for a new card before it passes (`expires_by` on the list). At least 1. At most 12.
- `exp_year` (integer | null): Four digits.
- `label` (string | null): To show your customer.
- `next_action` (object)
- `type` (string | null): One of: `REDIRECT`.
- `checkout_url` (string (uri) | null): Weav's save page, while `PENDING`.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null)
- `expires_at` (string (date-time) | null): When the save link ends, while `PENDING`.
- `activated_at` (string (date-time) | null)
- `failed_at` (string (date-time) | null)
- `removed_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
- `pagination` (object)
- `per_page` (integer, required)
- `next_cursor` (string | null, required)
- `prev_cursor` (string | null, required)
- `is_last_page` (boolean, required)
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"payment_methods": [
{
"id": "PM8K2Q7X9KBAT4MZ3N",
"type": "CARD",
"status": "ACTIVE",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"card": {
"brand": "VISA",
"last4": "1381",
"exp_month": 12,
"exp_year": 2030
},
"label": "Visa •••• 1381",
"…": "…"
}
],
"pagination": {
"per_page": 25,
"next_cursor": null,
"prev_cursor": null,
"is_last_page": true
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 422 Unprocessable Content
One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).
```json
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"channel": [
"The channel field is required."
]
}
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/payment-methods \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payment-methods';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/payment-methods',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/payment-methods');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/payment-methods",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/payment-methods", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/payment-methods")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payment-methods";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Save a payment method
`POST https://api.weavpayments.com/v1/payment-methods`
Saves a customer's card for later charges with no payer present (auto-debit, subscriptions). Send your customer to `next_action.checkout_url`: Weav's save page, where they type their card, agree to future charges and confirm it with their bank (3DS). Their bank may show a ₱10.00 check that is canceled right after. The card goes from their browser to the card processor; it never reaches Weav or you.
You get `PAYMENT_METHOD_SAVED` (now `ACTIVE`: charge it with `POST /v1/payments` and its `payment_method_id`) or `PAYMENT_METHOD_FAILED`. A link not used within 24 hours fails. Only cards today (`type: CARD`).
Saving cards is off until Weav turns it on for your account with your PCI DSS evidence (422 `4220401`); in test mode it always works, with a test save page.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Body
JSON, with `Content-Type: application/json`.
- `type` (string, required): One of: `CARD`.
- `customer` (object, required)
- `id` (string, required): Your own ID for the customer. List their saved methods with it. Up to 64 characters. Pattern: ^[A-Za-z0-9._:@-]+$.
- `name` (string | null): Weav's save page asks your customer when you leave it out. Up to 255 characters.
- `email` (string (email) | null): Up to 255 characters.
- `success_url` (string (uri) | null): HTTPS. Where your customer goes once it's saved. Up to 2048 characters.
- `cancel_url` (string (uri) | null): HTTPS. Where your customer goes if they leave, or the bank's check fails. Up to 2048 characters.
- `webhook_url` (string | null): This payment method's webhooks go here instead of your webhook URL. Up to 2048 characters.
## Responses
### 201 Created
Payment method created (code 2010400).
What `data` holds:
- `payment_method` (object)
- `id` (string)
- `type` (string): One of: `CARD`.
- `status` (string): `PENDING` until your customer saves it and their bank confirms it; `ACTIVE` (chargeable); `FAILED` when that never happens; `REMOVED`. One of: `PENDING`, `ACTIVE`, `FAILED`, `REMOVED`.
- `customer` (object)
- `id` (string)
- `name` (string | null)
- `email` (string | null)
- `card` (object | null): Once saved. Never the card number.
- `brand` (string): From the card processor. One of: `VISA`, `MASTERCARD`, `JCB`.
- `last4` (string)
- `exp_month` (integer | null): The expiry month your customer typed. Ask them for a new card before it passes (`expires_by` on the list). At least 1. At most 12.
- `exp_year` (integer | null): Four digits.
- `label` (string | null): To show your customer.
- `next_action` (object)
- `type` (string | null): One of: `REDIRECT`.
- `checkout_url` (string (uri) | null): Weav's save page, while `PENDING`.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null)
- `expires_at` (string (date-time) | null): When the save link ends, while `PENDING`.
- `activated_at` (string (date-time) | null)
- `failed_at` (string (date-time) | null)
- `removed_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
```json
{
"code": "2010400",
"success": true,
"message": "Payment method created",
"data": {
"payment_method": {
"id": "PM8K2Q7X9KBAT4MZ3N",
"type": "CARD",
"status": "PENDING",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"card": null,
"label": null,
"next_action": {
"type": "REDIRECT",
"checkout_url": "https://pay.weavpayments.com/save/PM8K2Q7X9KBAT4MZ3N"
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account",
"webhook_url": null,
"expires_at": "2026-11-03T10:12:00+08:00",
"activated_at": null,
"failed_at": null,
"removed_at": null,
"created_at": "2026-11-02T10:12:00+08:00",
"updated_at": "2026-11-02T10:12:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 422 Unprocessable Content
Invalid fields, or saving cards is off for your account (4220401).
```json
{
"code": "4220401",
"success": false,
"message": "Saving cards is off for your account.",
"errors": {
"type": [
"Contact support@weavpayments.com to turn it on. Weav needs your PCI DSS evidence first."
]
}
}
```
## Examples
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payment-methods \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "CARD",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account"
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/payment-methods';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'CARD',
customer: {
id: 'USER-123',
name: 'Juan dela Cruz',
email: 'juan@example.com',
},
success_url: 'https://shop.example/account/cards',
cancel_url: 'https://shop.example/account',
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payment-methods',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
type: 'CARD',
customer: {
id: 'USER-123',
name: 'Juan dela Cruz',
email: 'juan@example.com',
},
success_url: 'https://shop.example/account/cards',
cancel_url: 'https://shop.example/account',
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payment-methods', [
'type' => 'CARD',
'customer' => [
'id' => 'USER-123',
'name' => 'Juan dela Cruz',
'email' => 'juan@example.com',
],
'success_url' => 'https://shop.example/account/cards',
'cancel_url' => 'https://shop.example/account',
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payment-methods",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"type": "CARD",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com",
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"type": "CARD",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account"
}`)
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payment-methods", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"type": "CARD",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account"
});
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payment-methods")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payment-methods";
String body =
"""
{
"type": "CARD",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account"
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Retrieve a payment method
`GET https://api.weavpayments.com/v1/payment-methods/{id}`
## Path parameters
- `id` (string, required)
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
The payment method.
What `data` holds:
- `payment_method` (object)
- `id` (string)
- `type` (string): One of: `CARD`.
- `status` (string): `PENDING` until your customer saves it and their bank confirms it; `ACTIVE` (chargeable); `FAILED` when that never happens; `REMOVED`. One of: `PENDING`, `ACTIVE`, `FAILED`, `REMOVED`.
- `customer` (object)
- `id` (string)
- `name` (string | null)
- `email` (string | null)
- `card` (object | null): Once saved. Never the card number.
- `brand` (string): From the card processor. One of: `VISA`, `MASTERCARD`, `JCB`.
- `last4` (string)
- `exp_month` (integer | null): The expiry month your customer typed. Ask them for a new card before it passes (`expires_by` on the list). At least 1. At most 12.
- `exp_year` (integer | null): Four digits.
- `label` (string | null): To show your customer.
- `next_action` (object)
- `type` (string | null): One of: `REDIRECT`.
- `checkout_url` (string (uri) | null): Weav's save page, while `PENDING`.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null)
- `expires_at` (string (date-time) | null): When the save link ends, while `PENDING`.
- `activated_at` (string (date-time) | null)
- `failed_at` (string (date-time) | null)
- `removed_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"payment_method": {
"id": "PM8K2Q7X9KBAT4MZ3N",
"type": "CARD",
"status": "ACTIVE",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"card": {
"brand": "VISA",
"last4": "1381",
"exp_month": 12,
"exp_year": 2030
},
"label": "Visa •••• 1381",
"next_action": {
"type": null,
"checkout_url": null
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account",
"webhook_url": null,
"expires_at": null,
"activated_at": "2026-11-02T10:14:20+08:00",
"failed_at": null,
"removed_at": null,
"created_at": "2026-11-02T10:12:00+08:00",
"updated_at": "2026-11-02T10:14:20+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url =
'https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Remove a payment method
`DELETE https://api.weavpayments.com/v1/payment-methods/{id}`
Removes the card at the card processor too, so nobody can charge it again (`PAYMENT_METHOD_REMOVED`). Removing one already removed is fine. 409 `4090401` when the processor can't remove it right now (retry shortly).
## Path parameters
- `id` (string, required)
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Payment method removed (code 2000401).
What `data` holds:
- `payment_method` (object)
- `id` (string)
- `type` (string): One of: `CARD`.
- `status` (string): `PENDING` until your customer saves it and their bank confirms it; `ACTIVE` (chargeable); `FAILED` when that never happens; `REMOVED`. One of: `PENDING`, `ACTIVE`, `FAILED`, `REMOVED`.
- `customer` (object)
- `id` (string)
- `name` (string | null)
- `email` (string | null)
- `card` (object | null): Once saved. Never the card number.
- `brand` (string): From the card processor. One of: `VISA`, `MASTERCARD`, `JCB`.
- `last4` (string)
- `exp_month` (integer | null): The expiry month your customer typed. Ask them for a new card before it passes (`expires_by` on the list). At least 1. At most 12.
- `exp_year` (integer | null): Four digits.
- `label` (string | null): To show your customer.
- `next_action` (object)
- `type` (string | null): One of: `REDIRECT`.
- `checkout_url` (string (uri) | null): Weav's save page, while `PENDING`.
- `success_url` (string (uri) | null)
- `cancel_url` (string (uri) | null)
- `webhook_url` (string | null)
- `expires_at` (string (date-time) | null): When the save link ends, while `PENDING`.
- `activated_at` (string (date-time) | null)
- `failed_at` (string (date-time) | null)
- `removed_at` (string (date-time) | null)
- `created_at` (string (date-time))
- `updated_at` (string (date-time))
```json
{
"code": "2000401",
"success": true,
"message": "Payment method removed",
"data": {
"payment_method": {
"id": "PM8K2Q7X9KBAT4MZ3N",
"type": "CARD",
"status": "REMOVED",
"customer": {
"id": "USER-123",
"name": "Juan dela Cruz",
"email": "juan@example.com"
},
"card": {
"brand": "VISA",
"last4": "1381",
"exp_month": 12,
"exp_year": 2030
},
"label": "Visa •••• 1381",
"next_action": {
"type": null,
"checkout_url": null
},
"success_url": "https://shop.example/account/cards",
"cancel_url": "https://shop.example/account",
"webhook_url": null,
"expires_at": null,
"activated_at": "2026-11-02T10:14:20+08:00",
"failed_at": null,
"removed_at": "2026-11-20T09:00:00+08:00",
"created_at": "2026-11-02T10:12:00+08:00",
"updated_at": "2026-11-20T09:00:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
### 409 Conflict
The request conflicts with the current state, e.g. an Idempotency-Key reused with a different body (4090001), a request still in progress (4090002), a payment that can't be canceled (4090101) or can't be canceled at the provider yet (4090102, retry shortly), or a webhook still being sent (4090201).
```json
{
"code": "4090001",
"success": false,
"message": "This Idempotency-Key was already used with a different request."
}
```
## Examples
cURL:
```shell
curl -X DELETE https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url =
'https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N';
const response = await fetch(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'delete',
url: 'https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->delete('https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.delete(
"https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("DELETE", "https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.delete("https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payment-methods/PM8K2Q7X9KBAT4MZ3N";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.DELETE()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Get your balance
`GET https://api.weavpayments.com/v1/balance`
Your available balance and what is on hold for withdrawals in progress.
## Query parameters
- `currency` (string): Defaults to your account's default currency. One of: `PHP`.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Your balance in one currency.
What `data` holds:
- `balance` (object)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `available` (number): What you can withdraw.
- `on_hold` (number): Withdrawals on their way to your bank.
- `as_of` (string (date-time) | null): Time of the latest ledger entry.
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"balance": {
"currency": "PHP",
"available": 64835.17,
"on_hold": 119000.0,
"as_of": "2026-10-26T10:15:40+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 422 Unprocessable Content
One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).
```json
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"channel": [
"The channel field is required."
]
}
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/balance \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/balance';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/balance',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/balance');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/balance",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/balance", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/balance")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/balance";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# List ledger entries
`GET https://api.weavpayments.com/v1/ledger-entries`
Every change to your available balance, newest first.
Each entry is permanent. A correction is a new entry, never an edit.
## Query parameters
- `per_page` (integer): Items per page, 25 by default; values above 100 become 100. At least 1. Default: 25.
- `cursor` (string): The `next_cursor` or `prev_cursor` from the previous page. Up to 1024 characters.
- `date_from` (string (date)): Start date (Asia/Manila), inclusive. Send with `date_to`.
- `date_to` (string (date)): End date (Asia/Manila), inclusive.
- `type` (string): Only entries of this type. One of: `PAYMENT`, `REFUND`, `REVERSAL`, `WITHDRAWAL`, `WITHDRAWAL_RETURNED`, `FEE`, `ADJUSTMENT`.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
A page of ledger entries.
What `data` holds:
- `ledger_entries` (array of objects)
- `reference_id` (string)
- `type` (string): `PAYMENT`, `REFUND`, `REVERSAL`, `WITHDRAWAL`, `WITHDRAWAL_RETURNED`, `FEE` or `ADJUSTMENT`. One of: `PAYMENT`, `REFUND`, `REVERSAL`, `WITHDRAWAL`, `WITHDRAWAL_RETURNED`, `FEE`, `ADJUSTMENT`.
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number): Signed.
- `balance_after` (number): Your balance after this entry.
- `source` (object | null): `type` (`PAYMENT`, `REFUND`, `REVERSAL` or `WITHDRAWAL`) and `reference_id` of what caused it. Null when nothing did, such as an adjustment.
- `type` (string): One of: `PAYMENT`, `WITHDRAWAL`, `REFUND`, `REVERSAL`.
- `reference_id` (string)
- `description` (string | null): A readable label, mainly for adjustments.
- `posted_at` (string (date-time)): When the entry took effect.
- `pagination` (object)
- `per_page` (integer, required)
- `next_cursor` (string | null, required)
- `prev_cursor` (string | null, required)
- `is_last_page` (boolean, required)
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"ledger_entries": [
{
"reference_id": "LE5Q2W8E4R6T1Y",
"type": "PAYMENT",
"currency": "PHP",
"amount": 1250.0,
"balance_after": 64835.17,
"source": {
"type": "PAYMENT",
"reference_id": "PY7K3Q9M2X4TBA"
},
"description": null,
"posted_at": "2026-10-26T10:15:40+08:00"
}
],
"pagination": {
"per_page": 25,
"next_cursor": null,
"prev_cursor": null,
"is_last_page": true
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/ledger-entries \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/ledger-entries';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/ledger-entries',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/ledger-entries');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/ledger-entries",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/ledger-entries", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/ledger-entries")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/ledger-entries";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# List withdrawals
`GET https://api.weavpayments.com/v1/withdrawals`
Read your withdrawals. You create withdrawals in the dashboard, because each one needs a fresh code from a person's authenticator app. You can also turn on automatic withdrawals in the dashboard's settings: once a day, Weav withdraws your available balance to your payout account.
To find one withdrawal by any of its numbers, pass `reference` to the list. It matches `reference_id`, `provider_reference_id` or `channel_reference_id` exactly.
## Query parameters
- `per_page` (integer): Items per page, 25 by default; values above 100 become 100. At least 1. Default: 25.
- `cursor` (string): The `next_cursor` or `prev_cursor` from the previous page. Up to 1024 characters.
- `date_from` (string (date)): Start date (Asia/Manila), inclusive. Send with `date_to`.
- `date_to` (string (date)): End date (Asia/Manila), inclusive.
- `reference` (string): Exact match on `reference_id`, `provider_reference_id` or `channel_reference_id`. Up to 64 characters.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
A page of withdrawals.
What `data` holds:
- `withdrawals` (array of objects)
- `reference_id` (string)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number)
- `fee` (number | null)
- `total` (number | null): `amount` plus `fee`.
- `status` (string): `PENDING`, `FOR_SETTLEMENT`, `COMPLETED` or `FAILED`. One of: `PENDING`, `FOR_SETTLEMENT`, `COMPLETED`, `FAILED`.
- `rail` (string | null): INSTAPAY (real time) for 50,000.00 or less; PESONET (batch, not real time) above that, or when the bank isn't on InstaPay. Null for withdrawals made before rails were recorded. One of: `INSTAPAY`, `PESONET`.
- `provider_reference_id` (string | null): The bank partner's transfer reference. Null until known.
- `channel_reference_id` (string | null): The InstaPay or PESONet reference. Null until known.
- `destination` (object): `bank_name`, `account_name`, `account_number_last4`. Never the full account number.
- `bank_name` (string)
- `account_name` (string)
- `account_number_last4` (string)
- `failure` (object | null): `code` and `message` when the withdrawal failed. `code` can be null.
- `code` (string | null)
- `message` (string)
- `requested_at` (string (date-time))
- `completed_at` (string (date-time) | null)
- `failed_at` (string (date-time) | null)
- `updated_at` (string (date-time))
- `pagination` (object)
- `per_page` (integer, required)
- `next_cursor` (string | null, required)
- `prev_cursor` (string | null, required)
- `is_last_page` (boolean, required)
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"withdrawals": [
{
"reference_id": "WD2H6J9K3L7M4N",
"currency": "PHP",
"amount": 119000.0,
"fee": 10.0,
"total": 119010.0,
"status": "FOR_SETTLEMENT",
"rail": "PESONET",
"provider_reference_id": null,
"channel_reference_id": null,
"destination": {
"bank_name": "Sample Bank",
"account_name": "Sample Store Inc.",
"account_number_last4": "4821"
},
"failure": null,
"requested_at": "2026-10-25T14:05:00+08:00",
"completed_at": null,
"failed_at": null,
"updated_at": "2026-10-25T14:30:00+08:00"
}
],
"pagination": {
"per_page": 25,
"next_cursor": "eyJpZCI6MTM0LCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
"prev_cursor": null,
"is_last_page": false
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/withdrawals \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/withdrawals';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/withdrawals',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/withdrawals');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/withdrawals",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/withdrawals", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/withdrawals")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/withdrawals";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Retrieve a withdrawal
`GET https://api.weavpayments.com/v1/withdrawals/{reference_id}`
The amount leaves your balance when the withdrawal is requested. If it fails, a `WITHDRAWAL_RETURNED` ledger entry puts it back.
## When it arrives
- **InstaPay:** right away.
- **PESONet:** credited in batches on banking days. Sent from 4:00 PM the day before to 10:00 AM: credited from 10:40 AM. Sent from 10:00 AM to 1:00 PM: from 1:40 PM. Sent from 1:00 PM to 4:00 PM: from 4:40 PM.
- Sent on a Friday after 4:00 PM, or on a Saturday or Sunday: credited the next Monday, from 10:40 AM. Check at the end of that day before contacting support.
## Path parameters
- `reference_id` (string, required): Pattern: ^WD[0-9A-Z]{12}$.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
The withdrawal.
What `data` holds:
- `withdrawal` (object)
- `reference_id` (string)
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `amount` (number)
- `fee` (number | null)
- `total` (number | null): `amount` plus `fee`.
- `status` (string): `PENDING`, `FOR_SETTLEMENT`, `COMPLETED` or `FAILED`. One of: `PENDING`, `FOR_SETTLEMENT`, `COMPLETED`, `FAILED`.
- `rail` (string | null): INSTAPAY (real time) for 50,000.00 or less; PESONET (batch, not real time) above that, or when the bank isn't on InstaPay. Null for withdrawals made before rails were recorded. One of: `INSTAPAY`, `PESONET`.
- `provider_reference_id` (string | null): The bank partner's transfer reference. Null until known.
- `channel_reference_id` (string | null): The InstaPay or PESONet reference. Null until known.
- `destination` (object): `bank_name`, `account_name`, `account_number_last4`. Never the full account number.
- `bank_name` (string)
- `account_name` (string)
- `account_number_last4` (string)
- `failure` (object | null): `code` and `message` when the withdrawal failed. `code` can be null.
- `code` (string | null)
- `message` (string)
- `requested_at` (string (date-time))
- `completed_at` (string (date-time) | null)
- `failed_at` (string (date-time) | null)
- `updated_at` (string (date-time))
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"withdrawal": {
"reference_id": "WD2H6J9K3L7M4N",
"currency": "PHP",
"amount": 119000.0,
"fee": 10.0,
"total": 119010.0,
"status": "FOR_SETTLEMENT",
"rail": "PESONET",
"provider_reference_id": null,
"channel_reference_id": null,
"destination": {
"bank_name": "Sample Bank",
"account_name": "Sample Store Inc.",
"account_number_last4": "4821"
},
"failure": null,
"requested_at": "2026-10-25T14:05:00+08:00",
"completed_at": null,
"failed_at": null,
"updated_at": "2026-10-25T14:30:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/withdrawals/WD8H2K4M6P9QRT";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# List channels
`GET https://api.weavpayments.com/v1/channels`
Every channel and whether your account can use it now (allowed by Weav and switched on by you).
It also shows your payment limits. `maintenance` is always `null` for now. Creating a payment on a channel that is off returns `422` with code `4220101`.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
All channels.
What `data` holds:
- `payment_limits` (array of objects): The smallest and largest payment you can create, per currency. Null means no limit.
- `currency` (string): ISO 4217. PHP today; more as they're enabled for your account. One of: `PHP`.
- `min_amount` (number | null)
- `max_amount` (number | null)
- `channels` (array of objects)
- `channel` (string): The value you send when creating a payment. One of: `QR_PH`, `GCASH`, `MAYA`, `CARD`, `PAY_AND_GO`, `PALAWANPAY`, `BAYAD`.
- `name` (string)
- `is_enabled` (boolean): Whether new payments can use it (allowed by Weav and switched on by you).
- `apps` (array of objects): QR Ph only. The bank and e-wallet apps you can send as `qr_ph_app`. Empty for other channels.
- `code` (string)
- `name` (string)
- `maintenance` (object | null): Always null for now. Scheduled maintenance windows aren't shown here yet.
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"payment_limits": [
{
"currency": "PHP",
"min_amount": 0.01,
"max_amount": 50000.0
}
],
"channels": [
{
"channel": "QR_PH",
"name": "QR Ph",
"is_enabled": true,
"apps": [
{
"code": "BDO",
"name": "BDO Pay"
},
{
"code": "CHINABANK",
"name": "Chinabank"
},
{
"code": "GOTYME",
"name": "GoTyme"
},
{
"code": "LANDBANK",
"name": "LANDBANK"
},
{
"code": "METROBANK",
"name": "Metrobank"
},
{
"code": "PNB",
"name": "PNB"
},
{
"code": "RCBC",
"name": "RCBC"
},
{
"code": "ROBINSONS_BANK",
"name": "Robinsons Bank"
},
{
"code": "SECURITY_BANK",
"name": "Security Bank"
},
{
"code": "UNIONBANK",
"name": "UnionBank"
},
{
"code": "GRABPAY",
"name": "GrabPay"
},
{
"code": "SHOPEEPAY",
"name": "ShopeePay"
}
],
"maintenance": null
},
{
"channel": "GCASH",
"name": "GCash",
"is_enabled": true,
"apps": [],
"maintenance": null
},
{
"channel": "MAYA",
"name": "Pay with Maya",
"is_enabled": true,
"apps": [],
"maintenance": null
},
{
"channel": "CARD",
"name": "Credit or debit card",
"is_enabled": false,
"apps": [],
"maintenance": null
},
{
"channel": "PAY_AND_GO",
"name": "Pay & Go",
"is_enabled": true,
"apps": [],
"maintenance": null
},
{
"channel": "PALAWANPAY",
"name": "PalawanPay",
"is_enabled": true,
"apps": [],
"maintenance": null
},
{
"channel": "BAYAD",
"name": "Bayad",
"is_enabled": true,
"apps": [],
"maintenance": null
}
]
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/channels \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/channels';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/channels',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/channels');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/channels",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/channels", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/channels")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/channels";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Repost a payment's latest webhook
`POST https://api.weavpayments.com/v1/payments/{reference_id}/webhooks/repost`
Sends the payment's latest webhook again, to your current webhook URL, signed with your active key and a fresh `sent_at`. Only finished deliveries can be reposted. Once every 30 seconds per payment (4290201) and up to 20 a minute per account (4290202), API and dashboard together; to check a status, retrieve the payment instead. Proposed.
> **Note:** **To check a payment’s status, [retrieve the payment](https://docs.weavpayments.com/reference/payments/retrieve-payment).** It’s read-only and always has the latest status. A repost sends a signed request to your server each time, so it’s limited. Statuses can’t be set by hand: Weav confirms every result with the provider, and a late one still updates the payment and sends its webhook.
- Only a finished delivery can be reposted. If Weav is still retrying it, you get `409`.
- **Once every 30 seconds per payment**, and **up to 20 a minute** for your account in each mode, through the API and the dashboard together. Over either, you get `429` with a `Retry-After` header. Weav already retries every webhook for about 7 days (see [Retries](https://docs.weavpayments.com/retries#when-weav-retries-a-webhook)), so there’s never a need to repost in bulk.
- The repost is signed with your active signing key and gets a fresh `sent_at`. The event and its `data` stay the same, so handle it idempotently, as with any webhook.
- Every attempt shows under Developers in the dashboard, where you can also repost with one click.
## Errors
A payment with no webhook yet returns `404`. A webhook still being retried returns `409` with code `4090201`. Reposting the same payment within 30 seconds returns `429` with code `4290201`; more than 20 reposts a minute for your account in one mode returns `429` with code `4290202`.
## Path parameters
- `reference_id` (string, required): Weav's payment reference. Pattern: ^PY[0-9A-Z]{12}$.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 202 Accepted
Queued for repost (code 2020200).
What `data` holds:
- `event` (string): One of: `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_CANCELLED`, `PAYMENT_REVERSED`, `PAYMENT_REFUNDED`, `CHECKOUT_COMPLETED`, `CHECKOUT_EXPIRED`, `PAYMENT_METHOD_SAVED`, `PAYMENT_METHOD_FAILED`, `PAYMENT_METHOD_REMOVED`, `WITHDRAWAL_COMPLETED`, `WITHDRAWAL_FAILED`.
- `reference_id` (string)
```json
{
"code": "2020200",
"success": true,
"message": "Webhook queued for repost",
"data": {
"event": "PAYMENT_COMPLETED",
"reference_id": "PY7K3Q9M2X4TBA"
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
### 409 Conflict
The request conflicts with the current state, e.g. an Idempotency-Key reused with a different body (4090001), a request still in progress (4090002), a payment that can't be canceled (4090101) or can't be canceled at the provider yet (4090102, retry shortly), or a webhook still being sent (4090201).
```json
{
"code": "4090001",
"success": false,
"message": "This Idempotency-Key was already used with a different request."
}
```
### 429 Too Many Requests
Rate limited (code 4290000). Wait for `Retry-After` seconds.
```json
{
"code": "4290000",
"success": false,
"message": "Too many requests. Wait, then retry."
}
```
## Examples
cURL:
```shell
curl -X POST https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url =
'https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost';
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'post',
url: 'https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->post('https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.post(
"https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("POST", "https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.post("https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/payments/PY7K3Q9M2X4TBA/webhooks/repost";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Get your webhook URL
`GET https://api.weavpayments.com/v1/webhook-endpoint`
The one URL where Weav sends this mode's webhooks (payments and withdrawals).
Returns `404` if none is set for this mode.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Your webhook endpoint.
What `data` holds:
- `webhook_endpoint` (object)
- `url` (string)
- `events` (array of strings)
- `health` (string): One of: `UNKNOWN`, `HEALTHY`, `FAILING`, `DOWN`.
- `withdrawals_url` (string | null): Where withdrawal events go, when not url.
- `has_signing_key` (boolean): Webhooks go out only once a signing key exists. Create it in the dashboard under Developers.
- `last_success_at` (string (date-time) | null)
- `updated_at` (string (date-time) | null)
```json
{
"code": "2000000",
"success": true,
"message": "OK",
"data": {
"webhook_endpoint": {
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED",
"PAYMENT_FAILED",
"PAYMENT_CANCELLED",
"PAYMENT_REVERSED",
"PAYMENT_REFUNDED",
"CHECKOUT_COMPLETED",
"CHECKOUT_EXPIRED",
"WITHDRAWAL_COMPLETED",
"WITHDRAWAL_FAILED"
],
"withdrawals_url": null,
"health": "UNKNOWN",
"has_signing_key": true,
"last_success_at": null,
"updated_at": "2026-10-26T10:00:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
## Examples
cURL:
```shell
curl -X GET https://api.weavpayments.com/v1/webhook-endpoint \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/webhook-endpoint';
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'get',
url: 'https://api.weavpayments.com/v1/webhook-endpoint',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->get('https://api.weavpayments.com/v1/webhook-endpoint');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.get(
"https://api.weavpayments.com/v1/webhook-endpoint",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.weavpayments.com/v1/webhook-endpoint", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://api.weavpayments.com/v1/webhook-endpoint")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/webhook-endpoint";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.GET()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Set your webhook URL
`PUT https://api.weavpayments.com/v1/webhook-endpoint`
Sets or replaces this mode's webhook URL, and optionally a separate URL for withdrawal events. They can only be set here, not in the dashboard, so nobody changes them by accident; every change is emailed to your Owners and Developers.
Each webhook goes to exactly one URL: a payment's own `webhook_url` if it has one, else `withdrawals_url` for withdrawal events when set, else url (limited to events).
The URL must be public HTTPS: private and internal addresses are refused. Webhooks are signed with the signing key you create in the dashboard, and none are sent until one exists (`has_signing_key`).
## Where each webhook goes
Every webhook goes to exactly one URL, so one event never goes to two of your URLs. Weav picks it in this order:
1. **The payment’s `webhook_url`.** A payment created with one sends all its events there: completed, failed, canceled, reversed and refunded. Your `url` and its `events` list don’t apply to that payment.
2. **`withdrawals_url`, for withdrawal events.** When it’s set, `WITHDRAWAL_COMPLETED` and `WITHDRAWAL_FAILED` go there, and `url` no longer gets them.
3. **`url`, for everything else**, limited to the `events` you chose.
| Event | Payment has a webhook_url | withdrawals_url is set | Goes to |
| --- | --- | --- | --- |
| A payment event | Yes | (doesn’t matter) | `webhook_url` only |
| A payment event | No | (doesn’t matter) | `url`, if it’s in `events` |
| A withdrawal event | (not a payment) | Yes | `withdrawals_url` only |
| A withdrawal event | (not a payment) | No | `url`, if it’s in `events` |
- Every URL follows the same rules: public HTTPS only (private and internal addresses are refused when you set it, and checked again before each delivery), signed with your signing key (see [Verifying signatures](https://docs.weavpayments.com/webhooks/verifying-signatures)), retried the same way, and shown under Developers in the dashboard with its deliveries.
- Live and test keys each set their own mode’s URLs.
- Every change to `url` or `withdrawals_url` is emailed to your account’s Owners and Developers, with the API key that made it.
- In live mode, if a URL keeps failing, your Owners, Developers and technical contact get an email: when nothing has gotten through for 10 minutes, when it’s down for an hour, a reminder if it’s still down a day later, and one when it works again. If about a quarter of webhooks fail while others get through, they get one email about that too. A short blip sends nothing. Set the technical contact on the Developers page of your dashboard.
- Nothing is sent until you create a signing key in the dashboard under Developers. Until then, `has_signing_key` is `false`.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Body
JSON, with `Content-Type: application/json`.
- `url` (string (uri), required): Required. A public HTTPS URL, up to 2,048 characters. Its host must resolve. Private and internal addresses (such as `localhost` or `10.x`) are refused. Up to 2048 characters.
- `events` (array of strings): Which events url gets. Leave out for all of them. Doesn't limit `withdrawals_url` or a payment's `webhook_url`. One of: `PAYMENT_COMPLETED`, `PAYMENT_FAILED`, `PAYMENT_CANCELLED`, `PAYMENT_REVERSED`, `PAYMENT_REFUNDED`, `CHECKOUT_COMPLETED`, `CHECKOUT_EXPIRED`, `PAYMENT_METHOD_SAVED`, `PAYMENT_METHOD_FAILED`, `PAYMENT_METHOD_REMOVED`, `WITHDRAWAL_COMPLETED`, `WITHDRAWAL_FAILED`.
- `withdrawals_url` (string | null): Optional. Withdrawal events go here instead of url (url stops getting them). Null removes it; leaving it out keeps it. Up to 2048 characters.
## Responses
### 200 OK
Saved (code 2000000).
What `data` holds:
- `webhook_endpoint` (object)
- `url` (string)
- `events` (array of strings)
- `health` (string): One of: `UNKNOWN`, `HEALTHY`, `FAILING`, `DOWN`.
- `withdrawals_url` (string | null): Where withdrawal events go, when not url.
- `has_signing_key` (boolean): Webhooks go out only once a signing key exists. Create it in the dashboard under Developers.
- `last_success_at` (string (date-time) | null)
- `updated_at` (string (date-time) | null)
```json
{
"code": "2000000",
"success": true,
"message": "Webhook endpoint saved",
"data": {
"webhook_endpoint": {
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED",
"PAYMENT_FAILED",
"PAYMENT_CANCELLED",
"PAYMENT_REVERSED",
"PAYMENT_REFUNDED",
"CHECKOUT_COMPLETED",
"CHECKOUT_EXPIRED",
"WITHDRAWAL_COMPLETED",
"WITHDRAWAL_FAILED"
],
"withdrawals_url": null,
"health": "UNKNOWN",
"has_signing_key": true,
"last_success_at": null,
"updated_at": "2026-10-26T10:00:00+08:00"
}
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 422 Unprocessable Content
One or more fields are invalid (code 4220000), or the channel is off (4220101) or doesn't take the currency (4220102).
```json
{
"code": "4220000",
"success": false,
"message": "The given data was invalid.",
"errors": {
"channel": [
"The channel field is required."
]
}
}
```
## Examples
cURL:
```shell
curl -X PUT https://api.weavpayments.com/v1/webhook-endpoint \
-H "Authorization: Bearer $WEAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED"
],
"withdrawals_url": null
}'
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/webhook-endpoint';
const response = await fetch(url, {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/webhooks/weav',
events: ['PAYMENT_COMPLETED'],
withdrawals_url: null,
}),
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'put',
url: 'https://api.weavpayments.com/v1/webhook-endpoint',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
data: {
url: 'https://example.com/webhooks/weav',
events: ['PAYMENT_COMPLETED'],
withdrawals_url: null,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->put('https://api.weavpayments.com/v1/webhook-endpoint', [
'url' => 'https://example.com/webhooks/weav',
'events' => [
'PAYMENT_COMPLETED',
],
'withdrawals_url' => null,
]);
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.put(
"https://api.weavpayments.com/v1/webhook-endpoint",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
json={
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED",
],
"withdrawals_url": None,
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED"
],
"withdrawals_url": null
}`)
req, err := http.NewRequest("PUT", "https://api.weavpayments.com/v1/webhook-endpoint", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let body = json!({
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED"
],
"withdrawals_url": null
});
let response = reqwest::blocking::Client::new()
.put("https://api.weavpayments.com/v1/webhook-endpoint")
.bearer_auth(key)
.json(&body)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/webhook-endpoint";
String body =
"""
{
"url": "https://example.com/webhooks/weav",
"events": [
"PAYMENT_COMPLETED"
],
"withdrawals_url": null
}
""";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Remove your webhook URL
`DELETE https://api.weavpayments.com/v1/webhook-endpoint`
Stops this mode's webhooks until a URL is set again.
Removes `url` and `withdrawals_url`. Only payments and checkouts created with a `webhook_url` still get webhooks, until you set a URL again.
## Headers
- `Authorization` (string, required): `Bearer` plus your API key. See [Authentication](https://docs.weavpayments.com/authentication).
## Responses
### 200 OK
Removed (code 2000000). The withdrawals URL, if set, is removed too.
What `data` holds:
- `webhook_endpoint` (any | null)
```json
{
"code": "2000000",
"success": true,
"message": "Webhook endpoint removed",
"data": {
"webhook_endpoint": null
}
}
```
### 401 Unauthorized
Missing, malformed or unknown API key (code 4010000).
```json
{
"code": "4010000",
"success": false,
"message": "Missing or invalid API key."
}
```
### 403 Forbidden
The key can't make this request (code 4030000): an address outside the key's IP allowlist, a live key before your live mode is on, or an account that isn't active. A live key sent from a browser is refused (code 4030001): keep live keys on your server.
```json
{
"code": "4030000",
"success": false,
"message": "Requests with this key must come from an address on its allowlist."
}
```
### 404 Not Found
Not found in this mode for your account (code 4040000).
```json
{
"code": "4040000",
"success": false,
"message": "Not found."
}
```
## Examples
cURL:
```shell
curl -X DELETE https://api.weavpayments.com/v1/webhook-endpoint \
-H "Authorization: Bearer $WEAV_API_KEY"
```
Node.js:
```js
const url = 'https://api.weavpayments.com/v1/webhook-endpoint';
const response = await fetch(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
});
const result = await response.json();
```
JavaScript:
```js
import axios from 'axios';
const response = await axios.request({
method: 'delete',
url: 'https://api.weavpayments.com/v1/webhook-endpoint',
headers: {
Authorization: `Bearer ${process.env.WEAV_API_KEY}`,
},
// Read error answers too: their body has the error code.
validateStatus: () => true,
});
const result = response.data;
```
PHP:
```php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.weav.key'))
->delete('https://api.weavpayments.com/v1/webhook-endpoint');
$result = $response->json();
```
Python:
```python
import os
import requests
response = requests.delete(
"https://api.weavpayments.com/v1/webhook-endpoint",
headers={
"Authorization": f"Bearer {os.environ['WEAV_API_KEY']}",
},
timeout=30,
)
result = response.json()
```
Go:
```go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("DELETE", "https://api.weavpayments.com/v1/webhook-endpoint", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("WEAV_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
result, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(result))
}
```
Rust:
```rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box> {
let key = std::env::var("WEAV_API_KEY")?;
let response = reqwest::blocking::Client::new()
.delete("https://api.weavpayments.com/v1/webhook-endpoint")
.bearer_auth(key)
.send()?;
let status = response.status();
let result: serde_json::Value = response.json()?;
println!("{status} {result:#}");
Ok(())
}
```
Java:
```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WeavExample {
public static void main(String[] args) throws Exception {
String url = "https://api.weavpayments.com/v1/webhook-endpoint";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + System.getenv("WEAV_API_KEY"))
.DELETE()
.build();
HttpResponse response =
HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
```
---
# Changelog
Changes to the API and these docs, newest first.
## 3 Nov 2026
### API
- **New: a saved card's expiry.** Saved cards now carry `card.exp_month` and `card.exp_year`, and [GET /v1/payment-methods](https://docs.weavpayments.com/reference/payment-methods/list-payment-methods) takes `expires_by` (`YYYY-MM`) to find cards that expire soon.
- **New: a saved card's charges.** [GET /v1/payments](https://docs.weavpayments.com/reference/payments/list-payments) takes `payment_method_id`.
- **New: save the card while paying.** Add `save_payment_method: true` and `customer.id` to a `CARD` payment on [POST /v1/payments](https://docs.weavpayments.com/reference/payments/create-payment). Your customer pays and agrees to future charges in one step, with one check from their bank. You get `PAYMENT_COMPLETED`, then `PAYMENT_METHOD_SAVED`. See [Saved cards](https://docs.weavpayments.com/saved-cards).
- **New: save the card from a checkout.** [POST /v1/checkouts](https://docs.weavpayments.com/reference/checkouts/create-checkout) takes the same `save_payment_method` and `customer.id`. The card is saved if your customer pays by card.
### Docs
- **New: test saved cards.** Test mode's save page now has a card whose charges are declined and one that expires this month. See [Test mode](https://docs.weavpayments.com/test-mode#test-saved-cards).
- **New: [Subscriptions](https://docs.weavpayments.com/subscriptions).** How to charge a saved card on your own schedule, with retries, expiring cards and canceling.
## 2 Nov 2026
### API
- **New: saved cards.** Save your customer's card once on Weav's save page ([POST /v1/payment-methods](https://docs.weavpayments.com/reference/payment-methods/create-payment-method)), then charge it with no customer present: `payment_method_id` on [POST /v1/payments](https://docs.weavpayments.com/reference/payments/create-payment). Also list, retrieve and remove, and the `PAYMENT_METHOD_SAVED`, `PAYMENT_METHOD_FAILED` and `PAYMENT_METHOD_REMOVED` webhooks. Off until Weav turns it on for your account. See [Saved cards](https://docs.weavpayments.com/saved-cards).
Simpler payments and checkouts. Weav isn't live yet, so these changes don't keep the old names.
- **Changed: retries need nothing extra.** The `Idempotency-Key` header is now optional. Without it, your `external_reference_id` is the key: the same reference and body returns the stored response. See [Idempotency](https://docs.weavpayments.com/idempotency).
- **Changed: one shape for return URLs.** Payments take `success_url` and `cancel_url`, the same as checkouts (was `redirect_urls.success_url` and `redirect_urls.failure_url`). Both are optional for every channel: without them, your customer stays on Weav's result page.
- **Changed: `callback_url` is now `webhook_url`** on payments and checkouts. Without it, webhooks go to your webhook URL.
- **Changed: `customer` is optional with `HOSTED`.** Without a name, Weav's payment page asks your customer before the payment starts. With `DIRECT`, `customer.name` is still required.
## 1 Nov 2026
### API
- **New: live keys are refused from a browser.** A `wv_live_` key sent from a web page (with an `Origin` header) gets `403` with code `4030001`. Calls from your server are unchanged. See [Errors](https://docs.weavpayments.com/errors).
### Docs
- **Webhook IP addresses:** Weav sends webhooks from `18.136.62.102` and `18.139.176.153`. See [IP addresses](https://docs.weavpayments.com/webhooks/ip-addresses). And a full guide to your key's [IP allowlist](https://docs.weavpayments.com/authentication#ip-allowlist).
- A new home for the docs, with an API reference made from Weav's OpenAPI file: every endpoint with its fields, responses, and samples in cURL, Node.js, JavaScript, PHP, Python, Go, Rust and Java.
- **Try it:** send a request from any reference page with a test key. See [the API reference](https://docs.weavpayments.com/reference#try-it).
- **For AI assistants:** every page as Markdown (add `.md`), **Copy for LLM** and **More actions** (open in ChatGPT, Claude or Grok; connect Cursor, VS Code or Claude Code) on every page, full `llms.txt` files, and a public MCP server at `/mcp`. See [Build with AI](https://docs.weavpayments.com/build-with-ai).
## 29 Oct 2026
### API
- **New: checkouts.** [POST /v1/checkouts](https://docs.weavpayments.com/reference/checkouts/create-checkout) makes one link for an order or an amount; your customer chooses how to pay on Weav's page. Also [list](https://docs.weavpayments.com/reference/checkouts/list-checkouts), [retrieve](https://docs.weavpayments.com/reference/checkouts/retrieve-checkout) and [cancel](https://docs.weavpayments.com/reference/checkouts/cancel-checkout), the `CHECKOUT_COMPLETED` and `CHECKOUT_EXPIRED` webhooks, and `checkout_reference_id` on payments.
- **Changed: `next_action_format`** is now `HOSTED` (was `PAYMENT_LINK`) or `DIRECT`.
## 28 Oct 2026
### API
- **New: `references`** on the payment object: the provider’s other numbers for a payment, the ones your customer may have from their app or receipt (`invoice_number`, `receipt_number`, `transaction_id`). See [The payment object](https://docs.weavpayments.com/reference/payments/retrieve-payment#response-200).
- **Changed: `reference`** on [List payments](https://docs.weavpayments.com/reference/payments/list-payments) also matches a number in `references`, which can match more than one payment.
- **Changed: `channel_reference_id`** now holds only a reference that is unique to the payment. Maya’s receipt numbers and GCash’s transaction IDs can repeat, so they moved to `references`; for those channels `channel_reference_id` is null.
- **New: repost limits.** [Repost a webhook](https://docs.weavpayments.com/reference/webhooks/repost-payment-webhook) works once every 30 seconds per payment and up to 20 times a minute per account in each mode (codes `4290201` and `4290202`). To check a status, retrieve the payment.
### Docs
- New guides: [How payments work](https://docs.weavpayments.com/how-payments-work), with a sequence diagram for each kind of channel, and [Retries](https://docs.weavpayments.com/retries).
- Search across every page, with ⌘K or Ctrl K. Light and dark mode. Method badges in the API reference.
- Clearer [signing key rotation](https://docs.weavpayments.com/webhooks/verifying-signatures#rotating-your-signing-key): one key signs at a time.