n1coePayDocs

Get started

Payment intents

Charge with payment intents in three steps.

≈ 10 minutes · curl, Node.js, .NET, PHP or Java · n1co.js, Web or React Native

Step 1Create a payment intent on your server

Your server creates the intent with the amount of your order, never with one sent by the browser. It gives the page only the intent_token.

Your secret_key goes in the Authorization header and never leaves your server.

A saved card's id is the one card capture returns.

curl

POST /v1/payment_intents
curl -s -X POST https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com/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"],
    "save_card": true
    "payment_method": "65f1c0ffee0123456789abcd"
  }'

Node.js

server.mjs
app.post('/checkout/payment-intent', async (req, res) => {
  // The amount comes from your order, never from the browser.
  const response = await fetch('https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com/v1/payment_intents', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.N1CO_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: 1250,
      currency: 'USD',
      customer: { id: 'cust-1001', name: 'Ana López', email: 'ana.lopez@example.com', phone: '+503 7000-1234' },
      reference: 'order-8812',
      page_origins: ['https://shop.example.com'],
      save_card: true,
      payment_method: '65f1c0ffee0123456789abcd',
    }),
  });
  if (!response.ok) throw new Error(`n1co ${response.status}: ${await response.text()}`);
  const intent = await response.json();
  res.json({ intent_token: intent.intent_token });
});

.NET

Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("n1co", http =>
{
    http.BaseAddress = new Uri("https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com");
    http.DefaultRequestHeaders.Authorization =
        new("Bearer", Environment.GetEnvironmentVariable("N1CO_SECRET_KEY"));
});
var app = builder.Build();

app.MapPost("/checkout/payment-intent", async (IHttpClientFactory factory) =>
{
    // The amount comes from your order, never from the browser.
    var response = await factory.CreateClient("n1co").PostAsJsonAsync("/v1/payment_intents", new
    {
        amount = 1250,
        currency = "USD",
        customer = new { id = "cust-1001", name = "Ana López", email = "ana.lopez@example.com", phone = "+503 7000-1234" },
        reference = "order-8812",
        page_origins = new[] { "https://shop.example.com" },
        save_card = true,
        payment_method = "65f1c0ffee0123456789abcd",
    });
    response.EnsureSuccessStatusCode();
    var intent = await response.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>();
    return Results.Ok(new { intent_token = intent.GetProperty("intent_token").GetString() });
});

PHP

payment-intent.php
<?php

// The amount comes from your order, never from the browser.
$ch = curl_init('https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com/v1/payment_intents');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 1250,
        'currency' => 'USD',
        'customer' => ['id' => 'cust-1001', 'name' => 'Ana López', 'email' => 'ana.lopez@example.com', 'phone' => '+503 7000-1234'],
        'reference' => 'order-8812',
        'page_origins' => ['https://shop.example.com'],
        'save_card' => true,
        'payment_method' => '65f1c0ffee0123456789abcd',
    ]),
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('N1CO_SECRET_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($response === false || $status >= 300) {
    throw new RuntimeException("n1co $status: $response");
}
$intent = json_decode($response, true);
header('Content-Type: application/json');
echo json_encode(['intent_token' => $intent['intent_token']]);

PHP (SDK)

payment-intent.php
<?php

use N1co\EPay\N1co;

$n1co = new N1co((string) getenv('N1CO_SECRET_KEY'), ['base_url' => 'https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com']);

// The amount comes from your order, never from the browser.
$intent = $n1co->paymentIntents->create([
    'amount' => 1250,
    'currency' => 'USD',
    'customer' => ['id' => 'cust-1001', 'name' => 'Ana López', 'email' => 'ana.lopez@example.com', 'phone' => '+503 7000-1234'],
    'reference' => 'order-8812',
    'page_origins' => ['https://shop.example.com'],
    'save_card' => true,
    'payment_method' => '65f1c0ffee0123456789abcd',
]);
header('Content-Type: application/json');
echo json_encode(['intent_token' => $intent->intent_token]);

