# Accept payments (/docs/guides/accept-payments)

This guide shows you how to sell from your own server with the API. You create a cart, pay for it, handle every result the payment can come back with, and find the order that the payment makes. At the end you can also capture or void a payment you only authorized, and pay with a saved card, PayPal or Apple Pay.

The examples use a test secret token, `$COMECERO_TEST_TOKEN`, so no money moves. See [Account and test mode](/docs/getting-started/create-an-account) to get one and for the full list of test cards.

<Callout type="warn">
Card numbers that pass through your server bring your server into PCI DSS scope. If the shopper types the card in a browser, take the payment from the browser with a limited token instead, so the card goes from the browser to Comecero and never reaches your server. The calls are the same. See [Authentication](/docs/basic-concepts/authentication).
</Callout>

## How a checkout works [#how-a-checkout-works]

1. Create a **cart** with the items and the customer. Comecero works out tax, shipping and discounts.
2. Create a **payment** on the cart. The amount and currency come from the cart.
3. When the payment succeeds, the cart closes and becomes an **order** with the id in the cart's `order_id`.

There is no endpoint that creates an order directly. Orders come only from paid carts and paid invoices.

## Create a cart and pay for it [#create-a-cart-and-pay-for-it]

<Steps>
<Step>

### Create the cart [#create-the-cart]

Send the items and the customer to [Create a cart](/api/carts/create-cart). Every item needs a `product_id` of a product in your catalog.

```bash title="Request"
curl https://api.comecero.com/api/v1/carts \
  -H "Authorization: Bearer $COMECERO_TEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "USD",
    "items": [{ "product_id": "pdf-pro.test", "quantity": 1 }],
    "customer": {
      "name": "John Smith",
      "email": "john@example.com",
      "billing_address": { "country": "US", "postal_code": "27265" }
    }
  }'
```

```json title="Response"
{
  "object": "cart",
  "cart_id": "0091307952465CafhFSTa1",
  "order_id": "8352-CSGYX-348",
  "open": true,
  "currency": "USD",
  "subtotal": 99.95,
  "tax": 7.5,
  "total": 107.45
}
```

A secret or trusted token may set the price of an item with `reference_price` and `reference_currency`. A limited token may not (`403`).

</Step>
<Step>

### Check what the payment needs [#check-what-the-payment-needs]

[Get the cart options](/api/carts/get-cart-options) before you show the payment step. It lists the payment method types that can pay for this cart (`payment_methods`), the card brands your gateways take (`card_types`), the shipping methods the shopper can pick (`shipping_quotes`) and the customer fields that must be set before a payment (`customer_required_fields`, such as `billing_address.postal_code`). A payment on a cart that is missing a required field is refused with `400`.

</Step>
<Step>

### Pay for the cart [#pay-for-the-cart]

Send the payment method to [Pay for a cart](/api/carts/create-cart-payment). Do not send `total` or `currency`: they come from the cart.

<Tabs items={['curl', 'Node.js']}>
<Tab value="curl">

```bash
curl https://api.comecero.com/api/v1/carts/0091307952465CafhFSTa1/payments \
  -H "Authorization: Bearer $COMECERO_TEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_method": {
      "type": "credit_card",
      "data": {
        "number": "4444555566661111",
        "exp_month": 12,
        "exp_year": 2030,
        "cvv": "123",
        "cardholder_name": "John Smith"
      }
    }
  }'
```

</Tab>
<Tab value="Node.js">

