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

# Quickstart

> Take your first card payment end to end with the Payment Intents API and the JavaScript SDK

This walkthrough takes a card payment from start to finish: create an intent on your server, mount the card fields in the browser, and confirm.

<Steps>
  <Step title="Create a Payment Intent on your server">
    Call `POST /payment-intents` with your **secret key** when the customer reaches checkout. Amounts are integers in the currency's smallest unit (for PHP, centavos).

    ```bash theme={null}
    curl 'https://api.sbx.moduluslabs.io/ecom/v1/payment-intents' \
      -H 'Authorization: Bearer sk_test_...' \
      -H 'Content-Type: application/json' \
      -d '{
        "amount": 86500,
        "currency": "PHP",
        "description": "Order #1234",
        "metadata": { "order_id": "1234" }
      }'
    ```

    The response returns the `id` and `client_secret` you hand to the browser:

    ```json theme={null}
    {
      "id": "b7e2c1a4-9f3d-4c6b-8a21-5e0f7d9c3b18",
      "client_secret": "3f9a6d2e-7c14-4b8f-a5d0-1e6c2b9f4a73",
      "amount": 86500,
      "currency": "PHP",
      "status": "ACTIVE",
      "expires_at": "2026-09-04T12:00:00Z"
    }
    ```

    See [Create a Payment Intent](/api-reference/ecom/create-payment-intent) for the full request and response.
  </Step>

  <Step title="Include the SDK">
    Load the library from the Modulus CDN with a single script tag. Load it from the CDN, do not self-host or bundle a copy, so you are always on the latest patched build.

    ```html theme={null}
    <script src="https://js.sbx.moduluslabs.io/v1"></script>
    ```

    You point at `/v1`, not a pinned version like `/v1.4.2`. Patches and backward-compatible fixes ship behind that same URL, so the browser always fetches the latest `v1` build. The script exposes a global `Modulus`.
  </Step>

  <Step title="Initialize and mount the card fields">
    Construct `Modulus` with your **publishable key**, create an Elements group scoped to the intent, then mount the fields into placeholders on your page.

    ```html theme={null}
    <form id="payment-form">
      <div id="card-number"></div>
      <div id="card-expiry"></div>
      <div id="card-cvc"></div>
      <button id="pay" disabled>Pay</button>
    </form>

    <!-- The 3D Secure challenge renders here when the bank requires one -->
    <div id="threeds-container"></div>
    ```

    ```javascript theme={null}
    const modulus = new Modulus('pk_test_...');

    const elements = modulus.elements({
      paymentIntentId: 'b7e2c1a4-9f3d-4c6b-8a21-5e0f7d9c3b18',
      clientSecret: '3f9a6d2e-7c14-4b8f-a5d0-1e6c2b9f4a73',
    });

    elements.create('cardNumber').mount('#card-number');
    elements.create('cardExpiry').mount('#card-expiry');
    elements.create('cardCvc').mount('#card-cvc');

    // Keep Pay disabled until every field is valid and filled
    elements.on('change', (e) => {
      document.querySelector('#pay').disabled = !e.complete;
    });
    ```

    See [Card fields](/docs/ecom/jssdk/card-fields) for field types, styling, and events.
  </Step>

  <Step title="Confirm the payment">
    On submit, call `confirmPayment` with the mounted elements and the cardholder billing details. It collects the card securely, runs 3D Secure if required, and resolves once with the final result.

    ```javascript theme={null}
    document.querySelector('#payment-form').addEventListener('submit', async (ev) => {
      ev.preventDefault();

      const result = await modulus.confirmPayment({
        elements,
        billingDetails: {
          firstName: 'Juan', lastName: 'Dela Cruz',
          email: 'juan@example.com',
          address: { line1: '123 Main St', country: 'PH' },
        },
        // 3D Secure is automatic. Render any challenge in your own container,
        // and use onStatusChange to drive your UI. Frictionless cards never
        // reach 'requires_action' and show no challenge.
        threeDS: { container: '#threeds-container' },
        onStatusChange: (status) => {
          if (status === 'submitting')      showSpinner('Processing...');
          if (status === 'requires_action') hideSpinner();            // let the customer complete the challenge
          if (status === 'authenticating')  showSpinner('Authenticating...');
        },
      });

      hideSpinner();

      switch (result.status) {
        case 'SUCCEEDED':
          // result.receipt.transaction_id, result.receipt.approval_code, ...
          showReceipt(result.receipt);
          break;
        case 'DECLINED':
          showError(result.userMessage || 'Card declined. Try another card.');
          break;
        case 'PROCESSING':
          showPending("We're confirming your payment.");
          break;
        default: // FAILED, EXPIRED
          showError(result.userMessage || result.error?.message);
      }
    });
    ```

    3D Secure runs inside `confirmPayment` for both flows: **frictionless** cards resolve without a challenge, and **challenge** cards render the bank's step in your `threeds-container`. See [3D Secure](/docs/ecom/jssdk/3d-secure) for the container, window sizing, and lifecycle, and [Confirm the payment](/docs/ecom/jssdk/confirm-payment) for `billingDetails` and every result status.
  </Step>
</Steps>

<Tip>
  Test the whole flow with the sandbox [test cards](/docs/testing). Use a 3DS challenge card to see the in-page authentication step, and a frictionless card to see a payment that authenticates with no customer interaction.
</Tip>
