# Nexzoneo Payment API - Complete Developer Integration Guide

## Table of Contents

- [Overview](#overview)
- [Getting Started](#getting-started)
- [Authentication](#authentication)
- [API Endpoints](#api-endpoints)
  - [Payment Session Management](#payment-session-management)
  - [Refund Management](#refund-management)
  - [Chargeback Management](#chargeback-management)
  - [Payment Link Management](#payment-link-management)
- [Payment Session Status Codes](#payment-session-status-codes)
- [Response Format](#response-format)
- [Parameters Reference](#parameters-reference)
- [Webhooks](#webhooks)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [Best Practices](#best-practices)
- [Integration Examples](#integration-examples)

## Overview

The Nexzoneo Pay API allows you to integrate secure payment processing into your applications. This guide provides comprehensive documentation on how to use the API effectively, including all available parameters, status codes, and implementation patterns.

**Base URL:** `https://nexzoneo.com/api/merchant/`

## Getting Started

### Prerequisites

1. Active merchant account with Nexzoneo Pay
2. Site configured in the merchant dashboard
3. API credentials (Site Key and Security Token)
4. Server-side implementation (required for security)

### Quick Integration Steps

1. Set up your server-side endpoint
2. Configure authentication headers
3. Create payment sessions using the API
4. Handle webhook notifications
5. Test with test mode enabled

## Authentication

All API requests must include authentication headers. **Never expose credentials in client-side code.**

### Required Headers

```http
X-Site-Key: your_site_key_here
Authorization: your_security_token_here
Content-Type: application/json
```

### Security Requirements

- **All API calls must be made from your server** (PHP, Node.js, Python, etc.)
- Never include credentials in client-side JavaScript, mobile apps, or HTML
- Store credentials securely using environment variables
- Your client should call your server, which then calls our API

## API Endpoints

### Payment Session Management

#### 1. Create Payment Session

**Endpoint:** `POST /create_payment_session`

Creates a new payment session for processing customer payments.

#### Required Parameters

| Parameter        | Type   | Description                                                         |
| ---------------- | ------ | ------------------------------------------------------------------- |
| `order_id`       | string | Unique identifier for your order (must be unique per merchant site) |
| `total`          | number | Total amount to charge (positive number, supports decimals)         |
| `currency`       | string | Currency code (e.g., "USD", "EUR", "GBP")                           |
| `customer_email` | string | Customer's email address (used for receipts and notifications)      |

#### Optional Parameters

| Parameter      | Type   | Description                                                    |
| -------------- | ------ | -------------------------------------------------------------- |
| `items`        | array  | Array of order items (see [Items Structure](#items-structure)) |
| `return_url`   | string | Override site's default success redirect URL                   |
| `cancel_url`   | string | Override site's default cancellation redirect URL              |
| `callback_url` | string | Override site's default webhook URL                            |

#### Items Structure

Each item in the `items` array should contain:

```json
{
  "name": "Product Name",
  "quantity": 1,
  "unit_price": 29.99
}
```

#### Example Request

```json
{
  "order_id": "ORDER-2024-001",
  "total": 99.99,
  "currency": "USD",
  "customer_email": "customer@example.com",
  "items": [
    {
      "name": "Premium Subscription",
      "quantity": 1,
      "unit_price": 99.99
    }
  ],
  "return_url": "https://yourdomain.com/success",
  "cancel_url": "https://yourdomain.com/cancel",
  "callback_url": "https://yourdomain.com/webhook"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "session_id": 12345,
    "session_token": "sess_abc123def456...",
    "payment_url": "https://payment.nexzoneo.com/session/sess_abc123def456...",
    "reused": false
  },
  "request_id": "req_abc123",
  "duration_ms": 45.2
}
```

**Next Steps:** Redirect the customer to the `payment_url` to complete payment.

#### 2. Get Session Details

**Endpoint:** `GET /get_session?token={session_token}`

Retrieves current details about a payment session.

#### Query Parameters

| Parameter | Type   | Description                                  |
| --------- | ------ | -------------------------------------------- |
| `token`   | string | Payment session token from creation response |

#### Example Request

```http
GET /get_session?token=sess_abc123def456...
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "session": {
      "id": 12345,
      "session_token": "sess_abc123def456...",
      "external_order_id": "ORDER-2024-001",
      "customer_email": "customer@example.com",
      "amount": "99.99",
      "currency": "USD",
      "status": 2,
      "current_status": 2,
      "created_at": "2024-01-15 10:30:00",
      "completed_at": "2024-01-15 10:35:00",
      "expires_at": "2024-01-15 11:30:00",
      "payment_method": "card"
    }
  },
  "request_id": "req_def789",
  "duration_ms": 12.8
}
```

### Refund Management

#### 3. Create Refund

**Endpoint:** `POST /create_refund`

Creates a refund for a completed payment. Only payments with status `COMPLETED` or `PARTIALLY_REFUNDED` can be refunded.

#### Required Parameters

| Parameter    | Type   | Description                                                       |
| ------------ | ------ | ----------------------------------------------------------------- |
| `session_id` | number | Payment session ID to refund                                      |
| `amount`     | number | Refund amount (must be positive, up to remaining balance)         |
| `reason`     | string | Refund reason: `duplicate`, `fraudulent`, `requested_by_customer` |

#### Optional Parameters

| Parameter | Type   | Description                     |
| --------- | ------ | ------------------------------- |
| `comment` | string | Optional comment for the refund |

#### Example Request

```json
{
  "session_id": 12345,
  "amount": 29.99,
  "reason": "requested_by_customer",
  "comment": "Customer requested refund due to order cancellation"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "message": "Refund processed successfully",
    "refund_id": 456,
    "transaction_id": "txn_1234567890abcdef"
  },
  "request_id": "req_abc456",
  "duration_ms": 1250.3
}
```

#### 4. Get Refund Status

**Endpoint:** `GET /get_refund_status?refund_id={refund_id}`

Retrieves the current status of a specific refund.

#### Query Parameters

| Parameter   | Type   | Description                           |
| ----------- | ------ | ------------------------------------- |
| `refund_id` | number | Refund ID from create_refund response |

#### Example Request

```http
GET /get_refund_status?refund_id=456
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "refund": {
      "id": 456,
      "session_id": 12345,
      "payment_intent_id": "pi_1234567890abcdef",
      "transaction_id": "txn_1234567890abcdef",
      "amount": "29.99",
      "currency": "USD",
      "reason": "requested_by_customer",
      "status": "succeeded",
      "admin_comment": "Customer requested refund due to order cancellation",
      "created_at": "2024-01-15 11:00:00",
      "updated_at": "2024-01-15 11:00:15"
    }
  },
  "request_id": "req_def456",
  "duration_ms": 23.1
}
```

#### 5. List Refunds

**Endpoint:** `GET /list_refunds?session_id={session_id}`

Lists all refunds for a specific payment session.

#### Query Parameters

| Parameter    | Type   | Description                             |
| ------------ | ------ | --------------------------------------- |
| `session_id` | number | Payment session ID to query refunds for |

#### Example Request

```http
GET /list_refunds?session_id=12345
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "refunds": [
      {
        "id": 456,
        "session_id": 12345,
        "payment_intent_id": "pi_1234567890abcdef",
        "transaction_id": "txn_1234567890abcdef",
        "amount": "29.99",
        "currency": "USD",
        "reason": "requested_by_customer",
        "status": "succeeded",
        "admin_comment": "Customer requested refund",
        "created_at": "2024-01-15 11:00:00",
        "updated_at": "2024-01-15 11:00:15"
      }
    ],
    "total_refunded": "29.99"
  },
  "request_id": "req_ghi789",
  "duration_ms": 18.5
}
```

### Chargeback Management

#### 6. Create Chargeback

**Endpoint:** `POST /create_chargeback`

Creates a chargeback record for dispute management and evidence submission.

#### Required Parameters

| Parameter        | Type   | Description                                 |
| ---------------- | ------ | ------------------------------------------- |
| `session_id`     | number | Payment session ID to create chargeback for |
| `amount`         | number | Chargeback amount                           |
| `reason`         | string | Chargeback reason code                      |
| `dispute_reason` | string | Detailed reason for the dispute             |

#### Optional Parameters

| Parameter         | Type   | Description                                   |
| ----------------- | ------ | --------------------------------------------- |
| `evidence_due_by` | string | Due date for evidence submission (YYYY-MM-DD) |
| `admin_comment`   | string | Internal comment for chargeback management    |

#### Example Request

```json
{
  "session_id": 12345,
  "amount": 29.99,
  "reason": "fraudulent",
  "dispute_reason": "Customer claims unauthorized transaction",
  "evidence_due_by": "2024-02-15",
  "admin_comment": "Investigating with customer service logs"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "message": "Chargeback created successfully",
    "chargeback_id": 789,
    "status": "open"
  },
  "request_id": "req_chb123",
  "duration_ms": 156.7
}
```

#### 7. Submit Chargeback Evidence

**Endpoint:** `POST /submit_chargeback_evidence`

Submits evidence to dispute a chargeback.

#### Required Parameters

| Parameter       | Type   | Description                          |
| --------------- | ------ | ------------------------------------ |
| `chargeback_id` | number | Chargeback ID to submit evidence for |
| `evidence`      | object | Evidence documentation               |

#### Evidence Object Structure

| Field                    | Type   | Description                         |
| ------------------------ | ------ | ----------------------------------- |
| `customer_communication` | string | Customer emails, chat logs, etc.    |
| `shipping_documentation` | string | Tracking numbers, delivery proof    |
| `service_documentation`  | string | Service usage logs, screenshots     |
| `billing_documentation`  | string | Invoices, receipts, billing records |

#### Example Request

```json
{
  "chargeback_id": 789,
  "evidence": {
    "customer_communication": "Email thread showing customer satisfaction with purchase",
    "shipping_documentation": "Tracking #1234567890 - Delivered 2024-01-10",
    "service_documentation": "Customer used service for 30 days with 50+ logins",
    "billing_documentation": "Invoice #INV-001 sent and acknowledged by customer"
  }
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "message": "Evidence submitted successfully",
    "chargeback_id": 789,
    "status": "evidence_submitted"
  },
  "request_id": "req_evd456",
  "duration_ms": 234.1
}
```

#### 8. Get Chargeback History

**Endpoint:** `GET /get_chargeback_history?session_id={session_id}`

Retrieves all chargebacks for a specific payment session.

#### Query Parameters

| Parameter    | Type   | Description                                 |
| ------------ | ------ | ------------------------------------------- |
| `session_id` | number | Payment session ID to query chargebacks for |

#### Example Request

```http
GET /get_chargeback_history?session_id=12345
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "chargebacks": [
      {
        "id": 789,
        "session_id": 12345,
        "payment_intent_id": "pi_1234567890abcdef",
        "amount": "29.99",
        "currency": "USD",
        "reason": "fraudulent",
        "dispute_reason": "Customer claims unauthorized transaction",
        "status": "evidence_submitted",
        "evidence_due_by": "2024-02-15",
        "created_at": "2024-01-20 14:30:00",
        "updated_at": "2024-01-22 10:15:00"
      }
    ]
  },
  "request_id": "req_hist789",
  "duration_ms": 45.3
}
```

#### 9. Accept Chargeback

**Endpoint:** `POST /accept_chargeback`

Accepts a chargeback without disputing it.

#### Required Parameters

| Parameter       | Type   | Description             |
| --------------- | ------ | ----------------------- |
| `chargeback_id` | number | Chargeback ID to accept |

#### Optional Parameters

| Parameter | Type   | Description                         |
| --------- | ------ | ----------------------------------- |
| `reason`  | string | Reason for accepting the chargeback |

#### Example Request

```json
{
  "chargeback_id": 789,
  "reason": "Valid customer complaint - accepting chargeback"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "message": "Chargeback accepted successfully",
    "chargeback_id": 789,
    "status": "accepted"
  },
  "request_id": "req_acc321",
  "duration_ms": 123.4
}
```

### Payment Link Management

Payment Links are merchant-issued invoices a customer can pay without you building a
checkout page - useful for phone/email orders, deposits, one-off services, or B2B
invoicing. This feature must be enabled for your merchant account first (contact
support or your Nexzoneo admin); calls from a merchant without it enabled get a 403.

Creating a link does **not** email it. Pass `send_email: true` on create, or call
`send-payment-link` afterwards - this is deliberate, so integrating against this API
cannot accidentally email a real customer while you are still testing.

`amount` cannot be changed after creation (`update-payment-link` refuses it with 400).
To change the price - e.g. adding a late fee - cancel the link and create a new one
with `supersedes_link_id` pointing at the cancelled one.

#### 10. Create Payment Link

**Endpoint:** `POST /create-payment-link`

#### Required Parameters

| Parameter        | Type   | Description                              |
| ---------------- | ------ | ----------------------------------------- |
| `description`    | string | What the customer is paying for (≤255 chars), shown on the pay page |
| `amount`         | number | Positive, up to 99,999,999.99. Cannot be changed later. |
| `customer_email` | string | Where the invoice email is sent          |

#### Optional Parameters

| Parameter             | Type    | Description                                                        |
| ---------------------- | ------- | -------------------------------------------------------------------- |
| `currency`             | string  | ISO 4217 code. Defaults to `USD`.                                    |
| `customer_name`        | string  | Shown on the pay page and in the invoice email                       |
| `reference`            | string  | Your own order/invoice number, echoed back in list/get               |
| `due_at`               | date    | Cosmetic only - reminders stop after this date, the link stays payable |
| `expires_at`           | date    | Also cosmetic - does not block payment. There is deliberately no hard-expiry; cancel the link instead. |
| `supersedes_link_id`   | integer | id of a **cancelled** link you own that this one replaces (re-issue with a late fee) |
| `send_email`           | boolean | Email the invoice immediately. Default `false`.                     |

#### Example Request

```json
{
  "description": "Consultation fee — initial appointment",
  "amount": 240.00,
  "currency": "USD",
  "customer_email": "customer@example.com",
  "customer_name": "Jane Patient",
  "reference": "INV-1042",
  "due_at": "2026-10-04"
}
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "payment_link": {
      "id": 118,
      "token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "url": "https://payment.nexzoneo.com/pay/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "reference": "INV-1042",
      "description": "Consultation fee — initial appointment",
      "amount": 240.00,
      "currency": "USD",
      "customer_name": "Jane Patient",
      "customer_email": "customer@example.com",
      "status": "created",
      "payable": true,
      "cancellable": true,
      "deletable": true,
      "test": false,
      "due_at": "2026-10-04 00:00:00",
      "expires_at": null,
      "reminder_count": 0,
      "sent_at": null,
      "first_viewed_at": null,
      "paid_at": null,
      "cancelled_at": null,
      "cancel_reason": null,
      "supersedes_link_id": null,
      "created_at": "2026-09-20 12:00:00"
    }
  },
  "request_id": "req_pl001",
  "duration_ms": 22.4
}
```

**Error responses:** `400` missing/invalid field, `403` payment links not enabled for this merchant.

#### 11. Send (or Resend) Invoice Email

**Endpoint:** `POST /send-payment-link`

Emails the invoice to `customer_email`. Safe to call again on an already-sent
link - it is treated as a resend (e.g. after correcting a typo'd address) and
does **not** reset `sent_at` or the reminder schedule.

#### Required Parameters

| Parameter | Type    | Description        |
| --------- | ------- | ------------------- |
| `id`      | integer | Payment link id     |

#### Example Request

```json
{ "id": 118 }
```

#### Success Response

Same shape as Create (the updated `payment_link`, now with `sent_at` populated).

**Error responses:** `400` missing id, `404` unknown or not-yours, `409` link is
already paid or cancelled and can no longer be sent.

#### 12. List Payment Links

**Endpoint:** `GET /list-payment-links?status=&site_id=&limit=&offset=`

Scoped to your merchant account - never returns another merchant's links.

#### Query Parameters

| Parameter  | Type    | Description                                          |
| ---------- | ------- | ------------------------------------------------------ |
| `status`   | string  | Single status, or comma-separated list (e.g. `created,viewed`) |
| `site_id`  | integer | Restrict to one of your sites                          |
| `limit`    | integer | Default 50, max 200                                    |
| `offset`   | integer | Default 0                                              |

#### Example Request

```http
GET /list-payment-links?status=created,viewed&limit=20
```

#### Success Response

```json
{
  "status": "success",
  "data": {
    "success": true,
    "payment_links": [ { "id": 118, "status": "viewed", "...": "..." } ],
    "total": 1,
    "limit": 20,
    "offset": 0
  },
  "request_id": "req_pl_list",
  "duration_ms": 18.9
}
```

#### 13. Get Payment Link

**Endpoint:** `GET /get-payment-link?id={id}` or `GET /get-payment-link?token={token}`

Fetch by internal `id`, or by the `token` that appears in the public pay URL.

#### Success Response

Same shape as Create. `404` if the id/token does not exist or belongs to another merchant.

#### 14. Update Payment Link

**Endpoint:** `POST /update-payment-link`

Correction, not re-pricing. `amount` is deliberately not accepted here (400).

#### Required Parameters

| Parameter | Type    | Description    |
| --------- | ------- | -------------- |
| `id`      | integer | Payment link id |

#### Optional Parameters (any subset)

| Parameter        | Type   |
| ------------------ | ------ |
| `customer_email`   | string |
| `customer_name`    | string |
| `description`      | string |
| `reference`        | string |
| `due_at`           | date   |
| `expires_at`       | date   |

#### Example Request

```json
{ "id": 118, "customer_email": "jane.patient@example.com" }
```

**Error responses:** `400` invalid field value or an attempt to change `amount`,
`404` not found/not yours, `409` link is paid or cancelled and can no longer be edited.

#### 15. Cancel Payment Link

**Endpoint:** `POST /cancel-payment-link`

Stops the link from being payable - the public pay page returns `410 Gone`
afterwards. This is how you kill an unpaid invoice before re-issuing it (see
`supersedes_link_id` on create). Allowed even after the customer has viewed it.
Idempotent - cancelling an already-cancelled link returns 200, not an error.

#### Parameters

| Parameter | Type    | Required | Description                    |
| --------- | ------- | -------- | -------------------------------- |
| `id`      | integer | Yes      | Payment link id                  |
| `reason`  | string  | No       | Stored on the link, shown to you |

#### Example Request

```json
{ "id": 118, "reason": "Customer requested cancellation" }
```

**Error responses:** `404` not found/not yours, `409` already paid (issue a refund instead).

#### 16. Delete Payment Link

**Endpoint:** `POST /delete-payment-link` (or `DELETE`, same body)

Hard delete. Only allowed while the customer has never interacted with the
link (`status` is `created` or `sent` - never viewed, paid, or cancelled).
Anything else must be cancelled instead.

#### Required Parameters

| Parameter | Type    | Description     |
| --------- | ------- | ---------------- |
| `id`      | integer | Payment link id  |

#### Success Response

```json
{
  "status": "success",
  "data": { "success": true, "deleted": true, "id": 118 },
  "request_id": "req_pl_del",
  "duration_ms": 9.7
}
```

**Error responses:** `404` not found/not yours, `409` link has already been
viewed, paid, or cancelled - cancel it instead.

## Payment Session Status Codes

Understanding status codes is crucial for handling payment states correctly.

### Status Values

| Code | Name         | Description                                              |
| ---- | ------------ | -------------------------------------------------------- |
| `0`  | `CREATED`    | Session created, customer hasn't started payment         |
| `1`  | `PROCESSING` | Customer is actively completing payment                  |
| `2`  | `COMPLETED`  | Payment successful and confirmed                         |
| `3`  | `FAILED`     | Payment failed (card declined, insufficient funds, etc.) |
| `4`  | `EXPIRED`    | Session expired before completion                        |
| `5`  | `CANCELLED`  | Payment cancelled by customer or merchant                |

### Status vs Current Status

- **`status`**: The base status stored in the database
- **`current_status`**: The computed current status considering expiration

The `current_status` field automatically shows `4` (EXPIRED) when a session passes its expiration time, even if the database `status` is still `0` or `1`. This provides real-time status without requiring database updates for every expired session.

### Status Transitions

Valid status transitions:

- `CREATED` → `PROCESSING` → `COMPLETED`
- `CREATED` → `PROCESSING` → `FAILED`
- `CREATED` → `CANCELLED`
- `PROCESSING` → `CANCELLED`
- Any non-terminal status → `EXPIRED` (automatic)

Terminal statuses (no further changes): `COMPLETED`, `FAILED`, `EXPIRED`, `CANCELLED`

## Response Format

All API responses follow a consistent structure:

```json
{
  "status": "success|error",
  "data": {
    // Response data here
  },
  "request_id": "req_unique_id",
  "duration_ms": 45.2
}
```

### Response Headers

- `Content-Type: application/json`
- `X-Request-ID: req_unique_id`
- `X-Request-Duration: 45.2ms`

## Parameters Reference

### Currency Support

Supported currencies depend on your site configuration. Common currencies:

- `USD` - US Dollar
- `EUR` - Euro
- `GBP` - British Pound
- `CAD` - Canadian Dollar
- `AUD` - Australian Dollar

### Amount Formatting

- Use decimal numbers (e.g., `29.99`, not `2999`)
- Precision: up to 2 decimal places
- Must be positive (greater than 0)
- Minimum amount varies by currency

### URL Parameters

All URL parameters (`return_url`, `cancel_url`, `callback_url`) must:

- Be valid, accessible URLs
- Use HTTPS in production
- Accept the appropriate HTTP methods (GET for redirects, POST for webhooks)

## Webhooks

Webhooks provide real-time notifications about payment status changes.

### Webhook Payload

When a payment status changes, a POST request is sent to your callback URL:

```json
{
  "order_id": "ORDER-2024-001",
  "total": "99.99",
  "currency": "USD",
  "payment_status": "completed",
  "session_status": "completed",
  "transaction_ref": "TXN-789",
  "customer_email": "customer@example.com",
  "timestamp": "1705320900",
  "signature": "hmac_signature_here"
}
```

### Webhook Verification

Always verify webhook authenticity:

```php
<?php
$data = $_POST;
$received_signature = $data['signature'];
unset($data['signature']);

ksort($data);
$expected_signature = hash_hmac('sha256', json_encode($data), $your_security_token);

if (hash_equals($expected_signature, $received_signature)) {
    // Webhook is authentic - process the data
    $order_id = $data['order_id'];
    $payment_status = $data['payment_status'];

    // Update your database
    // Send confirmation emails
    // etc.
} else {
    // Invalid webhook - log and ignore
    error_log('Invalid webhook signature');
}
?>
```

### Payment Status Values in Webhooks

- `pending` - Payment is being processed
- `completed` - Payment was successful
- `failed` - Payment failed
- `cancelled` - Payment was cancelled
- `expired` - Payment session expired
- `partially_refunded` - Payment has been partially refunded
- `fully_refunded` - Payment has been fully refunded

### Refund Status Values

Refund status values returned in refund API responses:

- `pending` - Refund is being processed
- `succeeded` - Refund was successful
- `failed` - Refund failed
- `canceled` - Refund was canceled

## Error Handling

### Error Response Format

```json
{
  "status": "error",
  "data": {
    "error": "Error description",
    "fields": ["field1", "field2"] // For validation errors
  },
  "request_id": "req_error123",
  "duration_ms": 5.2
}
```

### Common HTTP Status Codes

| Code  | Description           | Common Causes                                 |
| ----- | --------------------- | --------------------------------------------- |
| `400` | Bad Request           | Missing/invalid parameters, invalid amounts   |
| `401` | Unauthorized          | Invalid or missing credentials                |
| `404` | Not Found             | Invalid endpoint or session not found         |
| `405` | Method Not Allowed    | Wrong HTTP method (e.g., GET instead of POST) |
| `500` | Internal Server Error | Server-side issues                            |

### Error Handling Best Practices

1. Always check the HTTP status code
2. Parse error messages for user-friendly display
3. Log errors for debugging
4. Implement retry logic for temporary failures
5. Validate data before sending requests

### Common Refund Errors

| Error Message                                        | Description                                 | Solution                                                   |
| ---------------------------------------------------- | ------------------------------------------- | ---------------------------------------------------------- |
| `Payment session not found or access denied`         | Invalid session ID or not owned by merchant | Verify session ID and ownership                            |
| `Refund amount must be greater than zero`            | Invalid refund amount                       | Check amount is positive number                            |
| `Invalid refund reason`                              | Reason not in allowed list                  | Use: `duplicate`, `fraudulent`, or `requested_by_customer` |
| `Refund not allowed: Payment is older than 120 days` | Payment processor refund deadline exceeded  | Cannot refund payments older than 120 days                 |
| `Refund amount exceeds remaining balance`            | Attempting to refund more than available    | Check remaining refundable amount first                    |

## Testing

### Test Mode

Enable test mode in your merchant site settings for safe testing:

- No real payments are processed
- Session tokens are prefixed with `TEST_`
- Use test card numbers for different scenarios

### Test Card Numbers

| Card Number        | Scenario           |
| ------------------ | ------------------ |
| `4242424242424242` | Successful payment |
| `4000000000000002` | Card declined      |
| `4000000000009995` | Insufficient funds |
| `4000000000000069` | Expired card       |
| `4000000000000127` | Incorrect CVC      |

### Testing Checklist

#### Payment Testing

- [ ] Successful payment flow
- [ ] Failed payment handling
- [ ] Webhook reception and verification
- [ ] Session expiration
- [ ] Cancel payment flow
- [ ] Error scenarios
- [ ] Status polling
- [ ] Duplicate order ID handling

#### Refund Testing

- [ ] Full refund of completed payment
- [ ] Partial refund of completed payment
- [ ] Multiple partial refunds
- [ ] Refund status checking
- [ ] Refund list retrieval
- [ ] Invalid session ID handling
- [ ] Invalid refund amount handling
- [ ] Invalid refund reason handling
- [ ] Already fully refunded payment
- [ ] Refund of test payments

#### Payment Link Testing

- [ ] Create a link, confirm it is not emailed by default
- [ ] Create with `send_email: true`, confirm the invoice arrives
- [ ] Send / resend an existing link, confirm `sent_at` does not change on resend
- [ ] Public pay page renders and marks the link `viewed`
- [ ] Attempt to change `amount` via update (expect 400)
- [ ] Cancel a link, confirm the pay page returns `410 Gone`
- [ ] Re-issue a cancelled link with `supersedes_link_id` (e.g. adding a late fee)
- [ ] Attempt to delete a link that has been viewed (expect 409)
- [ ] Cross-merchant access to another merchant's link (expect 404, not 403)
- [ ] Merchant without the feature enabled (expect 403)

## Best Practices

### Security

1. **Never expose API credentials** in client-side code
2. **Always validate webhook signatures** before processing
3. **Use HTTPS** for all URLs in production
4. **Implement proper access controls** for your webhook endpoints
5. **Log security events** for monitoring

### Performance

1. **Cache site settings** to reduce API calls
2. **Implement request timeouts** (recommended: 30 seconds)
3. **Use appropriate retry strategies** with exponential backoff
4. **Monitor API response times** and error rates

### User Experience

1. **Redirect users promptly** to payment URLs
2. **Handle all payment outcomes** (success, failure, cancel)
3. **Provide clear error messages** to users
4. **Implement loading states** during API calls
5. **Send confirmation emails** after successful payments

### Development

1. **Use test mode** during development
2. **Validate input data** before API calls
3. **Implement proper error handling** and logging
4. **Test webhook endpoints** thoroughly
5. **Monitor for duplicate payments** using order IDs

## Integration Examples

### PHP Example

```php
<?php
class Nexzoneoment {
    private $siteKey;
    private $securityToken;
    private $baseUrl = 'https://nexzoneo.com/api/merchant/';

    public function __construct($siteKey, $securityToken) {
        $this->siteKey = $siteKey;
        $this->securityToken = $securityToken;
    }

    public function createPaymentSession($orderData) {
        $url = $this->baseUrl . 'create_payment_session';

        $headers = [
            'Content-Type: application/json',
            'X-Site-Key: ' . $this->siteKey,
            'Authorization: ' . $this->securityToken
        ];

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($orderData),
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_TIMEOUT => 30
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode === 200) {
            return json_decode($response, true);
        } else {
            throw new Exception('API Error: ' . $response);
        }
    }

    public function getSession($token) {
        $url = $this->baseUrl . 'get_session?token=' . urlencode($token);

        $headers = [
            'X-Site-Key: ' . $this->siteKey,
            'Authorization: ' . $this->securityToken
        ];

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_TIMEOUT => 30
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode === 200) {
            return json_decode($response, true);
        } else {
            throw new Exception('API Error: ' . $response);
        }
    }

    public function verifyWebhook($postData, $signature) {
        $data = $postData;
        unset($data['signature']);
        ksort($data);

        $expectedSignature = hash_hmac('sha256', json_encode($data), $this->securityToken);
        return hash_equals($expectedSignature, $signature);
    }
}

// Usage Example
try {
    $nexzoneo = new NexzoneoPayment(
        $_ENV['NEXZONEO_SITE_KEY'],
        $_ENV['NEXZONEO_SECURITY_TOKEN']
    );

    $orderData = [
        'order_id' => 'ORDER-' . time(),
        'total' => 29.99,
        'currency' => 'USD',
        'customer_email' => 'customer@example.com',
        'items' => [
            [
                'name' => 'Premium Plan',
                'quantity' => 1,
                'unit_price' => 29.99
            ]
        ]
    ];

    $result = $nexzoneo->createPaymentSession($orderData);

    if ($result['status'] === 'success') {
        header('Location: ' . $result['data']['payment_url']);
        exit;
    } else {
        throw new Exception('Payment session creation failed');
    }

} catch (Exception $e) {
    error_log('Payment error: ' . $e->getMessage());
    // Show user-friendly error message
}
?>
```

### Node.js Example

```javascript
const express = require("express");
const fetch = require("node-fetch");
const crypto = require("crypto");

class Nexzoneoment {
  constructor(siteKey, securityToken) {
    this.siteKey = siteKey;
    this.securityToken = securityToken;
    this.baseUrl = "https://nexzoneo.com/api/merchant/";
  }

  async createPaymentSession(orderData) {
    const response = await fetch(this.baseUrl + "create_payment_session", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Site-Key": this.siteKey,
        Authorization: this.securityToken,
      },
      body: JSON.stringify(orderData),
      timeout: 30000,
    });

    const result = await response.json();

    if (!response.ok) {
      throw new Error(`API Error: ${result.data?.error || "Unknown error"}`);
    }

    return result;
  }

  async getSession(token) {
    const response = await fetch(
      this.baseUrl + `get_session?token=${encodeURIComponent(token)}`,
      {
        headers: {
          "X-Site-Key": this.siteKey,
          Authorization: this.securityToken,
        },
        timeout: 30000,
      }
    );

    const result = await response.json();

    if (!response.ok) {
      throw new Error(`API Error: ${result.data?.error || "Unknown error"}`);
    }

    return result;
  }

  verifyWebhook(postData, signature) {
    const data = { ...postData };
    delete data.signature;

    const sortedKeys = Object.keys(data).sort();
    const sortedData = {};
    sortedKeys.forEach((key) => {
      sortedData[key] = data[key];
    });

    const expectedSignature = crypto
      .createHmac("sha256", this.securityToken)
      .update(JSON.stringify(sortedData))
      .digest("hex");

    return crypto.timingSafeEqual(
      Buffer.from(signature, "hex"),
      Buffer.from(expectedSignature, "hex")
    );
  }
}

// Express.js integration
const app = express();
app.use(express.json());

const nexzoneo = new NexzoneoPayment(
  process.env.NEXZONEO_SITE_KEY,
  process.env.NEXZONEO_SECURITY_TOKEN
);

app.post("/create-payment", async (req, res) => {
  try {
    const orderData = {
      order_id: `ORDER-${Date.now()}`,
      total: req.body.amount,
      currency: req.body.currency || "USD",
      customer_email: req.body.email,
      items: req.body.items || [],
    };

    const result = await nexzoneo.createPaymentSession(orderData);
    res.json(result.data);
  } catch (error) {
    console.error("Payment creation error:", error.message);
    res.status(500).json({ error: "Payment creation failed" });
  }
});

app.post("/webhook", async (req, res) => {
  try {
    const isValid = nexzoneo.verifyWebhook(req.body, req.body.signature);

    if (!isValid) {
      console.error("Invalid webhook signature");
      return res.status(400).send("Invalid signature");
    }

    // Process the webhook
    const { order_id, payment_status } = req.body;
    console.log(`Payment ${order_id} status: ${payment_status}`);

    // Update your database here
    // Send confirmation emails

    res.status(200).send("OK");
  } catch (error) {
    console.error("Webhook processing error:", error.message);
    res.status(500).send("Error processing webhook");
  }
});
```

### Python Example

```python
import requests
import hmac
import hashlib
import json
from typing import Dict, Any

class Nexzoneoment:
    def __init__(self, site_key: str, security_token: str):
        self.site_key = site_key
        self.security_token = security_token
        self.base_url = 'https://nexzoneo.com/api/merchant/'

    def create_payment_session(self, order_data: Dict[str, Any]) -> Dict[str, Any]:
        url = self.base_url + 'create_payment_session'

        headers = {
            'Content-Type': 'application/json',
            'X-Site-Key': self.site_key,
            'Authorization': self.security_token
        }

        response = requests.post(
            url,
            headers=headers,
            json=order_data,
            timeout=30
        )

        result = response.json()

        if response.status_code != 200:
            raise Exception(f'API Error: {result.get("data", {}).get("error", "Unknown error")}')

        return result

    def get_session(self, token: str) -> Dict[str, Any]:
        url = f'{self.base_url}get_session?token={token}'

        headers = {
            'X-Site-Key': self.site_key,
            'Authorization': self.security_token
        }

        response = requests.get(url, headers=headers, timeout=30)
        result = response.json()

        if response.status_code != 200:
            raise Exception(f'API Error: {result.get("data", {}).get("error", "Unknown error")}')

        return result

    def verify_webhook(self, post_data: Dict[str, Any], signature: str) -> bool:
        data = post_data.copy()
        data.pop('signature', None)

        # Sort the data
        sorted_data = {k: data[k] for k in sorted(data.keys())}

        expected_signature = hmac.new(
            self.security_token.encode(),
            json.dumps(sorted_data, separators=(',', ':')).encode(),
            hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(expected_signature, signature)

# Usage example
import os
from datetime import datetime

nexzoneo = NexzoneoPayment(
    os.getenv('NEXZONEO_SITE_KEY'),
    os.getenv('NEXZONEO_SECURITY_TOKEN')
)

try:
    order_data = {
        'order_id': f'ORDER-{int(datetime.now().timestamp())}',
        'total': 29.99,
        'currency': 'USD',
        'customer_email': 'customer@example.com',
        'items': [
            {
                'name': 'Premium Plan',
                'quantity': 1,
                'unit_price': 29.99
            }
        ]
    }

    result = nexzoneo.create_payment_session(order_data)

    if result['status'] == 'success':
        payment_url = result['data']['payment_url']
        print(f'Redirect user to: {payment_url}')
    else:
        raise Exception('Payment session creation failed')

except Exception as e:
    print(f'Error: {e}')
```

---

This guide covers all aspects of using the Nexzoneo Pay API effectively. For additional support or questions, please contact our developer support team.
