# Hosted Page API
Source: https://paylinky.vercel.app/docs/hosted-page

Create payment links and let customers pay on a Paylinky-hosted checkout.

# Hosted Page API

Create a link on your server, redirect the customer to it, and let Paylinky handle country selection, operator, phone number, OTP steps and crypto addresses.

## Flow

1. Your backend calls `POST /api/v1/payment-links`.
2. Paylinky returns a `payment_link` (valid 30 minutes by default).
3. Redirect your customer to `payment_link`.
4. The customer pays on the hosted page (Mobile Money or crypto).
5. Paylinky sends a signed `payment.completed` / `payment.failed` webhook to your server.
6. The customer's browser is redirected to your `success_url` (if set).

## Create a link

```http
POST https://paylinky.vercel.app/api/v1/payment-links
```

| Field | Required | Description |
| - | - | - |
| `title` | Yes* | Short title shown to the customer (*or `description`) |
| `description` | No | Longer text |
| `amount` | If fixed | Amount in `currency` |
| `currency` | Yes | `XAF`, `XOF`, `CDF`, `NGN`, `GHS`, `KES`… — must match a country in `/countries` |
| `is_fixed_amount` | No | Default `true`. If `false` the customer enters the amount |
| `allowed_countries` | No | Array of country codes, e.g. `["CM"]` |
| `expires_in_minutes` | No | Default `30`; `0` or `null` = never; max `525600` |
| `reference` | No | Your order reference |
| `success_url` / `cancel_url` | No | Redirect targets after a final result |
| `Idempotency-Key` header | No | Same key returns the same link (`reused: true`) |

:::tabs
```bash cURL
curl -X POST https://paylinky.vercel.app/api/v1/payment-links \
  -H "Authorization: Bearer $PAYLINKY_KEY" \
  -H "Idempotency-Key: order-12345" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Order #12345",
    "amount": 10000,
    "currency": "XAF",
    "is_fixed_amount": true,
    "allowed_countries": ["CM"],
    "success_url": "https://shop.example/thanks"
  }'
```
```javascript Node.js
const res = await fetch("https://paylinky.vercel.app/api/v1/payment-links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAYLINKY_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "order-12345",
  },
  body: JSON.stringify({ title: "Order #12345", amount: 10000, currency: "XAF", allowed_countries: ["CM"] }),
});
const { payment_link } = await res.json();
// redirect the customer: res.redirect(payment_link)
```
```python Python
r = requests.post("https://paylinky.vercel.app/api/v1/payment-links",
    headers={"Authorization": f"Bearer {os.environ['PAYLINKY_KEY']}", "Idempotency-Key": "order-12345"},
    json={"title": "Order #12345", "amount": 10000, "currency": "XAF", "allowed_countries": ["CM"]})
payment_link = r.json()["payment_link"]
```
```php PHP
$ch = curl_init("https://paylinky.vercel.app/api/v1/payment-links");
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("PAYLINKY_KEY"), "Content-Type: application/json", "Idempotency-Key: order-12345"],
  CURLOPT_POSTFIELDS => json_encode(["title" => "Order #12345", "amount" => 10000, "currency" => "XAF"]),
]);
$link = json_decode(curl_exec($ch), true)["payment_link"];
```
:::

Response `201`:

```json
{
  "success": true,
  "payment_id": "5d2f3c0a-8f0e-4f9b-9d7e-0c1a2b3c4d5e",
  "slug": "pl-k3m9x2q7ab",
  "payment_link": "https://paylinky.vercel.app/pay/pl-k3m9x2q7ab",
  "title": "Order #12345",
  "description": null,
  "is_fixed_amount": true,
  "amount": 10000,
  "currency": "XAF",
  "allowed_countries": ["CM"],
  "status": "active",
  "payments_count": 0,
  "reference": null,
  "sandbox": false,
  "expires_at": "2026-01-01T10:30:00.000Z",
  "created_at": "2026-01-01T10:00:00.000Z",
  "reused": false
}
```

## Retrieve a link

```http
GET https://paylinky.vercel.app/api/v1/payment-links/{payment_id_or_slug}
```

Returns the same object plus `paid_at` (time of the confirmed payment, or `null`).

### Link statuses

| Status | Meaning |
| - | - |
| `active` | Can be paid |
| `paid` | A fixed-amount link that has been paid (single use) |
| `expired` | Past `expires_at` — the hosted page shows "link expired" |
| `disabled` | Disabled by the merchant or an administrator |

Variable-amount links (`is_fixed_amount: false`) stay `active` after payments and increment `payments_count`.

## Return URLs

After a final result, the hosted page redirects to `success_url` with `?reference=PLK-…&status=success`. **Never fulfil an order from this redirect alone** — use the webhook or `GET /transaction/{id}`.

:::note
Payment links created with a sandbox key (`plk_test_…`) open a sandbox checkout banner and never move real money.
:::