```js
const res = await fetch(`https://api.comecero.com/api/v1/carts/${cartId}/payments`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.COMECERO_TEST_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    payment_method: {
      type: 'credit_card',
      data: {
        number: '4444555566661111',
        exp_month: 12,
        exp_year: 2030,
        cvv: '123',
        cardholder_name: 'John Smith',
      },
    },
  }),
});
const body = await res.json();
```

</Tab>
</Tabs>

```json title="Response"
{
  "object": "payment",
  "payment_id": "0090460891496PyrYzd9DC",
  "success": true,
  "status": "completed",
  "currency": "USD",
  "total": 107.45,
  "order": "https://api.comecero.com/api/v1/orders/8352-CSGYX-348"
}
```

</Step>
</Steps>

## Handle the result [#handle-the-result]

Look at the HTTP status first, then the payment's `status`.

| Response | `status` | What it means | What you do |
|---|---|---|---|
| `201` | `completed` | Paid and captured. The order exists. | Show the receipt. |
| `201` | `pending` | Authorized only, not captured. The order exists. | Capture or void it later (see below). |
| `201` | `processing` | The gateway answers later, for example ACH. | Wait for the `payment:completed` or `payment:created_failure` webhook. |
| `201` | `initiated` | The shopper must act first: approve at PayPal or pass a 3-D Secure check. The cart stays open. | Follow `response_data`. When the shopper is back, get the payment and commit it only while it is still `initiated`. |
| `402` | | Declined. | Show the message and let the shopper try another method. |
| `422` | | The payment cannot be made now, for example the shopper's location is blocked. | Show the message. |
| `409` | | The same payment was sent again soon after a success (code `duplicate`). | Read the cart: it is already paid. |
| `502`, `504` | | The gateway answered with an error, or did not answer in time. | Get the payment before you try again. It may have gone through. |

### Declines [#declines]

A declined payment comes back as `402` with type `transaction_error`. The failed payment is in `error.meta.payment`, with the gateway's reason in `response_message`. Try it with the decline test card `4444555566669999`.

```json
{
  "error": {
    "type": "transaction_error",
    "code": "payment_declined",
    "message": "The payment was declined or could not be processed. Please try another payment method, or contact your bank for assistance and try again.",
    "reference": "YrpsaAx",
    "status": 402,
    "meta": {
      "payment": { "payment_id": "0090460891496PyrYzd9DD", "status": "failed" }
    }
  }
}
```

Show `message` to the shopper and let them try again with another card. Do not parse `message`: use `code` and `status`. See [Error handling](/docs/basic-concepts/error-handling).

### Payments that wait for the shopper [#payments-that-wait-for-the-shopper]

When the shopper has to do something, the payment comes back `201` with `status: initiated`. The next step is in `response_data`. Add `expand=response_data` to the payment request, or [get the response data](/api/payments/get-payment-response-data) of the payment.

- **`response_data.redirect_url`**: send the shopper's browser there. For PayPal it is the page where they approve. For a card it is a 3-D Secure check page.
- **`response_data.next_action`** (cards on Comecero Express Payments only): run the 3-D Secure check on your own page instead of redirecting. It carries `client_secret`, `publishable_key` and `stripe_account`: the values the payment provider's browser library needs to show the check (`handleNextAction`).

When the shopper is back, [get the payment](/api/payments/get-payment). The Comecero 3-D Secure page commits the payment itself when the check ends, so it may already be `completed` or `pending`. While it is still `initiated` (always the case after PayPal or the PayPal card check page), call [Commit a payment](/api/payments/commit-payment). The amounts are worked out again from the cart first, so a change to shipping or tax is charged. On success the cart closes and the order is made, exactly as for a payment that succeeded at once.

```bash title="Request"
curl -X POST https://api.comecero.com/api/v1/payments/0090460891496PyrYzd9DC/commit \
  -H "Authorization: Bearer $COMECERO_TEST_TOKEN"
```

The commit answers with the payment, usually `completed` or `pending`. A payment that is already `completed` or `cancelled` answers `422`, which is why you commit only while it is `initiated`. Two commits at the same time are safe: the second gets the first one's result.

## After the payment: the order [#after-the-payment-the-order]

A successful payment closes the cart and makes the order. The order id is the cart's `order_id`, and the payment's `order` field links to it. [Get the order](/api/orders/get-order) to show the receipt, or listen for the [`order:created`](/api/webhooks/order-created) webhook to start fulfillment.

- `payment_status` is `completed` once the money is captured, or `pending` while it is only authorized.
- `fulfilled` is `true` when every item is fulfilled. Digital items and services are fulfilled at once. Physical items wait for a shipment, or for automatic fulfillment (see [Auto fulfill and capture delay](/docs/reference/fulfillment-delay)).
- The customer gets the receipt email, and the payment and order events fire. See [Webhooks](/docs/guides/webhooks).

## Authorize now, capture later [#authorize-now-capture-later]

Send `capture: false` on the payment to only authorize it. The payment and the order are then `pending`: the shopper's money is held but not taken. Left out, `capture` follows your fulfillment settings (by default, payments are captured at once). A limited token cannot set it.

To take the money, call [Capture a payment](/api/payments/capture-payment) with no body to capture it all, or with a `total` to capture less. The part you do not capture is recorded as a refund.

<Callout type="warn">To capture it all, send no body at all. A JSON body without a `total`, even `{}`, captures nothing and records a refund of the whole payment. The same goes for [Capture an order](/api/orders/capture-order).</Callout>

```bash title="Request"
curl -X POST https://api.comecero.com/api/v1/payments/0090460891496PyrYzd9DC/capture \
  -H "Authorization: Bearer $COMECERO_TEST_TOKEN"
