# Hosted Checkout

Create a checkout session from your server and send the shopper to a payment page hosted by AuxCORE — full page, or embedded in an iframe on your site. Card data never reaches your servers. The [WooCommerce plugin](../getting-started/woocommerce-plugin.md) is built on this flow.

---

## How it works

1. **Your server creates a session** with `POST /api/v1/public/checkout-session` and your secret key. The response holds a `redirectUrl` and an `iframeUrl`.
2. **The shopper pays** on the hosted page — send them to `redirectUrl`, or load `iframeUrl` in an iframe.
3. **AuxCORE returns the shopper** to your `returnUrl`, `declineUrl` or `cancelUrl` — or to its own outcome page if you gave none, or they are not on your registered store URL.
4. **Your server fulfils the order from the webhook**, not from the return redirect: `transaction.approved` carries your `orderId`. `session.cancelled` and `session.expired` tell you the shopper did not pay.

The session lasts **10 minutes** by default. After that the shopper cannot pay on it; create a new one.

---

## Create a checkout session

```http
POST /api/v1/public/checkout-session
```

Send your **secret** key in `x-api-key`; the merchant comes from the key (ERP keys also send `X-Merchant-ID`).

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/checkout-session \
  -H "x-api-key: YOUR_SECRET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.90,
    "orderId": "ORD-1042",
    "description": "Order #1042",
    "customerEmail": "jane@example.com",
    "customerName": "Jane Doe",
    "billingAddress": {
      "line1": "123 Main St",
      "city": "Austin",
      "state": "TX",
      "postalCode": "78701",
      "country": "US"
    },
    "returnUrl": "https://shop.example.com/orders/1042/thanks",
    "declineUrl": "https://shop.example.com/orders/1042/payment-failed",
    "cancelUrl": "https://shop.example.com/cart"
  }'
```

### Request fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | number | Yes | Amount to charge; must be greater than 0 |
| `currency` | string | No | Default `USD` |
| `orderId` | string | No | Your order reference. Copied onto the transaction and into its webhooks; defaults to `wc-<sessionId>` |
| `description` | string | No | Order description |
| `customerEmail`, `customerName`, `billingPhone` | string | No | Prefill the payer's details |
| `billingAddress`, `shippingAddress` | object | No | `line1`, `line2`, `city`, `state`, `postalCode`, `country` (default `US`), `email`, `phone`, `firstName`, `lastName`, `company` |
| `lineItems` | array | No | Itemisation stored with the session: `{ "name", "quantity", "unitPrice", "total" }`. The charge comes from `amount` |
| `returnUrl` | string | No | Where to send the shopper after an approval (alias `successUrl`) |
| `declineUrl` | string | No | Where to send the shopper after a decline (alias `failureUrl`) |
| `cancelUrl` | string | No | Where to send the shopper who cancels |
| `reference`, `invoiceNumber`, `notes` | string | No | Your own references, stored with the session |
| `ttlSeconds` | integer | No | Session lifetime, from `60` up to the platform maximum (`600` by default) |
| `paymentPurpose` | string | No | Route the charge to the merchant's gateway reserved for this purpose (e.g. `donation`): 1–30 characters of `a-z`, `0-9`, `-`, `_` |
| `metadata` | object | No | Free-form data echoed back in the response. Older plugins send the fields above inside `metadata`; they are read from there when missing at the top level |

`webhookUrl` and `webhookSecret` are **rejected** (`400 UNSUPPORTED_FIELD`): webhooks are registered once per merchant with [`POST /register-webhook`](../getting-started/webhooks.md#register-with-an-api-key-public-api), not per session.

### Response

**201 Created**

```json
{
  "success": true,
  "status": "success",
  "data": {
    "sessionId": "cs_4f1d0c6b2a9e8d7c6b5a4f3e2d1c0b9a",
    "refNo": "AUX-20261008-9f3a1c2e",
    "iframeUrl": "https://uat.auxcore.net/iframe?cs=cs_4f1d0c6b2a9e8d7c6b5a4f3e2d1c0b9a",
    "redirectUrl": "https://uat.auxcore.net/vt?cs=cs_4f1d0c6b2a9e8d7c6b5a4f3e2d1c0b9a",
    "expiresAt": "2026-10-08T09:24:00.000Z",
    "metadata": null,
    "merchantLogo": null,
    "merchantName": "Acme Coffee",
    "redirectUrls": {
      "success": "https://shop.example.com/orders/1042/thanks",
      "decline": "https://shop.example.com/orders/1042/payment-failed",
      "cancel": "https://shop.example.com/cart"
    }
  }
}
```

- Use `redirectUrl` for a full-page checkout and `iframeUrl` to embed it. Use the URLs exactly as returned; their host depends on the environment.
- `redirectUrls` shows which of your redirect URLs will actually be used. `null` means that URL was dropped — see [Redirects and embedding](#redirects-and-embedding).

### Errors

| Status | `error.code` | Cause |
|--------|--------------|-------|
| `400` | `VALIDATION_ERROR` | `amount` missing or not positive, or `paymentPurpose` malformed |
| `400` | `UNSUPPORTED_FIELD` | `webhookUrl` or `webhookSecret` sent |
| `403` | `FORBIDDEN` | A `merchantId` (body, `X-Merchant-ID` or `metadata`) that the key does not belong to |
| `422` | `PAYMENT_PURPOSE_NOT_CONFIGURED` | No active card gateway serves that `paymentPurpose` |
| `429` | — | Rate limit: session creation and the shopper-side session calls share 30 requests per 15 minutes per IP ([Rate Limits](../getting-started/rate-limits.md)) |

---

## Redirects and embedding

Both depend on the merchant's **registered store URL** — the store origin saved with the merchant's WooCommerce settings in the dashboard ([Merchant setup](../getting-started/woocommerce-plugin.md#step-1-merchant-setup-auxcore-dashboard)).

- **Redirects:** `returnUrl`, `declineUrl` and `cancelUrl` are honoured only on that origin. Any other URL is dropped (it shows as `null` in `redirectUrls`) and the shopper lands on AuxCORE's outcome page instead.
- **Embedding:** the page may be framed by that origin and by the **Allowed origins** of the API key that created the session. Anywhere else the browser refuses the iframe — use `redirectUrl` instead.

Without a registered store URL, sessions still work as full-page checkouts; only merchant redirects and iframe embedding are unavailable.

---

## Confirm the payment

Never treat the return redirect as proof of payment — a shopper can open a return URL directly. Confirm on your server:

1. **Webhooks (recommended).** Subscribe to the default events with [`POST /register-webhook`](../getting-started/webhooks.md#register-with-an-api-key-public-api). Match `data.orderId` on `transaction.approved`, `transaction.declined` and `transaction.in_review` (hold the order until it is approved). [`session.cancelled` and `session.expired`](../reference/webhook-events.md#session-events) carry the `sessionId` and `orderId` of a session that was not paid.
2. **Lookup (fallback).** If no webhook arrives within a few seconds, look the payment up with `GET /api/v1/public/transaction-list?orderId=…` ([Transactions](./transactions.md)).

---

## Next steps

- [Webhook Events →](../reference/webhook-events.md)
- [Refunds →](./refunds.md)
- [WooCommerce Plugin →](../getting-started/woocommerce-plugin.md)
