Get started
Payment intents
Charge with payment intents in three steps.
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
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
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
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
<?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)
<?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
// 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"));
}
}{
"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
<!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
npm install @n1co/elementspnpm add @n1co/elementsyarn add @n1co/elementsbun add @n1co/elementsimport { 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
npm install @n1co/elementspnpm add @n1co/elementsyarn add @n1co/elementsbun add @n1co/elements<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
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-valuespnpm 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-valuesyarn 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-valuesbun 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-valuesimport '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
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
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
<?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)
<?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
@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.