# ePayDo API — Complete Reference

> Single-file reference for LLMs, SDK codegen, and AI agents. For human-readable docs visit https://epaydo.com/docs. For machine-readable schema see https://epaydo.com/openapi.json.

ePayDo is a crypto payment gateway on the TRON blockchain:
- **TRX** — native TRON token.
- **USDT (TRC-20)** — stablecoin on TRON.

Unique HD-derived deposit address per payment. Merchants integrate via one JSON API, one hosted checkout page, or a drop-in widget.

---

## Base URL

```
https://epaydo.com/api/v1
```

Sandbox and production run on the same host; the API key prefix decides the mode:
- `pk_live_...` + `sk_live_...` → live
- `pk_test_...` + `sk_test_...` → test mode (no real funds)

---

## Authentication

Every request to a merchant endpoint carries three headers:

| Header            | Description |
|-------------------|-------------|
| `X-API-Key`       | Public API key (`pk_live_...` or `pk_test_...`) |
| `X-Timestamp`     | Unix timestamp in seconds (must be within ±5 min of server time) |
| `X-Signature`     | HMAC-SHA256 hex of the canonical string (see below) |

### Canonical signing string

```
{timestamp}{METHOD}/{path}{rawBody}
```

- `timestamp` — same value as `X-Timestamp`.
- `METHOD` — uppercase HTTP verb (`GET`, `POST`, `DELETE`).
- `/path` — request path with leading slash, **including query string** (e.g. `/api/v1/payments?status=completed`).
- `rawBody` — exact raw request body for POST/PUT. Empty string for GET/DELETE.

Sign with the secret key (`sk_live_...` / `sk_test_...`):

```
X-Signature = hex(hmac_sha256(secret_key, canonical_string))
```

### Example (Node.js)

```js
import crypto from 'node:crypto';

const timestamp = Math.floor(Date.now() / 1000).toString();
const method = 'POST';
const path = '/api/v1/payments';
const body = JSON.stringify({ amount: '10.00', currency: 'USDT', order_id: 'ord_1' });

const signature = crypto
  .createHmac('sha256', SECRET_KEY)
  .update(timestamp + method + path + body)
  .digest('hex');

fetch('https://epaydo.com' + path, {
  method,
  headers: {
    'X-API-Key': API_KEY,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
    'Content-Type': 'application/json',
  },
  body,
});
```

### Example (PHP)

```php
$timestamp = (string) time();
$method    = 'POST';
$path      = '/api/v1/payments';
$body      = json_encode(['amount' => '10.00', 'currency' => 'USDT', 'order_id' => 'ord_1']);

$signature = hash_hmac('sha256', $timestamp . $method . $path . $body, $secretKey);

$response = Http::withHeaders([
    'X-API-Key'    => $apiKey,
    'X-Timestamp'  => $timestamp,
    'X-Signature'  => $signature,
])->withBody($body, 'application/json')
  ->post('https://epaydo.com' . $path);
```

### Replay protection

Signed requests are rejected if the timestamp is older than 5 minutes **or** the exact `(api_key, timestamp, signature)` triple has been seen before (Cache::add atomic guard).

### Rate limits

| Scope        | Default | Response when exceeded |
|--------------|---------|------------------------|
| Per API key  | 120 req/min (configurable per plan) | `429 Too Many Requests` with `Retry-After` |
| Payout create| 10 req/min per merchant | `429` |
| Public checkout endpoints | 60 req/min per IP+token | `429` |

---

## Resources

### Payment

Central object. Represents a payment attempt from a customer to a merchant.

