# Products & orders

Manage a merchant catalog, collect payment through hosted checkout, and connect verified orders to your fulfillment system.

> Developer preview. Deploy the updated Worker with migrations through 0011. New catalog orders require a card-only capture-on-delivery configuration. This guide describes source code, not a deployed capability. See [Delivery & payment holds](https://dashboard.atlaspay.sh/docs/delivery) and [Atlas wallet](https://dashboard.atlaspay.sh/docs/wallet) before enabling payments.

## Create and import products

Open **Get paid → Products** in the dashboard. Add a name, unique SKU, price, currency, product type, and optional description and variant. Each size/color combination is a separate sellable SKU. Products start inactive unless you explicitly enable them. Editing a price affects new orders, not an order already created.

The catalog is isolated by workspace and configured payment environment. The dashboard test-mode display toggle does not switch API credentials or move products between test and live. Only workspace owners can create, edit, or import through the dashboard.

Choose **Import CSV**, download the template, select your file, review the validated products, and confirm the import. The preview writes nothing. The confirmed batch either succeeds in full or rolls back. Imports are create-only: they never overwrite an existing SKU, even after a timeout or concurrent import. SKUs are case-sensitive.

```csv
sku,name,description,variant,price,currency,kind,active
SHIRT-BLUE-M,Classic shirt,Cotton shirt,Blue / Medium,25.00,USD,physical,false
```

Limits: 50 products per file, 256 KiB, UTF-8, one header row. Required headers are `sku`, `name`, `price`, and `currency`; the other template columns are optional. Quoted commas, escaped quotes, and embedded newlines are supported. `kind` is `physical`, `digital`, or `service`; `active` is `true` or `false`. Prices in CSV are decimal display amounts. API prices are integer Stripe charge units: USD 25.00 is `2500`, JPY 2500 is `2500`. Currency precision rules are enforced before import.

## Catalog API

Keep merchant API keys on your server. Add only the permissions your integration needs in Developers; existing payment-only keys do not automatically gain catalog or order access.

| Request | Scope | Behavior |
| --- | --- | --- |
| `GET /products?q=shirt&after=…` | `products:read` | Search by name/SKU; up to 50 products per page |
| `GET /products/{id}` | `products:read` | One product in this workspace and mode |
| `POST /products` | `products:write` | Create one product; duplicate SKU returns 409 |
| `PUT /products/{id}` | `products:write` | Replace editable fields using `product` and `expected_version` |
| `POST /products/import` | `products:write` | JSON array `products`, optional `dry_run`; 1–50 items |

List responses contain `data`, `has_more`, and `next_cursor`. Continue using `after=next_cursor`; do not treat IDs as timestamps. If an edit returns `VERSION_CONFLICT`, reload and review the latest product before submitting again. Create/import do not use payment idempotency headers: the unique SKU prevents duplicate creation. After an uncertain result, list or retrieve products to reconcile; do not assume a failed network response means no insert occurred.

Example product body:

```json
{
  "sku": "SHIRT-BLUE-M",
  "name": "Classic shirt",
  "description": "Cotton shirt",
  "variant": "Blue / Medium",
  "amount": 2500,
  "currency": "USD",
  "kind": "physical",
  "active": true
}
```

## Create a catalog order

Your server chooses the exact product IDs and quantities after the buyer reviews the merchant, variant, delivery details, and total. Atlas Pay calculates merchandise totals from the stored catalog, never from a shopper-supplied price. All items must be active and use one currency. An order stores an immutable item/price/version snapshot before requesting Stripe checkout, so a retry cannot silently use a new catalog price.

```bash
curl https://dashboard.atlaspay.sh/api/v1/orders \
  -H "Authorization: Bearer $ATLAS_PAY_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: store-order-1042-v1' \
  -d '{
    "reference": "store-order-1042",
    "items": [{"product_id":"prod_0123456789abcdef0123456789abcdef","quantity":2}],
    "shipping_amount": 500,
    "tax_amount": 250,
    "shipping_countries": ["US"],
    "success_url": "https://shop.example.com/thank-you",
    "cancel_url": "https://shop.example.com/cart"
  }'
```

Replace the example product ID and website with your own. This requires `orders:write`, enabled Stripe Connect charges, and an `Idempotency-Key`. Use the exact same key/body for retries. The merchant order reference is unique per workspace and mode; using a new key for the same reference returns 409 rather than creating a second payment. Reconcile expired or abandoned orders before deliberately issuing a new reference.

The response includes `data.id` (`ord_…`), `checkout_id`, `checkout_url`, item snapshots, `amount`, `currency`, payment status, and fulfillment status. Persist the order ID before redirecting to `checkout_url`. Replays may return a null URL after checkout finishes. There is no card charge solely because an order was created.

Orders support 1–10 distinct product lines, 1–100 units per line, and a total up to 99,999,999 charge units. Physical products require at least one permitted shipping country; Stripe collects the shipping address. Your server must compute tax and shipping before creating the order. Tax and shipping are fixed amounts displayed as separate checkout lines in this preview, not Stripe Tax calculations or carrier quotes.

## New orders: authorization, delivery, capture

New catalog orders use `capture_policy: on_delivery`. Stripe authorizes the total at checkout without capturing it. A fresh `payment_status: authorized`, known future `capture_before`, no settlement action, and no refund/dispute are required before dispatch. Confirm your delivery window fits the authorization deadline, and deduplicate dispatch by order ID.

Your trusted delivery service calls `POST /orders/{id}/delivery` with `orders:deliver`, a stable Idempotency-Key, and the actual delivery reference/timestamp. Atlas records delivery and then captures the existing full authorization. `POST /orders/{id}/cancel` with `orders:cancel` releases uncaptured holds. Neither action accepts a new amount or creates another charge. Unknown or expired authorizations require review. `order.delivered` does not mean `order.paid`; capture must still be verified.

Read the complete [delivery API, event flow and launch checklist](https://dashboard.atlaspay.sh/docs/delivery). The external delivery bot is not installed by this API implementation. The legacy `/fulfillment` endpoint below rejects capture-on-delivery orders, so it cannot trigger early capture.

## Legacy automatic-capture fulfillment

The sequence below applies only to existing orders with `capture_policy: automatic`. Payment collection remains on Stripe's hosted page.

```text
1. Buyer            -> Store backend     : Confirm item, variant and total
2. Store backend    -> Atlas Pay         : Create order (idempotency key)
3. Atlas Pay        -> D1                : Save product/price snapshot
4. Atlas Pay        -> Stripe           : Create connected-account checkout
5. Buyer            -> Stripe Checkout  : Authorize payment / authenticate
6. Stripe           -> Atlas Pay        : Signed payment event
7. Atlas Pay        -> Stripe           : Verify current session and charge
8. Atlas Pay        -> Store webhook    : Signed order.paid event
9. Store backend    -> Atlas Pay         : Retrieve current order
10. Store backend   -> Fulfillment      : Queue shipping once, by order ID
11. Fulfillment     -> Atlas Pay         : Record fulfillment and tracking
```

Use `GET /orders/{id}` with `orders:read` to refresh the current processor status. `GET /orders` is a paginated stored snapshot, not a fresh Stripe check. Before releasing goods, verify the expected order reference, items, total, currency, mode, and shipping address. Require `payment_status = paid`, `amount_refunded = 0`, and your own idempotent fulfillment check. A pending, failed, expired, disputed, refunded, or mismatched order must not automatically ship.

Atlas verifies both the Checkout Session and the succeeded, captured Stripe charge. A dispute creates a sticky hold requiring manual review; this preview has no automatic dispute-release endpoint. Even verified payment does not guarantee it can never be refunded or disputed later. Keep your store’s fulfillment and reconciliation process responsible for late changes.

After your fulfillment system has durably accepted or completed the shipment, record it:

```text
POST /orders/{id}/fulfillment
Authorization: Bearer <server-key-with-orders:write>
Content-Type: application/json
```

```json
{
  "status": "fulfilled",
  "carrier": "Your carrier",
  "tracking_number": "YOUR-TRACKING-NUMBER",
  "tracking_url": "https://carrier.example.com/track/YOUR-TRACKING-NUMBER"
}
```

Carrier and tracking fields are optional for digital goods/services. Identical fulfillment retries return the same record; a conflicting record returns 409. No separate idempotency header is needed for this transition. Atlas Pay does not buy a shipping label, ship stock, or deliver a digital file for you. The merchant system must prevent two webhook deliveries from creating two shipments.

## Order webhook events

| Event | Meaning |
| --- | --- |
| `order.authorized` | New capture-on-delivery order has a verified card hold; not captured payment |
| `order.delivered` | Trusted delivery attestation recorded; payment capture may still be pending |
| `order.canceled` | Processor canceled the authorization |
| `order.authorization_expired` | Authorization deadline passed; reconcile, do not capture or recharge |
| `order.authorization_review` | Unknown deadline or unsupported authorization; do not dispatch |
| `order.paid` | Current checkout and captured charge verified and current risk decision permits fulfillment; retrieve current state before acting |
| `order.risk_blocked` | Automatic block: stop dispatch, capture and fulfillment; reconcile any existing payment |
| `order.risk_review` | Automatic hold: review required before dispatch, capture or fulfillment |
| `order.payment_updated` | A refund or dispute hold was observed; reconcile before releasing goods |
| `order.fulfilled` | The merchant recorded fulfillment; not proof a carrier delivered it |

These use the existing API key’s signed callback and durable event feed. An order can also produce `checkout.updated` and `refund.updated`; use order IDs and stable event IDs to avoid duplicate work. Events may be delayed, duplicated, or arrive out of order. Never ship directly from an old `order.paid` payload without retrieving the current order. See [Webhooks & retries](https://dashboard.atlaspay.sh/docs/webhooks).

Refund catalog orders through `POST /refunds` using their `checkout_id` and the refund scope. Orders also refresh refund/dispute state on reads and on configured Stripe charge events. A refund created directly in Stripe still remains outside the separate merchant API refund registry. Order data and callbacks can include customer email and shipping addresses: restrict access, redact logs, and apply your retention policy.

## Saved cards and buying through Atlas chat

Add `payment_flow: "atlas_wallet"` when creating an order to return an Atlas buyer-review link instead of immediately creating Stripe Checkout. The buyer signs in, reviews the immutable total, selects their saved card and authorizes payment on Stripe. The default payment flow remains `hosted`; both flows use capture-on-delivery for new catalog orders. Already-created orders retain their original capture policy.

The wallet uses one platform Customer per buyer and environment, with an order-specific card copy for a direct connected-account payment. It includes consent, ownership checks, retry protection and removal/cleanup controls. `/wallet` and `/buy <order-link>` are private-chat entry points, not in-chat charging commands. Natural-language product discovery and native-app checkout remain follow-up work. Read the full [Atlas wallet setup and integration guide](https://dashboard.atlaspay.sh/docs/wallet) before enabling this option.

## Preview boundaries

This catalog does not yet reserve inventory, calculate tax, quote carrier shipping, group variants under a parent product, host product images, provide a public marketplace, or synchronize WooCommerce/Wix catalogs. The existing store integrations continue to use their own catalogs and checkout API. CSV and the new server-to-server product API are the import paths provided here. Availability remains subject to merchant country, Stripe capabilities, currency and payment-method eligibility—not unrestricted worldwide acceptance.

### Automatic payment risk decisions

Orders include `risk_decision`: `pending`, `allow`, `review` or `block`. Only `allow` permits normal dispatch/fulfillment, alongside every existing payment and delivery check. Retrieve the current order before acting. High processor risk blocks capture and fulfillment; elevated risk, failed checks and unavailable assessments require review. A block does not cancel or refund the payment. `order.risk_blocked` and `order.risk_review` callbacks carry the updated order snapshot and stable event IDs. A held order does not emit a new `order.authorized` or `order.paid` dispatch signal; previously sent events may still arrive late.

---

Source: [Atlas Pay — Products & orders](https://dashboard.atlaspay.sh/docs/products)

Prepared by Kyle Campbell
