# VeloraPay Merchant API Guide

Use this guide to connect a merchant billing system to VeloraPay. The core
integration is server-side: record invoice lines, create a hosted checkout,
redirect the payer, verify terminal webhooks, and fulfil only after the amount,
currency, invoice reference, and terminal status match.

## Staging

Base URL: `https://api.staging.velorapay.com`

Use the staging merchant API key issued to your account and synthetic invoices.
The examples below use `ak_test_example` as a fixture. Do not put real customer
data, live credentials, or payment secrets into screenshots, logs, support
tickets, or examples.

Current staging supports test integrations and test API keys only. Live key
creation is not enabled yet.

Recommended fixtures:

- API key: `ak_test_example`
- Invoice reference: `EPA-2026-001`
- Currency: `GHS`
- Webhook timestamp tolerance: `300 seconds`

## Integration Setup

Create the integration, test API key, and webhook endpoint in the VeloraPay
Merchant Dashboard before writing code. These setup actions are dashboard
actions. Your backend uses only the generated integration API key for the
payment APIs below.

In the Merchant Dashboard:

1. Create or select the test integration for the app, ERP, website, or
   back-office connection.
2. Create a test API key for that integration. Copy the one-time key into your
   backend secret manager.
3. Add your HTTPS webhook receiver URL. Copy the one-time webhook signing
   secret into your webhook verifier.
4. Record the integration name, key prefix, webhook URL, and secret owner in
   your deployment runbook.

After setup, use the integration API key from your backend for the payment APIs
below.

## Build Your First Payment

### 1. Record invoice lines

```bash
curl https://api.staging.velorapay.com/api/v1/invoice-lines \
  -H "Authorization: Bearer ak_test_example" \
  -H "Idempotency-Key: invoice-EPA-2026-001" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_ref": "EPA-2026-001",
    "currency": "GHS",
    "lines": [
      { "code": "EPA-PERMIT", "name": "Permit fee", "amount_minor": 300000 },
      { "code": "EPA-LEVY", "name": "Environmental levy", "amount_minor": 7038 }
    ]
  }'
```

Expected response:

```json
{
  "object": "invoice.line_items",
  "invoice_ref": "EPA-2026-001",
  "ingested": 2,
  "billed_minor": 307038,
  "errors": []
}
```

### 2. Create a checkout session

```bash
curl https://api.staging.velorapay.com/api/v1/sessions \
  -H "Authorization: Bearer ak_test_example" \
  -H "Idempotency-Key: checkout-EPA-2026-001" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_ref": "EPA-2026-001",
    "amount_minor": 307038,
    "currency": "GHS",
    "description": "EPA permit payment",
    "customer": { "name": "Kwame Asante", "email": "kwame@example.com" },
    "callback_url": "https://epa.gov.gh/payments/return"
  }'
```

Expected response:

```json
{
  "id": "cs_example",
  "status": "open",
  "amount_minor": 307038,
  "currency": "GHS",
  "invoice_ref": "EPA-2026-001",
  "description": "EPA permit payment",
  "checkout_url": "https://velorapay-checkout-staging-fxzcszzkha-uc.a.run.app/pay/cs_example",
  "expires_at": "2026-06-14T12:30:00Z"
}
```

Redirect the payer to `checkout_url`. Card entry stays on VeloraPay-hosted
checkout; do not collect card numbers or security codes in your application.

Set `callback_url` to your return route. Store the VeloraPay session id,
`invoice_ref`, and your order id before redirecting the payer. After checkout
completes, VeloraPay can return the payer to your callback URL. The return is
not payment proof; confirm final state by signed webhook or
`GET /api/v1/sessions/{session_id}` before marking the order paid.

### 3. Wait for completion

After you redirect the payer to `checkout_url`, VeloraPay-hosted checkout handles
payment-method selection, fee display, Mobile Money prompts, card entry, cash
slip creation, and payment collection. Your backend waits for a webhook or
session lookup; it does not start payment-method calls.

Signed webhooks are the primary fulfilment signal. If your backend needs a
fallback lookup after the payer returns, read the checkout session with the
integration API key. This endpoint does not require an `attempt_reference`.

```bash
curl https://api.staging.velorapay.com/api/v1/sessions/cs_example \
  -H "Authorization: Bearer ak_test_example"
```