| Field               | Type              | Description |
|---------------------|-------------------|-------------|
| `id`                | integer           | Internal numeric ID |
| `order_id`          | string, nullable  | Merchant-supplied external reference (max 100 chars, `A-Za-z0-9_-.:#`) |
| `public_token`      | string            | 32-char hex used in the hosted checkout URL |
| `amount`            | string (decimal)  | Amount in the payment's currency |
| `currency`          | string            | Currency code (`USDT`, `TRX`, `USD`, `EUR`, `GBP`, `TRY`) |
| `original_amount`   | string, nullable  | Amount before FX conversion |
| `original_currency` | string, nullable  | Currency before FX conversion |
| `status`            | enum              | See "Payment status" below |
| `payment_method`    | enum, nullable    | `crypto` |
| `deposit_address`   | string, nullable  | TRON Base58 address |
| `received_amount`   | string, nullable  | Actual on-chain received amount |
| `tx_hash`           | string, nullable  | Blockchain transaction hash |
| `customer_name`     | string, nullable  | Supplied by customer on checkout page |
| `customer_email`    | string, nullable  | Supplied by customer on checkout page |
| `metadata`          | object, nullable  | Arbitrary JSON (merchant-scoped, max 4KB) |
| `is_test`           | boolean           | True when the parent project is in test mode |
| `created_at`        | ISO 8601 UTC      | |
| `expiry_at`         | ISO 8601 UTC      | Payment auto-expires at this time if unpaid |
| `paid_at`           | ISO 8601 UTC, nullable | Set when status transitions to `completed` |
| `success_url`       | string, nullable  | Customer redirected here after successful payment |
| `cancel_url`        | string, nullable  | Customer redirected here after cancel / expire |
| `return_url`        | string, nullable  | Fallback if `success_url` / `cancel_url` not set |

### Payment status

| Status        | Terminal | Meaning |
|---------------|----------|---------|
| `pending`     | no       | Created, awaiting customer action or funds |
| `confirming`  | no       | Funds observed on-chain, waiting for required confirmations |
| `completed`   | yes      | Fully paid, balance credited, webhook sent |
| `paid_late`   | yes      | Funds arrived after `expiry_at` but merchant opted to accept |
| `partial`     | no       | Funds received but less than `amount`; awaiting top-up or resolution |
| `review`      | no       | Held for manual review (AML / fraud flag) |
| `expired`     | yes      | `expiry_at` passed without sufficient funds |
| `cancelled`   | yes      | Customer or merchant cancelled |
| `failed`      | yes      | Unable to complete |
| `refunded`    | yes      | Full refund issued |

---

## Endpoints

### Create payment

```
POST /api/v1/payments
```

**Body:**

| Field         | Type    | Required | Notes |
|---------------|---------|----------|-------|
| `amount`      | decimal | yes      | `0.000001` to `9999999.99` |
| `currency`    | string  | yes      | See accepted currencies in your project settings |
| `order_id`    | string  | no       | Your internal reference. Must be unique per merchant (idempotent on re-submit) |
| `customer_email` | string | no      | Pre-fills checkout page |
| `customer_name`  | string | no      | Pre-fills checkout page |
| `success_url` | url     | no       | Override project default |
| `cancel_url`  | url     | no       | Override project default |
| `return_url`  | url     | no       | Fallback redirect |
| `metadata`    | object  | no       | Up to 4 KB arbitrary JSON |
| `expiry_minutes` | integer | no    | Override default expiry (default: 30 minutes) |

**Idempotency.** Re-sending the same `order_id` within 24 h returns the existing payment (201 → 200). Send a different `order_id` or omit it to create a fresh one.

**Response 201:**

```json
{
  "id": 12345,
  "order_id": "ord_abc",
  "public_token": "f3a1...c2e9",
  "checkout_url": "https://epaydo.com/pay/f3a1...c2e9",
  "amount": "10.00",
  "currency": "USDT",
  "status": "pending",
  "is_test": false,
  "expiry_at": "2026-04-19T12:30:00Z",
  "created_at": "2026-04-19T12:00:00Z"
}
```

**Errors:**

| Code | Body                                           | Cause |
|------|------------------------------------------------|-------|
| 400  | `{"message":"amount must be a decimal..."}`    | Validation |
| 401  | `{"message":"Invalid signature"}`              | Bad HMAC / expired timestamp |
| 403  | `{"message":"Currency not enabled"}`           | Project doesn't allow this currency |
| 422  | `{"errors":{"currency":["..."]}}`              | Laravel-style validation errors |
| 429  | `{"message":"Rate limit exceeded"}`            | |

### List payments

```
GET /api/v1/payments?status=completed&from_date=2026-04-01&to_date=2026-04-19&per_page=50&is_test=false
```

Query params:

| Param     | Default | Notes |
|-----------|---------|-------|
| `status`  | any     | One of the payment status values |
| `from_date` | 30d ago | ISO date |
| `to_date`   | now     | ISO date |
| `is_test`   | false   | Set `true` to include test payments |
| `per_page`  | 15      | Max 100 |
| `page`      | 1       | |

**Response:**