Java

CheckoutController.java
// Java 21+, Spring Boot and Jackson.
@RestController
class CheckoutController {
  private final HttpClient http = HttpClient.newHttpClient();
  private final ObjectMapper json = new ObjectMapper();

  @PostMapping("/checkout/payment-intent")
  Map<String, Object> paymentIntent() throws Exception {
    // The amount comes from your order, never from the browser.
    var body = Map.of(
        "amount", 1250,
        "currency", "USD",
        "customer", Map.of("id", "cust-1001", "name", "Ana López", "email", "ana.lopez@example.com", "phone", "+503 7000-1234"),
        "reference", "order-8812",
        "page_origins", List.of("https://shop.example.com"),
        "save_card", true);
        "payment_method", "65f1c0ffee0123456789abcd");
    var request = HttpRequest.newBuilder(URI.create("https://api-epay-sandbox.n1co.shophttps://api-epay.n1co.com/v1/payment_intents"))
        .header("Authorization", "Bearer " + System.getenv("N1CO_SECRET_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(body)))
        .build();
    var response = http.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 300) {
      throw new IllegalStateException("n1co " + response.statusCode() + ": " + response.body());
    }
    var intent = json.readValue(response.body(), Map.class);
    return Map.of("intent_token", intent.get("intent_token"));
  }
}
201Created
{
  "id": "pi_UGF5bWVudEludGVudEV4YW1w",
  "object": "payment_intent",
  "intent_token": "pi_UGF5bWVudEludGVudEV4YW1w_token_SW50ZW50VG9rZW5FeGFtcGxlMDAwMDAw",
  "status": "requires_payment_method",
  "amount": 1250,
  "currency": "USD",
  "reference": "order-8812",
  "expires_at": "2026-10-17T16:04:05Z"
}
Field Values Default Purpose
currency ISO 4217 code with two decimal places, for example USD Required The currency of amount.
save_card true: the card is saved for the buyer; false: single use true What happens to the new card.
page_origins 1 to 5 https://… origins; http://localhost and http://127.0.0.1 for development Optional Only those pages can use the token.
Field Values Default Purpose
currency ISO 4217 code with two decimal places, for example USD Required The currency of amount.
payment_method Id of an active saved card of the same customer.id Optional Charges that card.
cvc_required true, false false (optional) Asks for the security code; only with payment_method.
page_origins 1 to 5 https://… origins; http://localhost and http://127.0.0.1 for development Optional Only those pages can use the token.

Step 2Charge on your page

Mount the card element and confirm the payment with the intent_token. Card data is typed into an n1co iframe, and 3D Secure appears when the bank asks for it.

Confirm the payment with the intent_token; the saved card needs no form. 3D Secure appears when the bank asks for it.

HTML

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>
      <button id="pay" type="submit">Pay $12.50</button>
    </form>

    <script src="https://elements.n1co.com/v1/n1co.js"></script>
    <script>
      const n1 = n1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA', { environment: 'sandbox' });
      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 intent_token.
      const intentToken = fetch('/checkout/payment-intent', { method: 'POST' })
        .then((r) => r.json())
        .then((body) => body.intent_token);

      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({ intent_token: await intentToken, card });
          const intent = await n1.confirmPayment({ intent_token: await intentToken });
          if (intent.status === 'succeeded') {
            // Your server confirms with the webhook before fulfilling the order.
            window.location.assign('/checkout/thanks');
          } else if (intent.status === 'processing') {
            message.textContent = 'We are confirming your payment. Do not pay again; we will let you know.';
          } 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>

Web

Terminal
npm install @n1co/elements
pnpm add @n1co/elements
yarn add @n1co/elements
bun add @n1co/elements
pay.ts
import { loadN1co } from '@n1co/elements';