```json
{
  "id": "cs_example",
  "status": "pending",
  "amount_minor": 307038,
  "currency": "GHS",
  "invoice_ref": "EPA-2026-001",
  "description": "EPA permit payment",
  "checkout_url": "https://velorapay-checkout-staging-fxzcszzkha-uc.a.run.app/pay/cs_example",
  "expires_at": "2026-06-14T12:30:00Z"
}
```

### 4. Fulfil only after final proof

Safe fulfilment requires all of these:

- `checkout.session.completed` or final checkout status.
- Terminal success.
- Matching `amount_minor`.
- Matching `currency`.
- Matching `invoice_ref`.
- Valid webhook signature when the signal comes from a webhook.
- A new event id that has not already been processed.

Do not fulfil on a redirect alone, pending status, failed status, expired
session, amount mismatch, currency mismatch, invalid signature, or duplicate
event with a changed body.

## Authentication And Idempotency

Send merchant API keys only from your server:

```http
Authorization: Bearer ak_test_example
```

Use `Idempotency-Key` on checkout and invoice-line writes. Retry with the same
key only when the request body is unchanged. Reusing a key with a different body
returns `IDEMPOTENCY_KEY_REUSED`.

## Payment Methods

For the standard hosted checkout integration, VeloraPay handles payment methods
after you redirect the payer to `checkout_url`.

- Mobile Money: VeloraPay-hosted checkout collects the payer network, phone
  number, and any provider verification step.
- Bank transfer: VeloraPay-hosted checkout shows transfer instructions when the
  rail is enabled.
- Cash deposit: VeloraPay-hosted checkout creates the slip; teller review can keep
  the checkout pending before the final completed event.
- Card: VeloraPay-hosted checkout owns card entry. Never collect raw card data in
  merchant forms, metadata, logs, or webhook handlers.

Payment-method endpoints in the API reference are used by VeloraPay-hosted
checkout or explicitly approved custom checkout clients. Merchant backends
normally do not call them.

## Webhooks

VeloraPay sends terminal checkout events to your registered HTTPS endpoint.

Headers:

- `X-VeloraPay-Event-Id`
- `X-VeloraPay-Event-Type`
- `X-VeloraPay-Signature`

Signature format:

```text
X-VeloraPay-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
v1 = HMAC_SHA256(signing_secret, "<t>.<raw_request_body>")
```

Verification checklist:

1. Read the raw request body.
2. Parse `t` and `v1` from `X-VeloraPay-Signature`.
3. Reject timestamps more than 5 minutes from your server clock.
4. Recompute `HMAC_SHA256(signing_secret, "<t>.<raw_request_body>")`.
5. Compare signatures with a constant-time comparison.
6. Store `X-VeloraPay-Event-Id`.
7. Return 200 for already-processed events.
8. Confirm `amount_minor`, `currency`, `invoice_ref`, and terminal status before fulfilment.

Event types:

- `checkout.session.completed`
- `checkout.session.failed`
- `checkout.session.expired`

## Staging Cases To Prove

Before requesting live credentials, prove:

- Successful checkout: invoice lines, checkout, terminal webhook, and fulfilment.
- Pending payment: order remains pending while `finalized` is false.
- Failure and expiry: no value is released.
- Webhook replay: duplicate event id returns `200` without second fulfilment.
- Signature rejection: changed body or stale timestamp is rejected.
- Idempotent retry: same key and same body replay safely; changed body fails.

## Error Handling

Errors use this envelope:

```json
{
  "error": {
    "code": "SESSION_NOT_PAYABLE",
    "message": "Checkout session is not open for payment."
  }
}
```

Use the OpenAPI `MerchantErrorCode` component for the stable error-code list.

Common handling:

- `UNAUTHORIZED`: check that the API key belongs to the right environment.
- `IDEMPOTENCY_KEY_REUSED`: retry with the original body or create a new key.
- `SESSION_NOT_PAYABLE`: refresh the checkout before asking the payer to continue.
- `PAYMENT_AMOUNT_MISMATCH`: refresh the hosted checkout and use the current amount.
- `RATE_LIMITED`: wait for `retry_after_seconds`.

## Versioning

Current public contract: `2026-06`.

The public API can add optional response fields without notice. Required request
fields, event names, webhook signature rules, and error-code meanings need a
migration note before they change.

## Reference

- API reference: `/reference/`
- OpenAPI JSON: `/openapi.json`
- Staging collection: `/sandbox.collection.json`
- llms.txt: `/llms.txt`