```json
{
  "data": [ { /* payment objects */ } ],
  "meta": {
    "current_page": 1,
    "last_page": 4,
    "per_page": 50,
    "total": 183
  }
}
```

### Retrieve payment

```
GET /api/v1/payments/{id}
```

Returns the full payment object.

### Refund a payment

```
POST /api/v1/refunds
```

**Body:**

| Field        | Type    | Required | Notes |
|--------------|---------|----------|-------|
| `payment_id` | integer | yes      | The payment to refund. Must have a non-null `received_amount` |
| `amount`     | decimal | no       | Defaults to the full received amount. Partial refunds allowed. |
| `reason`     | string  | no       | Free text, max 500 chars |

Refunds are reviewed before execution. Only one refund may be in-flight per payment at a time. Failed refunds with no `tx_hash` may be retried.

### List refunds

```
GET /api/v1/refunds?payment_id=12345
```

### Create payout

```
POST /api/v1/payouts
```

Withdraws merchant balance to an external TRON address.

**Body:**

| Field          | Type    | Required | Notes |
|----------------|---------|----------|-------|
| `amount`       | decimal | yes      | Max `999999.99` |
| `currency`     | string  | yes      | Must match a non-zero balance |
| `to_address`   | string  | yes      | TRON Base58 address |
| `notes`        | string  | no       | |

Rate limit: 10 requests / minute / merchant.

**Response 201:**

```json
{
  "id": 789,
  "status": "processing",
  "amount": "100.00",
  "currency": "USDT",
  "estimated_completion_at": "2026-04-19T12:05:00Z"
}
```

### List / retrieve payouts

```
GET /api/v1/payouts
GET /api/v1/payouts/{id}
```

### Balance

```
GET /api/v1/balance
```

**Response:**

```json
{
  "balances": [
    { "currency": "USDT", "available": "1520.50", "pending": "120.00" },
    { "currency": "TRX",  "available": "230.00",  "pending": "0.00" }
  ]
}
```

---

## Webhooks

Configure the endpoint in **Dashboard → Webhooks**. We deliver signed POST requests for every state change.

### Signature format

```
X-ePayDo-Signature = hex(hmac_sha256(api_secret, timestamp + '.' + rawBody))
X-ePayDo-Timestamp = {unix_timestamp}
```

Note: this is **different** from the API signing format. Webhooks use `timestamp.payload` with a period; the API uses `timestamp{METHOD}/{path}{body}` with no separator.

### Verification (Node.js)

```js
import crypto from 'node:crypto';

function verify(rawBody, signatureHeader, timestampHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestampHeader + '.' + rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
```

### Retry schedule

Failed deliveries (non-2xx response or timeout at 15 s) are retried:

```
+1 min, +5 min, +15 min, +1 h, +3 h, +6 h, +12 h, +24 h
```

After 8 failures the webhook is marked `dead`. You can replay from the dashboard.

### Event types

All events have this envelope:

```json
{
  "id": "evt_01HW3...",
  "event": "payment.completed",
  "created_at": "2026-04-19T12:34:56Z",
  "livemode": true,
  "data": { /* resource object matching the event type */ }
}
```

| Event                     | `data` contains |
|---------------------------|-----------------|
| `payment.created`         | full Payment |
| `payment.confirming`      | full Payment |
| `payment.completed`       | full Payment |
| `payment.paid_late`       | full Payment |
| `payment.partial`         | full Payment |
| `payment.expired`         | full Payment |
| `payment.cancelled`       | full Payment |
| `payment.failed`          | full Payment |
| `payment.refunded`        | full Payment |
| `refund.created`          | full Refund |
| `refund.completed`        | full Refund |
| `refund.failed`           | full Refund |
| `payout.processing`       | full Payout |
| `payout.completed`        | full Payout |
| `payout.failed`           | full Payout |

### Full payload example

```json
{
  "id": "evt_01HW3KZQN8FG...",
  "event": "payment.completed",
  "created_at": "2026-04-19T12:34:56Z",
  "livemode": true,
  "data": {
    "id": 12345,
    "order_id": "ord_abc",
    "public_token": "f3a1...c2e9",
    "amount": "10.00",
    "currency": "USDT",
    "status": "completed",
    "payment_method": "crypto",
    "deposit_address": "TXy...KLp",
    "received_amount": "10.00",
    "tx_hash": "a7b2c...",
    "customer_email": "customer@example.com",
    "metadata": { "cart_id": "c_42" },
    "is_test": false,
    "created_at": "2026-04-19T12:00:00Z",
    "expiry_at": "2026-04-19T12:30:00Z",
    "paid_at": "2026-04-19T12:34:50Z"
  }
}
```