// null during server-side rendering: call it in the browser.
const client = await loadN1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA', { environment: 'sandbox' });
if (client) {
  const card = client.elements().create('card', { locale: 'en' });
  card.mount('#card');

  document.querySelector('#pay')!.addEventListener('click', async () => {
    const { intent_token } = await fetch('/checkout/payment-intent', { method: 'POST' }).then((r) => r.json());
    const intent = await client.confirmPayment({ intent_token, card });
    const intent = await client.confirmPayment({ intent_token });
    // intent.status: 'succeeded', 'processing' or another, as in the HTML tab.
  });
}

Vue / Nuxt

Terminal
npm install @n1co/elements
pnpm add @n1co/elements
yarn add @n1co/elements
bun add @n1co/elements
Pay.vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { loadN1co, type N1coClient, type Element as CardElement } from '@n1co/elements';
import { loadN1co, type N1coClient } from '@n1co/elements';

const cardBox = ref<HTMLElement | null>(null);
const client = ref<N1coClient | null>(null);
const card = ref<CardElement | null>(null);
const message = ref('');
const complete = ref(false);
const paying = ref(false);

onMounted(async () => {
  // null during server-side rendering; in Nuxt you can also wrap this component in <ClientOnly>.
  client.value = await loadN1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA', { environment: 'sandbox' });
  if (!client.value || !cardBox.value) return;
  card.value = client.value.elements().create('card', { locale: 'en' });
  card.value.on('change', (e) => {
    complete.value = e.complete;
    message.value = e.error ? e.error.message : '';
  });
  card.value.mount(cardBox.value);
});

onBeforeUnmount(() => card.value?.unmount());

async function pay() {
  if (!client.value || !card.value) return;
  if (!client.value) return;
  paying.value = true;
  message.value = '';
  try {
    // Your endpoint creates the intent with your secret_key and returns only its intent_token.
    const { intent_token } = await fetch('/checkout/payment-intent', { method: 'POST' }).then((r) => r.json());
    const intent = await client.value.confirmPayment({ intent_token, card: card.value });
    const intent = await client.value.confirmPayment({ intent_token });
    if (intent.status === 'succeeded') {
      // Your server confirms with the webhook before fulfilling the order.
      window.location.assign('/checkout/thanks');
    } else if (intent.status === 'processing') {
      message.value = 'We are confirming your payment. Do not pay again; we will let you know.';
    } else {
      message.value = intent.last_error?.message ?? 'The payment could not be completed.';
      paying.value = false;
    }
  } catch (error) {
    message.value = error instanceof Error ? error.message : String(error);
    paying.value = false;
  }
}
</script>

<template>
  <form @submit.prevent="pay">
    <div ref="cardBox"></div>
    <p role="alert" v-text="message"></p>
    <button type="submit" :disabled="!complete || paying">Pay $12.50</button>
    <button type="submit" :disabled="paying">Pay $12.50</button>
  </form>
</template>

React Native

Terminal
npm install @n1co/react-native react-native-webview react-native-get-random-values
# Expo: npx expo install react-native-webview expo-crypto react-native-get-random-values
pnpm add @n1co/react-native react-native-webview react-native-get-random-values
# Expo: pnpm expo install react-native-webview expo-crypto react-native-get-random-values
yarn add @n1co/react-native react-native-webview react-native-get-random-values
# Expo: yarn expo install react-native-webview expo-crypto react-native-get-random-values
bun add @n1co/react-native react-native-webview react-native-get-random-values
# Expo: bun expo install react-native-webview expo-crypto react-native-get-random-values
Checkout.tsx
import 'react-native-get-random-values'; // first line of your app
import { useRef, useState } from 'react';
import { Button } from 'react-native';
import { CardField, N1coProvider, useN1co, type CardFieldHandle } from '@n1co/react-native';
import { N1coProvider, useN1co } from '@n1co/react-native';

// From an app, your server creates the intent without page_origins: the page that pays is n1co's.
export function App() {
  return (
    <N1coProvider publishableKey="pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA" environment="sandbox" locale="en">
      <Checkout />
    </N1coProvider>
  );
}

