> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pesaswap.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Card payments and 3DS

This page covers accepting a card payment and handling 3D Secure (3DS) authentication. If you haven't taken a payment yet, start with the quickstart guide.

<Note>
  An API key and a `profile_id` from the dashboard. Card acceptance must be enabled on your business profile.
</Note>

<Steps>
  <Step title="Create the 3DS payment">
    Create and confirm the payment in one call. Set `authentication_type` to `three_ds` to require 3D Secure, and pass a `return_url` — the bank sends the customer back there after the challenge.

    Replace `card_number`, `card_exp_month`, `card_exp_year`, `card_holder_name`, and `card_cvc` below with your own card data. The values shown are illustrative only and are not test credentials.

    <CodeGroup>
      ```bash cURL theme={"system"}
      curl --request POST \
        --url https://api.sandbox.pesaswap.io/payments \
        --header 'api-key: {{api_key}}' \
        --header 'Content-Type: application/json' \
        --data '{
          "amount": 100,
          "currency": "KES",
          "confirm": true,
          "customer_id": "{{customer_id}}",
          "profile_id": "{{profile_id}}",
          "payment_method": "card",
          "payment_method_type": "debit",
          "authentication_type": "three_ds",
          "return_url": "https://example.com/payments/return",
          "payment_method_data": {
            "card": {
              "card_number": "4242 4242 4242 4242",
              "card_exp_month": "12",
              "card_exp_year": "28",
              "card_holder_name": "Jane Doe",
              "card_cvc": "123"
            }
          }
        }'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch("https://api.sandbox.pesaswap.io/payments", {
        method: "POST",
        headers: {
          "api-key": "{{api_key}}",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          amount: 100,
          currency: "KES",
          confirm: true,
          customer_id: "{{customer_id}}",
          profile_id: "{{profile_id}}",
          payment_method: "card",
          payment_method_type: "debit",
          authentication_type: "three_ds",
          return_url: "https://example.com/payments/return",
          payment_method_data: {
            card: {
              card_number: "4242 4242 4242 4242",
              card_exp_month: "12",
              card_exp_year: "28",
              card_holder_name: "Jane Doe",
              card_cvc: "123",
            },
          },
        }),
      });

      const payment = await response.json();
      console.log(payment);
      ```

      ```python Python (requests) theme={"system"}
      import requests

      response = requests.post(
          "https://api.sandbox.pesaswap.io/payments",
          headers={
              "api-key": "{{api_key}}",
              "Content-Type": "application/json",
          },
          json={
              "amount": 100,
              "currency": "KES",
              "confirm": True,
              "customer_id": "{{customer_id}}",
              "profile_id": "{{profile_id}}",
              "payment_method": "card",
              "payment_method_type": "debit",
              "authentication_type": "three_ds",
              "return_url": "https://example.com/payments/return",
              "payment_method_data": {
                  "card": {
                      "card_number": "4242 4242 4242 4242",
                      "card_exp_month": "12",
                      "card_exp_year": "28",
                      "card_holder_name": "Jane Doe",
                      "card_cvc": "123",
                  }
              },
          },
      )

      payment = response.json()
      print(payment)
      ```

      ```php PHP theme={"system"}
      <?php
      $curl = curl_init();

      curl_setopt_array($curl, [
          CURLOPT_URL => "https://api.sandbox.pesaswap.io/payments",
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_CUSTOMREQUEST => "POST",
          CURLOPT_HTTPHEADER => [
              "api-key: {{api_key}}",
              "Content-Type: application/json",
          ],
          CURLOPT_POSTFIELDS => json_encode([
              "amount" => 100,
              "currency" => "KES",
              "confirm" => true,
              "customer_id" => "{{customer_id}}",
              "profile_id" => "{{profile_id}}",
              "payment_method" => "card",
              "payment_method_type" => "debit",
              "authentication_type" => "three_ds",
              "return_url" => "https://example.com/payments/return",
              "payment_method_data" => [
                  "card" => [
                      "card_number" => "4242 4242 4242 4242",
                      "card_exp_month" => "12",
                      "card_exp_year" => "28",
                      "card_holder_name" => "Jane Doe",
                      "card_cvc" => "123",
                  ],
              ],
          ]),
      ]);

      $response = curl_exec($curl);
      curl_close($curl);

      echo $response;
      ```
    </CodeGroup>

    The response comes back with `status: "requires_customer_action"` and a `next_action` object of type `redirect_to_url` — a successful call does not mean the payment is done:

    <CodeGroup>
      ```json 200 Response theme={"system"}
      {
        "payment_id": "pay_mbabizu24mvu3mela5njyhpit4",
        "status": "requires_customer_action",
        "amount": 100,
        "currency": "KES",
        "next_action": {
          "type": "redirect_to_url",
          "redirect_to_url": "https://sandbox.pesaswap.io/3ds/redirect/pay_mbabizu24mvu3mela5njyhpit4"
        }
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Redirect the customer">
    1. Read `next_action.redirect_to_url` from the response.
    2. Redirect the customer's browser to that URL. The issuing bank runs its 3DS challenge there (OTP, banking app approval, etc).
    3. When the challenge finishes, the bank redirects the customer back to the `return_url` you supplied.

    With `authentication_type: "no_three_ds"`, there is no redirect step. The payment can go straight to `succeeded` (or `failed`) in the same confirm call or shortly after, depending on the connector.
  </Step>

  <Step title="Poll for the final status">
    Once the customer returns to your `return_url`, fetch the authoritative status from the connector before treating the payment as complete.

    <Warning>
      The hit on your `return_url` is not proof that the payment succeeded. It only tells you the customer's browser came back — the bank may still be finalizing the authentication, or the challenge may have failed. Never mark an order as paid at this point.

      Always call `GET /payments/{payment_id}?force_sync=true` after the redirect to fetch the authoritative status from the connector before you treat the payment as complete.
    </Warning>

    <CodeGroup>
      ```bash cURL theme={"system"}
      curl --request GET \
        --url 'https://api.sandbox.pesaswap.io/payments/pay_mbabizu24mvu3mela5njyhpit4?force_sync=true' \
        --header 'api-key: {{api_key}}'
      ```

      ```javascript Node.js theme={"system"}
      const response = await fetch(
        "https://api.sandbox.pesaswap.io/payments/pay_mbabizu24mvu3mela5njyhpit4?force_sync=true",
        {
          method: "GET",
          headers: { "api-key": "{{api_key}}" },
        }
      );

      const payment = await response.json();
      console.log(payment.status);
      ```

      ```python Python (requests) theme={"system"}
      import requests

      response = requests.get(
          "https://api.sandbox.pesaswap.io/payments/pay_mbabizu24mvu3mela5njyhpit4",
          params={"force_sync": "true"},
          headers={"api-key": "{{api_key}}"},
      )

      payment = response.json()
      print(payment["status"])
      ```

      ```php PHP theme={"system"}
      <?php
      $curl = curl_init();

      curl_setopt_array($curl, [
          CURLOPT_URL => "https://api.sandbox.pesaswap.io/payments/pay_mbabizu24mvu3mela5njyhpit4?force_sync=true",
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_CUSTOMREQUEST => "GET",
          CURLOPT_HTTPHEADER => [
              "api-key: {{api_key}}",
          ],
      ]);

      $response = curl_exec($curl);
      curl_close($curl);

      echo $response;
      ```
    </CodeGroup>
  </Step>
</Steps>

## Status reference

| Status                     | What it means                                              | What to do                                                                     |
| -------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `requires_customer_action` | The 3DS challenge is pending.                              | Redirect the customer to `next_action.redirect_to_url` if you haven't already. |
| `processing`               | The bank has the payment and is finalizing it.             | Keep polling.                                                                  |
| `succeeded`                | The payment is settled.                                    | Stop — settled.                                                                |
| `failed`                   | The bank declined the payment or the 3DS challenge failed. | Stop — failed.                                                                 |

## Common errors

<AccordionGroup>
  <Accordion title="Missing return_url on a 3DS payment">
    When `authentication_type` is `three_ds`, `return_url` is required — without it the bank has nowhere to send the customer back to after the challenge.
  </Accordion>

  <Accordion title="Expired card">
    The connector declines the payment and the final status is `failed`.
  </Accordion>

  <Accordion title="Treating the return_url redirect as success">
    The redirect only means the customer's browser came back to your site. Always confirm with `GET /payments/{payment_id}?force_sync=true` before marking the order paid.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/essentials/payments-guide/quickstart">
    The full create-and-poll payment loop.
  </Card>

  <Card title="Wallet payments" icon="wallet" href="/essentials/payments-guide/wallet-payments">
    M-Pesa, Airtel, and MTN MoMo.
  </Card>

  <Card title="Mandates and recurring" icon="repeat" href="/essentials/payments-guide/mandates-and-recurring">
    Save a card for repeat charges.
  </Card>

  <Card title="Refunds" icon="rotate-ccw" href="/essentials/payments-guide/refunds">
    Refund a settled card payment.
  </Card>
</CardGroup>