### Idempotency

Every event has a stable `id`. We may redeliver the same event; store seen IDs and return 200 for duplicates.

---

## Test mode

- Obtain `pk_test_*` / `sk_test_*` keys from **Dashboard → API Keys → Test mode**.
- Test payments never touch the real blockchain.
- To simulate completion:

```
POST /api/v1/public/payments/{public_token}/simulate-complete
```

(no auth — public endpoint, only works when `is_test=true`).

- Test mode has its own webhook endpoint and its own balance namespace.

---

## Error responses

All errors return JSON:

```json
{
  "message": "Human-readable summary",
  "error_code": "insufficient_balance",
  "details": { "available": "5.00", "requested": "10.00" }
}
```

Stable `error_code` values:

| Code                      | HTTP | Meaning |
|---------------------------|------|---------|
| `authentication_required` | 401  | Missing headers |
| `invalid_signature`       | 401  | HMAC mismatch |
| `timestamp_skew`          | 401  | Timestamp outside ±5 min |
| `replay_detected`         | 401  | Signature seen before |
| `rate_limited`            | 429  | |
| `validation_failed`       | 422  | `details` contains per-field errors |
| `currency_not_enabled`    | 403  | Project config |
| `method_not_enabled`      | 403  | Payment method disabled for project |
| `insufficient_balance`    | 422  | Payout larger than available |
| `limit_exceeded`          | 422  | Monthly tx count / volume plan cap |
| `not_found`               | 404  | |
| `idempotency_conflict`    | 409  | Same `order_id` with different amount/currency |
| `internal_error`          | 500  | Retry safe if request was idempotent |

---

## Checkout (hosted)

You don't have to render your own payment UI. After `POST /payments` returns a `checkout_url`, redirect the customer there. The page handles address display, QR codes, and polls for status.

The customer is redirected to `success_url` / `cancel_url` on completion.

---

## Widget (drop-in)

Embed the checkout in your own page:

```html
<script src="https://epaydo.com/widget.js"></script>
<button onclick="ePay.open('f3a1...c2e9')">Pay with ePayDo</button>
```

Or inline:

```html
<div id="checkout"></div>
<script>
  ePay.openInline('f3a1...c2e9', '#checkout');
</script>
```

Listen for events:

```js
window.addEventListener('epay:payment:completed', (e) => {
  console.log('Paid:', e.detail);
});
```

Allowed event detail fields: `id`, `payment_id`, `order_id`, `status`, `amount`, `currency`, `tx_hash`, `paid_at`.

---

## SDKs

- Node / TypeScript: `npm install @epaydo/node` — see https://github.com/epaydo/sdk-node
- PHP: `composer require epaydo/php` — see https://github.com/epaydo/sdk-php

---

## MCP (Model Context Protocol)

For AI agents and IDEs:

```json
{
  "mcpServers": {
    "epaydo": {
      "command": "npx",
      "args": ["-y", "@epaydo/mcp"],
      "env": {
        "EPAYDO_API_KEY": "pk_live_...",
        "EPAYDO_API_SECRET": "sk_live_..."
      }
    }
  }
}
```

Exposed tools:

- `list_payments(status?, from_date?, to_date?, limit?)`
- `get_payment(id | order_id)`
- `create_payment(amount, currency, order_id?, customer_email?, metadata?)`
- `refund_payment(payment_id, amount?, reason?)`
- `list_payouts(status?)`
- `create_payout(amount, currency, to_address, notes?)`
- `get_balance()`

---

## Changelog & versioning

- Current API version: `v1`.
- Breaking changes require a new major version (`v2`). Additive changes (new fields, new events) do not.
- Deprecated fields remain at least 12 months after announcement.
- Subscribe to https://epaydo.com/changelog.xml for Atom feed.

---

## Support

- Docs: https://epaydo.com/docs
- Status page: https://epaydo.com/status
- Email: support@epaydo.com
- Security: security@epaydo.com (PGP: https://epaydo.com/.well-known/security.txt)
