# Simple payment

Accept a one-off payment: create a checkout, send the customer to the Payout payment page and mark the order as paid when the webhook confirms it.

## Steps

1. **Get an API key**

   Generate an API key (client ID and secret) in the backoffice: [Sandbox](https://sandbox.payout.one/developers/keys/new) or [Production](https://app.payout.one/developers/keys/new). You need a Payout account first.

   Set a notify URL before you generate the key. Payout sends webhooks (POST requests) about checkouts and other events to this URL.

2. **Get a Bearer token**

   Exchange the client ID and secret for a token:

   ```bash
   curl --location --request POST 'https://sandbox.payout.one/api/v1/authorize' \
   --header 'Content-Type: application/json' \
   --header 'Accept: application/json' \
   --data-raw '{
       "client_id": "DC995618-7ED8-4070-9DA0-48B6F86551C3",
       "client_secret": "q3dpHpYtDrH-KmGD4HMn5OTEx6IsZPBokQ8CqMONWqMSEePWy9bXd3Ua3KvO7f6C"
   }'
   ```

   Response:

   ```json
   {
       "token": "SFMyNTY.EXAMPLE-TOKEN.dGhpcy1pcy1hLWZha2Utc2lnbmF0dXJlLWV4YW1wbGU",
       "valid_for": 6000
   }
   ```

   Send the token in the `Authorization: Bearer` header of every other request. It is valid for `valid_for` seconds; then request a new one.

3. **Create the checkout**

   Send the details of your order. Give every checkout its own `Idempotency-Key`: a request with a key that was already used returns the existing checkout instead of creating a new one.

   ```bash
   curl --location --request POST 'https://sandbox.payout.one/api/v1/checkouts' \
   --header 'Content-Type: application/json' \
   --header 'Accept: application/json' \
   --header 'Authorization: Bearer SFMyNTY.EXAMPLE-TOKEN.dGhpcy1pcy1hLWZha2Utc2lnbmF0dXJlLWV4YW1wbGU' \
   --header 'Idempotency-Key: d2c9f095-8e4c-498a-802a-68c220dbe1bd' \
   --data-raw '{
       "amount": 300,
       "currency": "EUR",
       "customer": {
           "first_name": "John",
           "last_name": "Doe",
           "email": "john.doe@example.com",
           "phone": "+421900000000"
       },
       "billing_address": {
           "name": "John Doe",
           "address_line_1": "Billing Address Line 1",
           "address_line_2": "Billing Address Line 2",
           "city": "Billington",
           "postal_code": "BL92883",
           "country_code": "GB"
       },
       "shipping_address": {
           "name": "John Doe",
           "address_line_1": "Shipping Address Line 1",
           "address_line_2": "Shipping Address Line 2",
           "city": "Shippington",
           "postal_code": "W153KF",
           "country_code": "GB"
       },
       "products": [
           {
               "name": "Product 1",
               "unit_price": 100,
               "quantity": 3
           }
       ],
       "external_id": "order-1001",
       "nonce": "bzAwd2VzTU5UMEtWZ3IzMA",
       "metadata": {
           "source": "eshop"
       },
       "redirect_url": "https://eshop.example.com/payment/redirect",
       "signature": "7aff78519a0d038ba4960a53a1bc82f0ff35ce7b84d9cbe367360295652f07fb"
   }'
   ```

   `amount` is in cents: 300 is 3.00 EUR. The same applies to other currencies, so 30000 is 300 CZK. Only `amount`, `currency`, `customer`, `external_id`, `nonce`, `redirect_url` and `signature` are required; [Create checkout](https://developers.payout.tech/api/payment.html#create_checkout) lists all fields.

   To sign the request, join the values with `|`, hash the string with SHA-256 and send the hash as `signature`:

   ```text
   Pattern: amount|currency|external_id|nonce|client_secret
   Input:   300|EUR|order-1001|bzAwd2VzTU5UMEtWZ3IzMA|q3dpHpYtDrH-KmGD4HMn5OTEx6IsZPBokQ8CqMONWqMSEePWy9bXd3Ua3KvO7f6C
   SHA-256: 7aff78519a0d038ba4960a53a1bc82f0ff35ce7b84d9cbe367360295652f07fb
   ```

   > [!NOTE]
   > Every signature in the API is the SHA-256 hash in lowercase hex. Some libraries return uppercase hex; convert it to lowercase before you send or compare it.

4. **Verify the response**

   The response contains the new checkout with its own `nonce` and `signature`. Compute the signature from the response values and compare it with `signature` to make sure the response comes from Payout.

   Response:

   ```json
   {
       "object": "checkout",
       "id": 141447,
       "external_id": "order-1001",
       "amount": 300,
       "currency": "EUR",
       "redirect_url": "https://eshop.example.com/payment/redirect",
       "idempotency_key": "d2c9f095-8e4c-498a-802a-68c220dbe1bd",
       "customer": {
           "first_name": "John",
           "last_name": "Doe",
           "name": "John Doe",
           "email": "john.doe@example.com",
           "phone": "+421900000000",
           "note": null
       },
       "checkout_url": "https://sandbox.payout.one/checkouts/U0ZNeU5UWS5FWEFNUExFLUNIRUNLT1VULVRPS0VOLm5vdC1hLXJlYWwtc2lnbmF0dXJl/?account_id=RVhBTVBMRS1BQ0NPVU5ULVRPS0VOLTAwMDAwMDAwMDA",
       "metadata": {
           "source": "eshop"
       },
       "status": "processing",
       "nonce": "V2cwZ0U0YU9tNlVoOUlrbA",
       "signature": "0cce8d83001e51f45278a01a731566cf4805a9470d35cf1d58a1f1bf4fd98c0a",
       "payment": null,
       "all_payments": [],
       "billing_address": {
           "name": "John Doe",
           "address_line_1": "Billing Address Line 1",
           "address_line_2": "Billing Address Line 2",
           "city": "Billington",
           "postal_code": "BL92883",
           "country_code": "GB"
       },
       "shipping_address": {
           "name": "John Doe",
           "address_line_1": "Shipping Address Line 1",
           "address_line_2": "Shipping Address Line 2",
           "city": "Shippington",
           "postal_code": "W153KF",
           "country_code": "GB"
       },
       "products": [
           {
               "name": "Product 1",
               "quantity": 3,
               "unit_price": 100
           }
       ],
       "payment_token": "U0ZNeU5UWS5FWEFNUExFLUNIRUNLT1VULVRPS0VOLm5vdC1hLXJlYWwtc2lnbmF0dXJl",
       "is_status_final": false
   }
   ```

   Signature of this response:

   ```text
   Pattern: amount|currency|external_id|nonce|client_secret
   Input:   300|EUR|order-1001|V2cwZ0U0YU9tNlVoOUlrbA|q3dpHpYtDrH-KmGD4HMn5OTEx6IsZPBokQ8CqMONWqMSEePWy9bXd3Ua3KvO7f6C
   SHA-256: 0cce8d83001e51f45278a01a731566cf4805a9470d35cf1d58a1f1bf4fd98c0a
   ```

5. **Redirect the customer to the payment page**

   Send the customer to `checkout_url` from the response.

6. **Handle the customer's return**

   When the payment succeeds or fails, Payout sends the customer back to the `redirect_url` of the checkout. The redirect doesn't carry the result of the payment; the webhook does.

7. **Receive the webhook**

   Payout sends the result as a POST request with a JSON body to your notify URL. Respond with a 2xx status code; otherwise Payout sends the webhook again.

   `checkout.succeeded` payload:

   ```json
   {
       "external_id": "order-1001",
       "object": "webhook",
       "type": "checkout.succeeded",
       "data": {
           "object": "checkout",
           "id": 141447,
           "external_id": "order-1001",
           "amount": 300,
           "currency": "EUR",
           "redirect_url": "https://eshop.example.com/payment/redirect",
           "customer": {
               "first_name": "John",
               "last_name": "Doe",
               "email": "john.doe@example.com",
               "card_number_masked": ""
           },
           "payment": {
               "object": "payment",
               "status": "successful",
               "payment_method": "card",
               "failure_reason": "",
               "created_at": 1759744800,
               "fee": 8,
               "net": 292
           },
           "metadata": {
               "source": "eshop"
           },
           "status": "succeeded",
           "is_status_final": true
       },
       "nonce": "eE1FNXBhS3ZxVklRZG5JZA",
       "signature": "3f2c8df4770fd18311f3360d96aacace00842b464a0317a45768a622531dc79d"
   }
   ```

   > [!NOTE]
   > `is_status_final` tells whether the checkout has reached a final state and will not change any more. For `succeeded` checkouts it is always `true`. For `expired` checkouts it becomes `true` only 6 days after the expiry, so that late bank transfers can still arrive and be matched.

8. **Verify the webhook signature**

   Compute the signature from the top-level `external_id`, `type` and `nonce` of the webhook and compare it with `signature`. Process the webhook only if they match.

   ```text
   Pattern: external_id|type|nonce|client_secret
   Input:   order-1001|checkout.succeeded|eE1FNXBhS3ZxVklRZG5JZA|q3dpHpYtDrH-KmGD4HMn5OTEx6IsZPBokQ8CqMONWqMSEePWy9bXd3Ua3KvO7f6C
   SHA-256: 3f2c8df4770fd18311f3360d96aacace00842b464a0317a45768a622531dc79d
   ```

9. **Mark the order as paid**

   Mark the order as paid once you have received and verified `checkout.succeeded`. Its `is_status_final: true` confirms that the checkout will not change any more.

   You can also listen for the `checkout.finalized` webhook, which is sent when `is_status_final` changes to `true`. It is useful for `expired` checkouts, which become final 6 days after they expire. `checkout.finalized` is available on request; contact support to enable it for your account.

## Test cards

Use these cards in the sandbox.

| Card number | Expiry | CVV | 3-D Secure | Result |
| --- | --- | --- | --- | --- |
| `4245757666349685` | 12/29 | `123` | Challenge required | Authorized |
| `5150030090350186` | 12/29 | `123` | 3DS Method required, then frictionless success | Authorized |
| `4012001037141120` | 12/29 | `123` | 3DS Method and challenge required | Authorized |
| `5100052384536834` | 12/29 | `123` | Challenge parameters if an SDK object is sent in `OrderCreateRequest` | Authorized |
| `5100052384536818` | 02/32 | `123` | Challenge required; without 3-D Secure, soft decline (SSD) | Authorized |
| `5100052384536826` | 12/29 | `123` | Frictionless, positive authentication | Authorized |
| `5521455186577727` | 12/29 | `123` | Frictionless, negative authentication | Not authorized (authentication fails) |

## Next steps

- [Refund](https://developers.payout.tech/guides/payment-gateway-use-cases-refund.html) a paid checkout.
- Send the customer straight to one payment method: [Checkout payment methods](https://developers.payout.tech/guides/payment-gateway-use-cases-checkout-payment-methods.html).
- Let the customer pay by bank transfer from your own page: [Payment instructions](https://developers.payout.tech/guides/payment-gateway-use-cases-payment-instructions.html).
- Save the customer's card for later payments: [Store card](https://developers.payout.tech/guides/payment-gateway-use-cases-store-card.html).