function Checkout() {
  const card = useRef<CardFieldHandle>(null);
  const [complete, setComplete] = useState(false);
  const { confirmPayment } = useN1co();

  async function pay() {
    const { intent_token } = await fetch('https://shop.example.com/checkout/payment-intent', { method: 'POST' }).then(
      (r) => r.json(),
    );
    const intent = await confirmPayment({ intent_token, card: card.current });
    const intent = await confirmPayment({ intent_token });
    // intent.status: 'succeeded', 'processing' or another, as in the HTML tab.
  }

  return (
    <>
      <CardField ref={card} onChange={(event) => setComplete(event.complete)} />
      <Button title="Pay" disabled={!complete} onPress={pay} />
    </>
  );
  return <Button title="Pay" onPress={pay} />;
}
Option Values Default Where
environment 'production', 'sandbox' 'production' n1co(), loadN1co(), N1coProvider
locale 'es', 'en' 'es' create('card', …), N1coProvider

Methods and options of each SDK: Web and React Native.

Step 3Confirm with the webhook

Your server fulfills the order when n1co notifies it of the payment, with a valid signature. The check is in Set up webhooks.

Node.js

server.mjs
import express from 'express';
import { verifyWebhook } from './webhooks.mjs';

// express.raw: the signature covers the raw body, before express.json().
app.post('/webhooks/n1co', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => {
  if (!verifyWebhook(req.headers, req.body)) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  if (event.type === 'payment_intent.succeeded') {
    // Fulfill the order of event.data.object.reference; drop repeated event.id values.
  }
  res.sendStatus(204);
});

.NET

Program.cs
app.MapPost("/webhooks/n1co", async (HttpRequest request) =>
{
    using var buffer = new MemoryStream();
    await request.Body.CopyToAsync(buffer);
    var body = buffer.ToArray();
    var h = request.Headers;
    if (!N1coWebhook.Verify(h["webhook-id"], h["webhook-timestamp"], h["webhook-signature"], body))
        return Results.BadRequest();
    using var evt = System.Text.Json.JsonDocument.Parse(body);
    if (evt.RootElement.GetProperty("type").GetString() == "payment_intent.succeeded")
    {
        // Fulfill the order of data.object.reference; drop repeated ids.
    }
    return Results.NoContent();
});

PHP

webhook-endpoint.php
<?php

require __DIR__ . '/webhooks.php';

$body = file_get_contents('php://input');
if (!verify_webhook($body)) {
    http_response_code(400);
    exit;
}
$event = json_decode($body, true);
if ($event['type'] === 'payment_intent.succeeded') {
    // Fulfill the order of $event['data']['object']['reference']; drop repeated ids.
}
http_response_code(204);

PHP (SDK)

webhook-endpoint.php
<?php

require __DIR__ . '/webhooks.php'; // $event, verified by the SDK

if ($event->type === 'payment_intent.succeeded') {
    // Fulfill the order of $event->data->object->reference; drop repeated $event->id values.
}
http_response_code(204);

Java

WebhookController.java
@RestController
class WebhookController {
  private final ObjectMapper json = new ObjectMapper();

  @PostMapping("/webhooks/n1co")
  ResponseEntity<Void> receive(@RequestHeader HttpHeaders headers, @RequestBody byte[] body) throws Exception {
    if (!N1coWebhook.verify(headers.getFirst("webhook-id"), headers.getFirst("webhook-timestamp"),
        headers.getFirst("webhook-signature"), body)) {
      return ResponseEntity.badRequest().build();
    }
    var event = json.readValue(body, Map.class);
    if ("payment_intent.succeeded".equals(event.get("type"))) {
      // Fulfill the order of data.object.reference; drop repeated ids.
    }
    return ResponseEntity.noContent().build();
  }
}

You took your first payment.

Your server created the intent, your page charged it and the webhook confirmed it.

Type to search the documentation.