n1coePayDocs API reference

Charge

Charge with payment intents. Your server creates the intent, your page confirms it with n1co.js and the webhook tells you the result.

  1. Your server creates a payment intent with POST /v1/payment_intents and hands its client_secret to the page.
  2. Your page confirms the payment with confirmPayment() from n1co.js, which shows 3D Secure if the bank asks for it.
  3. Your server fulfills the order when the payment_intent.succeeded webhook confirms it.

First, your server creates the intent:

Create a payment intent
curl -s -X POST https://api-epay.dev.n1co.dev/v1/payment_intents \
  -H "Authorization: Bearer $N1CO_SECRET_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": 1250,
    "currency": "USD",
    "customer": {
      "id": "cust-1001",
      "name": "Ana López",
      "email": "ana.lopez@example.com",
      "phone": "+503 7000-1234"
    },
    "description": "Order 8812",
    "reference": "order-8812",
    "metadata": { "cart": "c-1029" },
    "page_origins": ["https://shop.example.com"]
  }'
201 Created
{
  "id": "pi_UGF5bWVudEludGVudEV4YW1w",
  "object": "payment_intent",
  "client_secret": "pi_UGF5bWVudEludGVudEV4YW1w_secret_U2VjcmV0RXhhbXBsZTAwMDAwMDAwMDAw",
  "status": "requires_payment_method",
  "amount": 1250,
  "currency": "USD",
  "description": "Order 8812",
  "reference": "order-8812",
  "metadata": { "cart": "c-1029" },
  "billing": null,
  "page_origins": ["https://shop.example.com"],
  "card": null,
  "payment_method": null,
  "cvc_required": false,
  "next_action": null,
  "last_error": null,
  "attempts_remaining": 3,
  "authorization_code": null,
  "order_id": null,
  "cancellation_reason": null,
  "refund": null,
  "created_at": "2026-10-17T15:04:05Z",
  "updated_at": "2026-10-17T15:04:05Z",
  "succeeded_at": null,
  "canceled_at": null,
  "expires_at": "2026-10-17T16:04:05Z"
}

Then, the page. It asks for the client_secret once and uses it on every attempt:

pay.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Pay</title>
  </head>
  <body>
    <form id="payment-form">
      <div id="card"></div>
      <p id="payment-message" role="alert"></p>
      <button id="pay" type="submit" disabled>Pay $12.50</button>
    </form>

    <script src="https://elements.n1co.dev/v1/n1co.js"></script>
    <script>
      const n1 = n1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA');
      const card = n1.elements().create('card', { locale: 'en' });
      const pay = document.querySelector('#pay');
      const message = document.querySelector('#payment-message');

      // Your endpoint creates the intent with your secret key and returns only its client_secret.
      const clientSecret = fetch('/checkout/payment-intent', { method: 'POST' })
        .then((r) => r.json())
        .then((body) => body.client_secret);

      card
        .on('change', ({ complete, error }) => {
          pay.disabled = !complete;
          message.textContent = error ? error.message : '';
        })
        .mount('#card');

      document.querySelector('#payment-form').addEventListener('submit', async (event) => {
        event.preventDefault();
        pay.disabled = true;
        message.textContent = '';
        try {
          const intent = await n1.confirmPayment({ client_secret: await clientSecret, card });
          if (intent.status === 'succeeded') {
            // Your server confirms with the webhook before fulfilling the order.
            window.location.assign('/checkout/gracias');
          } else if (intent.status === 'processing') {
            message.textContent = "We're confirming your payment. Don't pay again; we'll let you know.";
          } else if (intent.status === 'canceled') {
            message.textContent = 'This payment can no longer be completed. Please start your purchase again.';
          } else {
            message.textContent = intent.last_error?.message ?? 'The payment could not be completed.';
            pay.disabled = false;
          }
        } catch (error) {
          message.textContent = error.message;
          pay.disabled = false;
        }
      });
    </script>
  </body>
</html>

Create the payment intent#

  • amount is in minor units: 1250 is 12.50.
  • currency is an ISO 4217 code with two decimal places. Your store charges in a single currency, the one of its first successful payment.
  • customer carries your id for the buyer, their name, email and phone, as in Save a card.
  • billing is the billing address. n1co asks for it for cards from the United States and Canada.
  • page_origins limits the intent to your pages, as with card sessions.

The client_secret is shown only here. An intent expires in one hour and has 3 attempts. The rules of each field are in the API reference.

Charge a saved card#

Your server attaches the card with its id in payment_method. The card must belong to the customer with that customer.id. The browser never picks or sees saved cards.