```

To cancel the hold instead, call [Void a payment](/api/payments/void-payment). The payment becomes `cancelled` and no money is taken. Both calls only work while the payment is `pending` (`422` otherwise) and need a secret or trusted token.

[Capture an order](/api/orders/capture-order) and [Void an order](/api/orders/void-order) do the same for every pending payment of an order. Money that was captured is given back with a refund, not a void. See [Refunds](/docs/guides/refunds).

## Other ways to pay [#other-ways-to-pay]

### A saved payment method [#a-saved-payment-method]

Add `"save": true` to the `payment_method` to save the card on the customer once the payment succeeds. A payment for a subscription always saves it. You can also save one ahead with [Create a customer payment method](/api/customers/create-customer-payment-method). Cards (`credit_card`) and US bank accounts (`ach`) can be saved.

To pay with a saved method, send its id. With neither `payment_method` nor `payment_method_id`, the customer's default method is used.

```json
{
  "customer_id": "0091307952401CUa8Hn2Lq",
  "payment_method_id": "0091307952433PMk2Lw7Qx"
}
```

[List the customer's payment methods](/api/customers/list-customer-payment-methods) to find the ids. Card numbers are never returned, only the brand and the last four digits.

### PayPal [#paypal]

Send `type: paypal` with the pages PayPal sends the shopper back to.

```json
{
  "payment_method": {
    "type": "paypal",
    "data": {
      "success_url": "https://shop.example.com/checkout/paypal/return",
      "cancel_url": "https://shop.example.com/checkout"
    }
  }
}
```

The payment comes back `initiated` with `response_data.redirect_url`. Send the shopper there. When PayPal sends them to `success_url`, commit the payment. If the cart total went up too much since they approved, the commit leaves the payment `initiated` with a new `redirect_url` for them to approve again. Send `use_billing_agreement: true` when later payments, such as subscription renewals, must be charged without the shopper.

### Apple Pay [#apple-pay]

Apple Pay runs in the browser with the Payment Request API. The cart builds the payment sheet for you.

1. [Get the browser payment request](/api/carts/get-cart-user-agent-payment-request) for the cart, with `apple_pay` as the payment method type. It returns `methodData`, `details` and `paymentOptions`, ready for `new PaymentRequest(methodData, details, paymentOptions)`, plus `payment_url` and `merchant_validation_url`.
2. In the `onmerchantvalidation` handler, send the validation URL Apple gives you to [Validate the Apple Pay merchant](/api/carts/validate-cart-user-agent-payment-request-merchant) and pass the returned `data` to `event.complete()`.
3. When the shopper changes the shipping address or option on the sheet, send the change to [Update a browser payment request](/api/carts/update-cart-user-agent-payment-request) and pass the result to `updateWith()`. Tax and shipping are worked out again.
4. When the shopper approves, post the payment to `payment_url` with `payment_method.type` set to `apple_pay` and the sheet's `details` in `payment_method.data.details`, then call `complete()` on the sheet with the result.

Google Pay uses the `wallet` payment method type instead, and so does Apple Pay when the cart options offer it under `wallet` rather than `apple_pay`: you send the token the wallet gives you (`wallet` and `token`). Its browser settings are on the `wallet` entry in the cart options. See the `payment_method` field of [Pay for a cart](/api/carts/create-cart-payment).

## Good practice [#good-practice]

- **Never retry a payment by itself.** After a `502`, a `504` or a lost connection, get the cart or the payment first. A blind retry can charge the shopper twice.
- **Set your own `payment_id`** when you can. An id can be used only once, so a request sent twice cannot make two payments.
- **Trust the webhook, not the browser.** A shopper can close the tab after paying. Use [`order:created`](/api/webhooks/order-created) and [`payment:completed`](/api/webhooks/payment-completed) to start fulfillment.

<Cards>
  <Card title="Webhooks" href="/docs/guides/webhooks" />
  <Card title="Refunds" href="/docs/guides/refunds" />
  <Card title="Subscriptions" href="/docs/guides/subscriptions" />
  <Card title="Going live" href="/docs/guides/going-live" />
</Cards>
