# Build a payment page for your store

> Take your store's payments on a page you host, then check the signed link, read what to charge, report the payment and send the shopper back.

Source: https://www.coritan.com/docs/organizations/storefront/commerce-payment-pages/

A *payment page* is a page you host that takes your store's payments on your own payment account, with the payment provider you choose. Checkout offers it as the payment provider `external`. Your storefront sends the shopper to the page with a signed link, and the page reads from us what to charge, takes the money, reports the outcome and sends the shopper back. We place the order once the page reports a payment that succeeded.

You sell the orders paid on a payment page yourself. Coritan is not their merchant of record: we take none of their money, issue no invoice or credit note for them and book nothing on your balance for them, as [What the order looks like](#what-the-order-looks-like) describes.

The API calls a payment page a *payment connector*. Its routes are under `https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/payment-connectors`, and the API reference lists them under [Commerce](/docs/api/reference/organizations/commerce/commerce/). The dashboard does the same in **Payments**, as [Take payments on your own payment pages](/docs/organizations/commerce/payments/) describes.

## Before you begin

- Our staff turn payment pages on for a store, so ask [support](https://www.coritan.com/dashboard/support). Until then, adding a page answers `409` with `external_payments_not_enabled`, and the list of pages has `external_payments_enabled: false`.
- Adding, changing and deleting pages takes an organization API key with the `commerce.payments:write` scope, or a member's token with the Admin role or above. Reading them takes `commerce.payments:read`, or the Billing role or above.
- Host the page at an `https` address on a public host, with somewhere safe to keep two secrets.
- List `external` in the `payment_providers` of each region that should offer the page, as [Add regions](/docs/organizations/storefront/commerce-api/#add-regions) describes.
- Set the store's `storefront_url`, or have your storefront send a `return_url` and a `cancel_url` with each payment, so that the page can send the shopper back.

## How a payment goes

1. Your storefront lists the cart's payment providers, as [Take payment](/docs/organizations/storefront/store-api/#take-payment) describes. `external` is among them when a page can take the cart's payment, with the `display_name`, `description` and `brands` to show in its `config`.
2. The storefront starts a payment session with `external`, and sends the shopper to the session's `redirect_url`: your page's address with a signed link added.
3. Your page [checks the link](#check-the-signed-link), then [looks up what to charge](#look-up-what-to-charge).
4. The shopper pays on your page, with your payment provider.
5. Your page [reports the outcome](#report-the-payment). A payment that succeeded becomes the order at once.
6. Your page [sends the shopper back](#send-the-shopper-back) to the `return_url`, or to the `cancel_url` when they give up. The storefront completes the cart, which answers the order.

We trust only what your page signs. A shopper coming back to the storefront proves nothing, so the order waits for your page's report.

A cart is offered one page: the first one added that is turned on, has its secrets, is in the cart's mode and takes the cart's currency. A cart that buys or spends a gift card is offered none.

## Add the payment page

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/payment-connectors" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "livemode": false,
    "display_name": "Credit / Debit Card",
    "description": "Pay with your card, Apple Pay or Google Pay.",
    "brands": ["visa", "mastercard", "amex", "apple_pay", "google_pay"],
    "start_url": "https://pay.example.com/start/",
    "currencies": ["USD"]
  }'
```

| Field | Takes |
| --- | --- |
| `display_name` | The name the storefront shows for the page, up to 80 characters. Required. |
| `start_url` | Where the shopper is sent: `https` on a public host, up to 1,000 characters, without a `#` part, and without `session`, `expires`, `mode` or `sig` in its query, which the signed link adds. Required. |
| `livemode` | `true` for live carts, or `false` for test carts, which is the default. It never changes, so add a second page for the other mode. |
| `description` | Text the storefront shows under the name, up to 300 characters. |
| `brands` | The marks the storefront shows beside the name, from `visa`, `mastercard`, `amex`, `discover`, `apple_pay` and `google_pay`. What the page takes is up to the page. |
| `currencies` | The currencies the page takes, up to 50, such as `["USD", "EUR"]`. `null`, the default, takes any. |
| `handoff_minutes` | How long the signed link works, from 5 to 1,440 minutes. Defaults to `30`. |
| `enabled` | `false` adds the page turned off. Defaults to `true`. |
| `kind` | `signed_redirect`, the only kind, which is the default. |

The answer is `201` with the `payment_connector` and its two `secrets`, each 64 lowercase hex characters:

```json
{
  "payment_connector": {
    "id": 4,
    "kind": "signed_redirect",
    "livemode": false,
    "enabled": true,
    "display_name": "Credit / Debit Card",
    "description": "Pay with your card, Apple Pay or Google Pay.",
    "brands": ["visa", "mastercard", "amex", "apple_pay", "google_pay"],
    "start_url": "https://pay.example.com/start/",
    "currencies": ["USD"],
    "handoff_minutes": 30,
    "has_secrets": true,
    "api_base": "/api/v1/orgs/acme/commerce/payment-connectors/4",
    "lookup_url": "/api/v1/orgs/acme/commerce/payment-connectors/4/sessions/lookup",
    "events_url": "/api/v1/orgs/acme/commerce/payment-connectors/4/events",
    "created_at": "2026-10-04T11:40:00Z",
    "updated_at": "2026-10-04T11:40:00Z"
  },
  "secrets": {
    "outbound_secret": "0f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a6978",
    "inbound_secret": "8a7b6c5d4e3f20118a7b6c5d4e3f20118a7b6c5d4e3f20118a7b6c5d4e3f2011"
  }
}
```

Put both secrets in your page's settings, with the `lookup_url` and the `events_url` on `https://api.coritan.com`. The page checks the links we send it with the `outbound_secret`, and signs its calls to us with the `inbound_secret`.

> [!WARNING]
> Only this answer shows the secrets, and we keep them sealed. `has_secrets` says that a page has them, but no route shows them again. If they are lost, rotate them.

A store has up to 10 payment pages. Beyond that, adding one answers `409` with `limit_reached`.

### Change, turn off or delete a page

- `GET /commerce/payment-connectors` lists the pages, oldest first, with `external_payments_enabled`. `GET /commerce/payment-connectors/{connector_id}` answers one.
- `PATCH /commerce/payment-connectors/{connector_id}` changes the fields you send, with the same rules, and `"description": null` removes the description. `livemode` and `kind` cannot change, which answers `422`.
- `"enabled": false` turns the page off. No new cart is offered it, and the page can still look up and report the payments already started on it.
- `POST /commerce/payment-connectors/{connector_id}/rotate-secrets` makes two new secrets and answers them with the `payment_connector`, this once. The old ones stop working at once: until the page has the new ones, the links we send fail its check and we refuse what it signs.
- `DELETE /commerce/payment-connectors/{connector_id}` answers `{"deleted": true}`. While a shopper may still be paying on the page, it answers `409` with `connector_in_use`, and `open_sessions` counts those payments. A payment that is still open counts until an hour after its link stops working, so turn the page off instead, and delete it once they are done.

> [!CAUTION]
> Deleting a page and rotating its secrets cannot be undone. A deleted page can no longer look up or report a payment.

## Check the signed link

Checkout sends the shopper to the page's `start_url` with four parameters added, after a `?`, or after a `&` when the address already has a query:

```text
https://pay.example.com/start/?session=cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2&expires=1791117000&mode=test&sig=3c3c43e567a4e1b6cd13ea8d0fd24dcbd529de26fd7ffb1b056b803cf8c7b6ac
```

| Parameter | Holds |
| --- | --- |
| `session` | The payment's reference. Treat it as opaque text of up to 128 letters, digits, `_`, `.` and `-`. |
| `expires` | When the link stops working, in Unix seconds: `handoff_minutes` after the shopper was sent. |
| `mode` | `live` or `test`, the cart's mode. |
| `sig` | The lowercase hex HMAC-SHA256 of `<session>.<expires>.<mode>`, keyed with the outbound secret. |

Before you show the shopper anything, compute `sig` again and compare the two in constant time, and refuse a link whose `expires` has passed. When one page serves both a test and a live payment page, check each link with the secret of its `mode`.

```python
import hashlib
import hmac
import time


def link_is_valid(outbound_secret: str, session: str, expires: str, mode: str, sig: str) -> bool:
    message = f"{session}.{expires}.{mode}".encode()
    expected = hmac.new(outbound_secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig.lower()) and expires.isdigit() and time.time() <= int(expires)
```

To test your check, use the example `outbound_secret` above with the link above. It signs the text `cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2.1791117000.test`, which gives its `sig`. Its `expires` is Oct 4, 2026, 12:30 UTC, so the signature matches but the link has expired.

When the shopper starts paying again for the same total, checkout sends a new link for the same `session`, with a later `expires` and a new `sig`. When the cart changes, the next link names a new `session`, and the old one looks up as `canceled`.

## Sign your requests

Your page makes two calls to us, each a `POST` with a JSON body of up to 256 KB. Sign each one with the inbound secret in the `X-Coritan-Signature` header:

```text
X-Coritan-Signature: t=1791115204,v1=a438d205245d8ee44124b41ac8090e9fbb1a0996130b8ebfade7e9321c06a80b
```

`t` is when you sign, in Unix seconds. `v1` is the lowercase hex HMAC-SHA256 of `t`, a full stop and the raw body, keyed with the inbound secret. Sign the exact bytes you send, because a body encoded again after signing no longer matches. We refuse a `t` more than 5 minutes from our clock, so sign each request as you send it, retries included. A header can carry more than one `v1`, and one that matches is enough.

```python
import hashlib
import hmac
import json
import time


def signed(inbound_secret: str, payload: dict) -> tuple[bytes, str]:
    body = json.dumps(payload, separators=(",", ":")).encode()
    t = int(time.time())
    v1 = hmac.new(inbound_secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return body, f"t={t},v1={v1}"
```

With the example `inbound_secret` above, the `t` `1791115204` and the body `{"session":"cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2"}`, the signed bytes are `1791115204.{"session":"cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2"}`, which give the header above.

Instead of a signature, a call can carry an organization API key with `commerce.payments:write` in `X-API-Key`. We then check the key and ignore any signature. The inbound secret signs only this page's two calls, while the key can also change and delete the store's pages.

A page that is turned off still answers both calls, so that it can finish the payments already started on it.

| Answer | Why |
| --- | --- |
| `401` with `authentication_required` | The call has neither a signature nor a key. |
| `401` with `invalid_signature` | The header is not `t=<unix seconds>,v1=<hex signature>`, the signature does not match the inbound secret, or the page has no inbound secret. |
| `401` with `signature_expired` | `t` is more than 5 minutes from our clock. |
| `413` with `payload_too_large` | The body is over 256 KB. |

## Look up what to charge

Look the session up once the link checks out, and again just before you take the money, because the cart can change while the shopper is on your page.

```bash
BODY='{"session":"cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2"}'
T=$(date +%s)
V1=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$INBOUND_SECRET" | sed 's/^.*= //')

curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/payment-connectors/4/sessions/lookup" \
  -H "Content-Type: application/json" \
  -H "X-Coritan-Signature: t=$T,v1=$V1" \
  --data-binary "$BODY"
```

The answer is the `payment_page.v1` document:

```json
{
  "schema": "payment_page.v1",
  "session": "cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2",
  "status": "open",
  "livemode": false,
  "amount": 5896,
  "currency_code": "USD",
  "email": "alex@example.com",
  "customer": {"first_name": "Alex", "last_name": "Morgan", "phone": "+12025550143"},
  "billing_address": {"first_name": "Alex", "last_name": "Morgan", "company": "", "address_1": "350 Fifth Avenue", "address_2": "", "city": "New York", "province": "NY", "postal_code": "10118", "country_code": "US", "phone": "+12025550143"},
  "shipping_address": {"first_name": "Alex", "last_name": "Morgan", "company": "", "address_1": "350 Fifth Avenue", "address_2": "", "city": "New York", "province": "NY", "postal_code": "10118", "country_code": "US", "phone": "+12025550143"},
  "lines": [
    {"kind": "item", "sku": "MUG-BLU", "title": "Mug (Blue)", "quantity": 2, "unit_price": 2000, "subtotal": 4000, "discount_total": 400, "total": 3600, "tax_total": 297}
  ],
  "shipping_total": 1500,
  "fee_lines": [{"code": "shipping_protection", "label": "Shipping protection", "total": 499, "tax_total": 0}],
  "tax_total": 297,
  "adjustment_total": 0,
  "return_url": "https://shop.example.com/checkout",
  "cancel_url": "https://shop.example.com/checkout",
  "expires_at": "2026-10-04T12:30:00Z",
  "reference_label": "Cart cart_01j8z4a7b2c9d3e6f5g8h1k0m4"
}
```

| Field | Holds |
| --- | --- |
| `status` | Whether to take the money, as the next table says. |
| `livemode` | `true` for a live cart. |
| `amount`, `currency_code` | What to charge, in the currency's smallest unit, such as cents. |
| `email`, `customer` | The shopper's email address, and the `first_name`, `last_name` and `phone` of the billing address, or of the shipping address when the cart has no billing address. |
| `billing_address`, `shipping_address` | The cart's addresses, with every key present and `""` for a missing value. `billing_address` is the shipping address when the cart has no billing address. |
| `lines` | Each item in the cart: its `kind`, which is `item`, or `gift` for one a promotion added free; its `sku`; its `title`, with the variant in brackets; the `quantity`, `unit_price`, `subtotal` and `discount_total`; the `total` after discounts and before tax; and its `tax_total`. |
| `shipping_total` | Shipping after its discounts, before tax. |
| `fee_lines` | The [store fees](/docs/organizations/storefront/store-api/#offer-store-fees) the shopper chose, each with its `code`, `label`, `total` before tax and `tax_total`. |
| `tax_total` | All the tax on the cart. |
| `adjustment_total` | Whatever makes the parts add up to `amount`. |
| `return_url`, `cancel_url` | Where to send the shopper back. |
| `expires_at` | When the link stops working. |
| `reference_label` | A label for the payment in your provider's records. |

The parts always add up to `amount` exactly:

```text
sum of lines[].total + shipping_total + sum of fee_lines[].total + tax_total + adjustment_total = amount
```

When your provider takes a breakdown of the payment, send it these parts, with an `adjustment_total` that is not `0` as a line of its own.

| `status` | What to do |
| --- | --- |
| `open` | Take the payment. |
| `paid` | Your page already reported this payment as succeeded. Take nothing more, and send the shopper to the `return_url`. |
| `failed` | Your page reported that the payment failed. Take nothing more: the shopper starts again at checkout, which sends a new link. |
| `canceled` | The cart has another payment now, or it changed, closed or became an order. Take nothing, and send the shopper to the `cancel_url`. |
| `expired` | The link's time is up. Take no new payment, and send the shopper to the `cancel_url` to start again. A payment you already took can still be reported. |

A `session` that this page did not issue answers `404` with `not_found`, and a body that is not `{"session": "<reference>"}` answers `422` with `invalid`.

## Report the payment

Report each outcome to the `events_url`, signed in the same way:

```bash
BODY='{"id":"pay-77810","type":"payment.succeeded","session":"cps_1042_k3m9q2x7v5b8n4r6t1w0z8y2","amount":5896,"currency_code":"USD","reference":"ch_77810","occurred_at":"2026-10-04T12:02:30Z"}'
T=$(date +%s)
V1=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$INBOUND_SECRET" | sed 's/^.*= //')

curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/payment-connectors/4/events" \
  -H "Content-Type: application/json" \
  -H "X-Coritan-Signature: t=$T,v1=$V1" \
  --data-binary "$BODY"
```

With the example `inbound_secret`, that body signed at `t` `1791115351` gives the `v1` `91f9caf8bd2e5a75723fea54e3df5bece12d0976cbd3f1bddc5daf7a1e260959`.

| Field | Takes |
| --- | --- |
| `id` | Your own ID for the event, unique for the page, up to 128 characters. A number counts as text. Required. |
| `type` | `payment.succeeded`, `payment.failed` or `payment.pending`. Required. |
| `session` | The `session` from the link. Required. |
| `amount`, `currency_code` | What the shopper paid, in the currency's smallest unit, and its currency. Required with `payment.succeeded`. |
| `reference` | Your provider's reference for the payment, up to 255 characters. The order's payment keeps it as `page_reference`. |
| `message` | A note of up to 1,000 characters, such as why the payment failed. |
| `occurred_at` | When it happened, as text of up to 64 characters, which we keep as you send it. |

What each `type` does:

- `payment.succeeded`: the payment is paid, and we place the order at once. A success can follow a `payment.failed` for the same `session`, as long as checkout has not started another payment for the cart since.
- `payment.failed`: the payment fails, with your `message` as the reason. Completing the cart answers `payment_failed`, and the shopper can pay again.
- `payment.pending`: we note it, and completing the cart answers `payment_processing` until you report the outcome.

The answer is `200`:

```json
{"status": "processed", "order_id": "order_01j8z3k4m5n6p7q8r9s0t1v2w3"}
```

| `status` | What it means |
| --- | --- |
| `processed` | We acted on it. For a success, `order_id` is the order's `order_` ID, or `null` while we are still placing it. |
| `duplicate` | The page reported an event with this `id` before. Nothing changes, and `order_id` names the order once there is one. |
| `ignored` | Nothing changes: a `payment.failed` or `payment.pending` for a payment that is closed, or a success the page reported before, with the same amount, currency and reference. |
| `unmatched` | The payment succeeded but cannot pay for the cart's order. We keep it for you to refund, as [Refund what could not become an order](#refund-what-could-not-become-an-order) describes. |

Send an event again with the same `id` until it answers `200`, because a repeat never counts twice. A `404` or a `422` keeps nothing, so correct the event and send it again.

## Send the shopper back

After the shopper pays, send them to the `return_url`, and when they give up, to the `cancel_url`. Both are on your storefront: it sends them when it starts the payment, and they default to `<storefront_url>/checkout`.

The storefront then completes the cart, as [Complete the order](/docs/organizations/storefront/store-api/#complete-the-order) describes. Once your page has reported the success, completing answers the order. Until then it answers `409` with `payment_processing`, while your page reports the payment as pending, or for 15 minutes after the shopper was last sent to your page. After that it answers `409` with `payment_requires_action`, and the shopper can pay again. A success your page reports later still places the order, unless the cart was paid another way or changed in the meantime.

## What the order looks like

An order paid on a payment page is yours to sell:

- Its `seller_of_record` is `merchant` and its `seller_entity_key` is `null`, where the store's other orders name a Coritan company.
- We issue no invoice or credit note for it, so its `documents` stay empty, and the Store API's `sold_by` is `null`.
- We book nothing on the store's balance or ledger for it: no sale, no commission and no processing fee. Its tax is worked out as on any cart, and the [tax report](/docs/organizations/storefront/commerce-payouts/#download-the-tax-report) leaves it out, so account for it yourself.
- Its payment has the `provider` `external`, and your page's `reference` as its `page_reference`.
- With [payment routing](/docs/organizations/storefront/commerce-payment-routing/) on, its `payment_route` says which route paid for it.
- We send `commerce.order.placed` and `commerce.payment.captured` as for any order, and email the shopper the order confirmation.

### Refund an order paid on a payment page

Return the money to the shopper at your payment provider, then record the refund with `POST /commerce/orders/{order_id}/refunds`, as [Refund or cancel an order](/docs/organizations/storefront/commerce-api/#refund-or-cancel-an-order) describes. We move no money: the refund is `succeeded` at once, with a `gateway_refund_id` that starts with `external_refund_`, and no credit note. What is left to refund goes down, the order's timeline says to return the money at the payment page's provider, and we email the shopper the refund notice and send `commerce.payment.refunded`. Cancelling the order records the refund of what is left in the same way.

### Refund what could not become an order

We never refund a payment that answered `unmatched`, so refund it at your payment provider. The event's `detail.reason`, in [the page's events](#read-what-a-page-reported), says why it could not pay for the order:

| `reason` | What happened |
| --- | --- |
| `second_payment` | The page already reported a success for this payment, and this one has another amount, currency or reference. |
| `amount_mismatch` | The amount or the currency is not the payment's. |
| `cart_missing` | The cart no longer exists. |
| `already_ordered` | The cart already became an order. |
| `session_replaced` | Checkout started another payment for the cart, on your page or another way to pay. |
| `cart_not_open` | The cart was closed without an order. |
| `paid_otherwise` | Another payment for the cart already succeeded. |
| `cart_changed` | The cart's total or currency changed after the payment started. |
| Another code, such as `insufficient_inventory` | We could not place the order, for the reason the code names. `order_not_placed` gives no reason. |

When the cart already has an order, the payment joins it: the order's `payments` hold it with `unmatched: true`, and once you have refunded it, you record that with its `payment_id`, as [Refund or cancel an order](/docs/organizations/storefront/commerce-api/#refund-or-cancel-an-order) describes. Otherwise there is no order to record it on, and we close the payment.

For each one, we send the `commerce.payment.refund_required` webhook:

```json
{
  "id": "evt_48230",
  "type": "commerce.payment.refund_required",
  "livemode": false,
  "created_at": "2026-10-04T12:41:07Z",
  "resource": {"type": "cart", "id": "cart_01j8z4a7b2c9d3e6f5g8h1k0m4"},
  "data": {"cart": "cart_01j8z4a7b2c9d3e6f5g8h1k0m4", "session_id": 1042, "order_id": null, "amount": 5896, "currency_code": "USD", "reference": "ch_77811", "reason": "cart_changed", "livemode": false}
}
```

`resource` is the order when the payment joined one, and `order_id` is then its numeric `id`. Otherwise both name the cart. `reference` is your page's reference for the payment.

## Read what a page reported

`GET /commerce/payment-connectors/{connector_id}/events` lists what the page reported, newest first, 50 at a time and up to 200 with `limit`. Filter it by `status` (`processed`, `ignored` or `unmatched`) and by `type`, and page back by sending `next_before_id` as `before_id`.

```bash
curl "https://api.coritan.com/api/v1/orgs/acme/commerce/payment-connectors/4/events?status=unmatched" \
  -H "X-API-Key: $ORG_API_KEY"
```

Each event has the page's `id` as its `event_id`, with its `type`, `session_id`, `status` and `created_at`. Its `detail` holds what the page sent (`reference`, `amount`, `currency_code`, `message` and `occurred_at`) and, once known, the `reason` and the `order_id`. A repeat that answered `duplicate` is not listed.

## Result

Checkout offers your page as `external` to carts of its mode and currency in the regions that list it. A shopper who pays there comes back to an order whose `seller_of_record` is `merchant`, and the page's events show each payment it reported.

## Troubleshooting

`409` with `external_payments_not_enabled`
: Our staff have not turned payment pages on for the store. Ask [support](https://www.coritan.com/dashboard/support). When they turn them off, your pages stay, but no cart is offered them and no page can be added.

`409` with `limit_reached`
: The store has 10 payment pages. Delete one you no longer use.

`422` with `field` set to `start_url`
: The `message` says why: the address is not `https`, its host is not public, it has a `#` part, or it carries a parameter that the link adds.

`409` with `connector_in_use`
: A shopper may still be paying on the page. Send `"enabled": false` instead, and delete the page an hour after its last link stops working.

The storefront is not offered `external`
: Check that our staff turned payment pages on, that the cart's region lists `external`, that a page that is turned on is in the cart's mode and takes its currency, and that the cart neither buys nor spends a gift card. With [payment routing](/docs/organizations/storefront/commerce-payment-routing/) on, the cart's route must also list `external`.

`401` with `invalid_signature`
: Sign the exact bytes you send, with the inbound secret rather than the outbound one, and with the secrets of the latest rotation.

`401` with `signature_expired`
: Your server's clock is more than 5 minutes off, or the request was signed long before it was sent. Correct the clock, and sign each request as you send it.

`404` with `not_found`
: The page in the address does not exist, or this page did not issue the `session`. Use the `lookup_url` and `events_url` of the page whose secret checked the link.

`404` with `commerce_not_enabled`
: The organization has no store, or its store is disabled.

Completing the cart answers `payment_processing` after the shopper paid
: We have not had your page's `payment.succeeded`. Check that the page reports to the `events_url`, signs with the inbound secret and gets a `200`.

Links fail the page's check after a rotation
: Links sent before the rotation were signed with the old outbound secret. The shopper starts the payment again at checkout for a new link.

## Related

- [Take payments on your own payment pages](/docs/organizations/commerce/payments/)
- [Route carts between ways to pay](/docs/organizations/storefront/commerce-payment-routing/)
- [Build a checkout with the Store API](/docs/organizations/storefront/store-api/)
- [Receive organization webhooks](/docs/organizations/webhooks/)

## API

- `GET /api/v1/orgs/{org_slug}/commerce/payment-connectors`: List the store's payment connectors, oldest first (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-payment-connectors)
- `POST /api/v1/orgs/{org_slug}/commerce/payment-connectors`: Add a payment page the store hosts, and get its two secrets (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-payment-connectors)
- `GET /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}`: Read one payment connector, with the addresses its page calls (lookupurl and eventsurl) (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}`: Change what a connector shows and where it sends shoppers, or turn it off (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}`: Delete a payment connector (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id)
- `GET /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}/events`: List the events the connector's page reported, newest first (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id-events)
- `POST /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}/events`: Report a payment's outcome on the payment page (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id-events)
- `POST /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}/rotate-secrets`: Replace both secrets of a connector and get the new ones, shown this once (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id-rotate-secret)
- `POST /api/v1/orgs/{org_slug}/commerce/payment-connectors/{connector_id}/sessions/lookup`: Look up what to charge for a hand-off, as the payment page does before taking any money (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-payment-connectors-connector-id-sessions-look)
