# Charges

A charge is how you ask a customer for a payment, and the first thing to reach for. Creating one locks the exchange rate for 30 minutes, allocates a deposit address for exactly that payment and returns a checkout_url the customer can open. Pass whatever you already know about the customer and the order. It only needs the coin up front, so ask which one first. You do not need a payment link to charge someone.

[New here? See how to charge a customer, step by step](https://velirapay.com/docs/charge-a-customer)

## The charge object

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Public code, also the last path segment of the checkout URL. |
| `object` | "charge" | The kind of object. |
| `status` | "pending" \| "underpaid" \| "paid" \| "expired" \| "canceled" | Pending until a confirmed payment arrives, the timer runs out or the charge is canceled. See how the status changes below. |
| `payment_link` | string\|null | The link it was created through, if any. |
| `fiat_amount, fiat_currency` | string | What the customer owes, in your currency. |
| `coupon, discount_amount` | string\|null | The coupon code the customer used and what it took off the price, or null without a coupon. fiat_amount is already the lower price. |
| `asset` | string | The coin and network the customer pays with: BTC, LTC, DOGE, BCH, ETH, SOL, BNB, TRX, USDT, USDC, USDT_BSC, USDC_BSC. |
| `asset_amount` | string | Exact crypto amount to send, rounded up at the locked rate. |
| `exchange_rate` | string | Fiat per one unit of the asset at creation. |
| `received_amount, remaining_amount` | string\|null, string | What has been credited so far and what is still missing. |
| `overpaid` | boolean | True when the surplus exceeds your overpayment threshold. |
| `refunded_amount, refunds` | string, array | What you recorded as sent back: amount, txid, reason, explorer_url, created_at per refund. |
| `deposit_address` | string | The address this charge is paid into, made for it alone. |
| `transactions` | array | On-chain transfers matched to the charge: txid, amount, confirmations, required_confirmations, credited, explorer_url, seen_at. An unconfirmed transfer the chain stops reporting for 30 minutes (dropped or replaced) is removed again. explorer_url is null for a simulated test payment. |
| `customer_email` | string\|null | Receives a receipt when the charge is paid, a notice when a payment falls short, and a notice of each refund you record. |
| `customer_name` | string\|null | From customer.name, from the checkout when the link asks for it, or from the invoice. |
| `customer` | object | Everything known about who pays: email, name, ip_address, user_agent, reference, phone, country and metadata. See customer details below. |
| `custom_fields` | array | Labelled details: the customer's answers to the link's extra questions, or what you passed when creating the charge. Each has label and value. |
| `invoice` | string\|null | The invoice this charge is an attempt to pay, if any. |
| `timeline` | array | What happened so far, oldest first: event, time. Left out when the charge comes embedded in an event, and in MCP server results. |
| `description, metadata` | string\|null, object | Yours to fill: order numbers, references. The description is up to 500 characters. Metadata values must be scalar: 20 keys max, keys up to 40 characters, values up to 500. |
| `success_url, cancel_url, success_message` | string\|null | What you set on the charge: the page the customer is offered a button back to once they have paid, the page they can go back to without paying, and what they are told once they have paid. Null ones fall back to the payment link's and then to your account's defaults, without showing it here. |
| `checkout_url` | string | The hosted payment page. |
| `receipt_url` | string\|null | A printable receipt for the customer, once the charge is paid. |
| `expires_at, paid_at, created_at` | string, string\|null, string | ISO 8601 timestamps in UTC. |

## Endpoints

- GET `/v1/charges` List charges, newest first. Filter with ?status=, ?payment_link=, ?asset=, ?search= (the charge id, customer email or customer name), ?created_from= and ?created_to=.
- POST `/v1/charges` Create a charge; the body is described below. Answers 201 with the charge, 429 with Retry-After once the account has created 300 charges in this mode in the last 30 minutes (canceled ones included, paid live ones not, through the API, MCP and the dashboard; payments customers start at checkout do not count), or 503 while no exchange rate is available.
- GET `/v1/charges/{id}` Retrieve a charge and its current status.
- POST `/v1/charges/{id}/cancel` Cancel a pending charge nothing has been sent to yet. Answers 409 once it is paid, underpaid, expired or canceled, and as soon as a transfer to it has been seen, even an unconfirmed one.
- POST `/v1/charges/{id}/send` Email the customer the payment page of a pending charge. They have until expires_at to pay, so for a bill to pay later create an invoice instead. 409 unless it is pending and has a customer email; 429 with Retry-After within 10 minutes of the last email, or past 100 emails an hour for the account (shared with invoices and refund notices, and with the other accounts its owner owns). In test mode, only members of your account are emailed; anyone else answers 422 under errors.customer_email.
- POST `/v1/charges/{id}/accept` Accept an underpaid charge, or a late payment on an expired or canceled one, as paid for what actually arrived. It is forwarded to you and charge.paid goes out. 409 for any other charge.
- POST `/v1/charges/{id}/refunds` Record a refund you sent from your wallet. Body: amount (in the asset, up to 18 decimals), txid (optional, letters and digits, up to 100) and reason (optional, up to 255). Answers 201 with the updated charge; 422 when the amount is more than what was counted towards the charge, less what was already refunded; a late payment counts once it is accepted.
- POST `/v1/charges/{id}/simulate` Test mode only: pay the charge without coins. Body: amount (optional, in the asset; left out, whatever is still due, so pass less to try an underpayment). 409 for a live charge, and for one that is not awaiting payment.

## Creating a charge

| Field | Type | Description |
| --- | --- | --- |
| `amount` | string | What to charge, with up to two decimals, 0.01 or more. Required, except through a fixed-price payment link, which sets it. |
| `currency` | string | USD, EUR, GBP, CAD, AUD, CHF, TRY. Required, except through a payment link, whose currency is used instead. |
| `asset` | string | Required. A coin GET /v1/assets lists as accepted and, through a payment link, one of the coins the link accepts. The amount has to reach the coin's minimum. |
| `payment_link` | string\|null | Only when the payment belongs to an existing link: the id of a payment link of the same mode to charge through. On an open-amount link, amount has to fall within its min_amount and max_amount; an archived link, or one past its expires_at or max_payments, answers 422. |
| `customer` | object\|null | What you know about who pays; see customer details below. |
| `customer_email` | string\|null | The older form of customer.email; when both are sent, customer.email is used. |
| `coupon` | string\|null | The code of one of your coupons of this mode, taken off the amount. Answers 422 under errors.coupon when it cannot be used; see coupons. |
| `description, metadata` | string\|null, object\|null | As on the charge object. |
| `custom_fields` | array\|null | Up to five labelled details shown with the charge in the dashboard: label (up to 60 characters) and value (up to 255). Use it to pass the answers to a payment link's questions. |
| `success_url, cancel_url` | string\|null | https:// addresses on a public website, up to 2,048 characters: a button back once the customer has paid, and a link back from the checkout. They take precedence over the payment link's and your account's defaults. |
| `success_message` | string\|null | Up to 1,000 characters, shown to the customer once they have paid and included in the receipt email. It takes precedence over the payment link's and your account's default. |
| `send_email` | boolean | Email the customer the payment page once the charge exists. Needs a customer email, and follows the limits of POST /v1/charges/{id}/send, refusing before anything is created. |

A charge created through a payment link skips the questions its hosted checkout would ask: require_email, collect_name and required custom_fields are not enforced. Pass the answers you have under customer and custom_fields.

## Example: create a charge

```bash
curl -X POST 'https://api.velirapay.com/v1/charges' \
  --fail-with-body --silent --show-error --max-time 30 \
  -H "Authorization: Bearer ${VELIRAPAY_API_KEY:?Set VELIRAPAY_API_KEY}" \
  -H 'Accept: application/json' \
  -H 'Idempotency-Key: 4d6c1e1a-…' \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": "150.00",
    "currency": "USD",
    "asset": "BTC",
    "customer": {
      "email": "buyer@example.com",
      "ip_address": "203.0.113.7",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) …",
      "reference": "cus_1042",
      "country": "FR"
    },
    "description": "Order #1042",
    "metadata": {"order_id": "1042"},
    "success_url": "https://example.com/orders/1042"
  }'
```

**Response: 201 Created**

```json
{
  "data": {
    "id": "2t4bcuqkpkoy",
    "object": "charge",
    "status": "pending",
    "fiat_amount": "150.00",
    "fiat_currency": "USD",
    "asset": "BTC",
    "asset_amount": "0.0025",
    "received_amount": null,
    "remaining_amount": "0.0025",
    "overpaid": false,
    "exchange_rate": "60000",
    "deposit_address": "bc1q…",
    "transactions": [],
    "checkout_url": "https://velirapay.com/c/2t4bcuqkpkoy",
    "expires_at": "2026-09-17T00:30:00.000000Z",
    …
  }
}
```

Poll GET /v1/charges/{id} if you must, but a webhook endpoint tells you when a payment is seen on-chain and when a charge is paid, underpaid, expired or canceled, without polling.

## Customer details

The customer object gathers what is known about who pays, to help you spot fraud before you ship anything. When the customer starts the payment on the hosted checkout, the IP address and browser (user agent) they did it from are recorded for you. When you create the charge from your own server, that request comes from you rather than from the customer, so pass what you know under customer instead:

| Field | Type | Description |
| --- | --- | --- |
| `email, name` | string | Who pays, up to 255 characters each. The receipt goes to the email; the older top-level customer_email still works. |
| `ip_address` | string | The IPv4 or IPv6 address your customer ordered from. |
| `user_agent` | string | Their browser's user agent, up to 1000 characters. |
| `reference` | string | Your own id for the customer, up to 255 characters. |
| `phone` | string | A phone number, as you have it, up to 50 characters. |
| `country` | string | The ISO 3166 two-letter country code, such as FR. Stored in capitals. |
| `metadata` | object | Anything else worth keeping, such as the age of their account or how many orders they placed: up to 20 keys with scalar values, under the same length limits as the charge's own metadata. |

Any other key in customer is refused with 422, so a typo does not go unnoticed. The object comes back on the charge, in the dashboard and in every webhook about it, with null for whatever is not known and an empty metadata object.

## How the status changes

- `pending → paid, underpaid` Confirmed funds arrive, within or short of your underpayment tolerance.
- `pending → expired` The rate lock runs out. Expiry waits while a transfer to the charge is still confirming, and for up to 3 hours while its chain cannot be read.
- `pending → canceled` You cancel it, the customer switches to another coin on the checkout, or the invoice it belongs to is voided.
- `underpaid → paid` The rest arrives. An underpaid charge never expires.
- `underpaid, expired, canceled → paid` You accept an underpaid charge, or a late payment on an expired or canceled one, as paid, with POST /v1/charges/{id}/accept or in the dashboard (admins only).

## How payments are detected

Every open charge's deposit address is checked about once a minute. A transfer shows up under transactions as soon as the network reports it, the checkout page tells the customer it was seen, and a charge.payment_detected webhook goes out with the transfer under data.transaction. It is credited once it has the required confirmations (2 for Bitcoin, 6 on Ethereum by default, other networks have their own); only then does the charge become paid or underpaid, with a charge.paid or charge.underpaid webhook. A charge does not expire while a transfer is still confirming.

Because the check runs once a minute, a transfer can already have confirmations when it is first seen, and on a fast chain charge.paid can follow charge.payment_detected within the same minute. A simulated test payment is recorded as confirmed straight away, without a charge.payment_detected.

Every charge is given a deposit address of its own, so a payment is matched to its charge by where it was sent rather than by its amount.

A payment that arrives after a charge expired or was canceled is still recorded against it. Its address is watched for 3 hours after expires_at, and every day at 04:00 UTC the addresses of closed charges up to 30 days old are read again, so a later payment can still surface the next morning. The charge keeps its status, a charge.late_payment webhook goes out, the dashboard flags the late payment, and you decide whether to accept it as paid.

A second transfer to a charge that is already paid raises charge.late_payment as well. It is counted automatically once confirmed, so received_amount, and possibly overpaid, change without a further webhook.

## Refunds

A refund is a transfer you make yourself, from your own wallet: a payment that has been forwarded to you cannot be pulled back. Record it on the charge, in the dashboard or with POST /v1/charges/{id}/refunds, and the charge shows what was sent back, the customer is emailed if the charge has their email (at most once every 10 minutes for one charge, and counted against the account's 100 customer emails an hour), and a charge.refunded event goes out. A late payment is accepted before it is refunded, so that it is forwarded to you first.

## Underpayments and overpayments

Customers rarely send the exact amount: wallets and exchanges take fees out of it. Each account sets an underpayment tolerance under Payment settings. A payment that lands within the tolerance settles the charge as paid; one that falls further short moves it to underpaid, where received_amount and remaining_amount tell you where it stands. The customer can top it up on the same checkout page until the rate lock ends, and you can accept it as paid from the dashboard. An underpaid charge never expires: it stays underpaid until the rest arrives or you accept it.

Paying too much never blocks a charge. When the surplus exceeds your overpayment threshold, overpaid is true so you can refund the difference.

A charge can only be created for an asset your account has a payout address for, and for at least the asset's minimum; anything else fails validation with 422 under errors.asset. [See the coins and their minimums](https://velirapay.com/docs/assets)