With a saved card
curl -s -X POST https://api-epay.dev.n1co.dev/v1/payment_intents \
  -H "Authorization: Bearer $N1CO_SECRET_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": 1250,
    "currency": "USD",
    "customer": { "id": "cust-1001", "name": "Ana López", "email": "ana.lopez@example.com", "phone": "+503 7000-1234" },
    "payment_method": "card_Q2FyZEV4YW1wbGUwMDAwMDAx"
  }'

The page confirms with the client_secret alone, without a card element:

const intent = await n1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA').confirmPayment({ client_secret });
Error What happened What to do
404 card_not_found The card does not belong to that customer. Check both id values.
409 card_unavailable The card can no longer be used. Charge with the card element.

A new card is never saved when charging. To save it, use a card session.

Ask for the security code of a saved card#

Your server creates the intent with payment_method and "cvc_required": true. Your page mounts the cvc element and confirms with confirmPayment({ client_secret, cvc }).

The buyer types the security code only inside n1co's frame: it never passes through your page or your server. n1co registers it for the saved card and then makes the charge.

pay-with-code.html
<form id="payment-form">
  <div id="cvc"></div>
  <p id="payment-message" role="alert"></p>
  <button id="pay" type="submit" disabled>Pay $12.50</button>
</form>

<script src="https://elements.n1co.dev/v1/n1co.js"></script>
<script>
  const n1 = n1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA');
  // brand and last4 of the attached card: from your server or from intent.payment_method.
  const cvc = n1.elements().create('cvc', { brand: 'visa', last4: '4242', locale: 'en' });
  const pay = document.querySelector('#pay');
  const message = document.querySelector('#payment-message');

  const clientSecret = fetch('/checkout/payment-intent', { method: 'POST' })
    .then((r) => r.json())
    .then((body) => body.client_secret);

  cvc
    .on('change', ({ complete, error }) => {
      pay.disabled = !complete;
      message.textContent = error ? error.message : '';
    })
    .mount('#cvc');

  document.querySelector('#payment-form').addEventListener('submit', async (event) => {
    event.preventDefault();
    pay.disabled = true;
    try {
      const intent = await n1.confirmPayment({ client_secret: await clientSecret, cvc });
      // Handle intent.status as in the example above.
      if (intent.status === 'succeeded') window.location.assign('/checkout/thanks');
      else if (intent.status === 'requires_payment_method') {
        message.textContent = intent.last_error.message;
        pay.disabled = false;
      }
    } catch (error) {
      message.textContent = error.message;
      pay.disabled = false;
    }
  });
</script>
Case What happened What to do
cvc_required rejection Confirmed without the cvc element. No attempt was used and nothing was charged. Confirm with cvc.
last_error.code card_update_failed n1co did not accept the code. Nothing was charged. Let the buyer retry.
last_error.code card_unavailable n1co no longer knows the card; it is now unavailable. Charge with the card element.

What confirmPayment() returns#

n1co.js handles 3D Secure in its own window and returns the intent. Check intent.status:

status What to do on your page
succeeded Show the confirmation.
processing Show that it is being confirmed. Do not create another intent.
requires_payment_method Show last_error.message and let the buyer retry.
canceled Create a new intent if the buyer wants to pay.

These cases need attention:

  • billing_required: ask for country, state and postal code, and confirm again with billing.
  • card_unavailable: the saved card stopped working; the buyer pays with the card element.
  • outcome_unknown: the intent stays processing and n1co never charges twice. Wait for the webhook.

The last_error codes are in Errors and the promise rejections in the n1co.js reference.

Confirm on your server#

Fulfill the order only when your server receives payment_intent.succeeded with a valid signature, or reads the intent with GET /v1/payment_intents/{id} and it is succeeded.

curl -s https://api-epay.dev.n1co.dev/v1/payment_intents/pi_UGF5bWVudEludGVudEV4YW1w \
  -H "Authorization: Bearer $N1CO_SECRET_KEY"
Event When
payment_intent.succeeded Once, when the charge is accepted.
payment_intent.payment_failed For every failed attempt.
payment_intent.canceled When it is canceled or expires.

States, cancellation and refunds#

States of a payment intent An intent starts in requires_payment_method. Confirming moves it to processing, which ends in succeeded, goes back to requires_payment_method if the attempt fails, or moves to requires_action when the bank asks for 3D Secure. From requires_action, a completed authentication goes back to processing and a failed or expired one goes back to requires_payment_method. Canceling or expiring from requires_payment_method or requires_action leads to canceled. requires_payment_method processing succeeded requires_action canceled create confirm failed attempt accepted 3D Secure authenticated failed or expired cancel or expire cancel or expire
succeeded and canceled are final. With no attempts left, the intent also ends canceled.

Cancel an intent you no longer need with POST /v1/payment_intents/{id}/cancel. It can be canceled only in requires_payment_method or requires_action.

To return a succeeded payment, create a refund.