# Create & verify payments

Initialize a charge, direct the customer to checkout, and confirm what happened.

## Create on your server

POST /payments starts a payment through a connected provider. The provider field selects a provider; its connection must belong to the key’s environment. Calculate the amount from trusted order data.

### cURL

```
curl https://api.veyqo.org/api/v1/payments \
  -H "Authorization: Bearer $VEYQO_SECRET_KEY" \
  -H "Idempotency-Key: $ORDER_PAYMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50000,
    "currency": "NGN",
    "reference": "ORDER-1001",
    "provider": "paystack",
    "customer": { "email": "customer@example.com" },
    "metadata": { "order_id": "1001" }
  }'
```

### Node.js

```
// Server-side JavaScript (Node.js 18+). Values come from your order database.
const response = await fetch('https://api.veyqo.org/api/v1/payments', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VEYQO_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': order.paymentAttemptKey, // Persist before the request.
  },
  body: JSON.stringify({
    amount: order.totalMinor,
    currency: order.currency,
    reference: order.reference,
    provider: 'paystack',
    customer: { email: order.customerEmail },
  }),
});
const result = await response.json();
if (!response.ok || !result.success) {
  throw new Error(result.error?.code ?? 'PAYMENT_REQUEST_FAILED');
}
// Persist result.data.id against the order before redirecting the customer.
const checkoutUrl = result.data.checkout_url;
```

## Payment request fields

| Field | Meaning |
| --- | --- |
| amount | Required positive integer in the smallest currency unit. |
| currency | Required three-letter currency code, supported by the provider. |
| customer.email | Required customer email for the nested customer form. |
| reference | Your order reference; if omitted, the payment ID becomes the reference. |
| provider | Provider slug, such as paystack. |
| callback_url | Optional HTTPS URL the provider can return the customer to. |
| metadata | Optional application context, such as an internal order ID. |

## Retry the same operation safely

Send an Idempotency-Key on POST requests. Generate and persist it before the first attempt. If the request times out, retry with the same key and exactly the same body. A different body with the same key returns a conflict.

Use a new key for a new logical operation. Your order reference and idempotency key serve different purposes: one identifies the order; the other identifies a request attempt across retries.

## Verify before fulfilment

GET /payments/{id} retrieves the stored payment. POST /payments/{id}/verify queries the provider and updates the payment result. Both require payments:read.

Pending is not a successful payment. Store the payment ID and reconcile webhooks against the order’s reference, expected amount and currency. Deduplicate fulfilment even if the same confirmation arrives more than once.

## Read the real payment response

POST /payments returns HTTP 201 with a payment in data. Its checkout address is data.checkout_url. It is not data.checkout.url, and its initial status is usually pending. The provider URL in the example is illustrative.

```json
{
  "success": true,
  "data": {
    "id": "pay_01M6EB66SC61Q3RQYAN0693DGM",
    "object": "payment",
    "reference": "ORDER-1001",
    "status": "pending",
    "amount": 50000,
    "amount_refunded": 0,
    "currency": "NGN",
    "customer": {
      "email": "customer@example.com",
      "name": "Ada Lovelace",
      "phone": null
    },
    "description": "Order 1001",
    "checkout_url": "https://checkout.paystack.com/example",
    "provider": "paystack",
    "provider_reference": "provider_reference_example",
    "metadata": {
      "order_id": "1001"
    },
    "failure_code": null,
    "failure_message": null,
    "authorized_at": null,
    "succeeded_at": null,
    "failed_at": null,
    "created_at": "2026-11-02T12:00:00+00:00"
  }
}
```

## Keep the payment ID and merchant reference

Persist the Veyqo payment ID alongside your order before redirecting the customer. GET /payments/{id} and POST /payments/{id}/verify accept either that ID or your URL-encoded reference directly in the path. There is no ?reference= lookup on the single-payment endpoint.

Use the Veyqo ID for unambiguous reconciliation, especially when order references are reused across sandbox and live. Cross-project resources stay not-found; a known payment in the other environment of the same project can produce ENVIRONMENT_MISMATCH.

## Payment states

| Status | Meaning / action |
| --- | --- |
| created | Recorded by Veyqo before a provider result. Do not fulfil. |
| pending | Waiting for the customer or provider. Do not fulfil. |
| processing | Provider outcome is not settled yet. Verify later. |
| succeeded | Payment confirmed. Compare amount, currency and order ownership before fulfilling once. |
| failed | Payment failed. Inspect failure_code and failure_message. |
| cancelled | Payment was cancelled. Do not fulfil. |
| partially_refunded | Some captured funds have been refunded. Track amount_refunded. |
| refunded | The captured payment has been fully refunded. |

## API failures and payment failures are different

An error envelope with success:false describes a rejected or failed API operation. A 200 response can return a payment whose status is failed, and a 201 response can return a pending payment. Inspect both layers. Payment failure_code is not the same catalogue as error.code.

gateway_declined means the provider rejected the payment; show an appropriate customer message and let the customer choose another method. amount_mismatch means the provider-reported amount failed reconciliation; do not fulfil, do not override it from browser data, and investigate the stored order and provider transaction. A creation failure can store the corresponding API error code on the payment. failure_message contains provider-specific detail and may be null; do not use its wording as a stable machine code.

- [Create a payment](https://veyqo.org/docs/api-payments-create): POST /payments

- [Verify a payment](https://veyqo.org/docs/api-payments-verify): POST /payments/{id}/verify
