Empezar
Payment intents
Cobra con payment intents en tres pasos.
Paso 1Crea un payment intent en tu servidor
Tu servidor crea el intent con el monto de tu pedido, nunca con uno que llegue del navegador. A la página le entrega solo el intent_token.
Tu secret_key va en el encabezado Authorization y nunca sale de tu servidor.
El id de una tarjeta guardada es el que devuelve la captura de tarjeta.
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) => {
// El monto sale de tu pedido, nunca del navegador.
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) =>
{
// El monto sale de tu pedido, nunca del navegador.
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
// El monto sale de tu pedido, nunca del navegador.
$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']);
// El monto sale de tu pedido, nunca del navegador.
$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 y 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 {
// El monto sale de tu pedido, nunca del navegador.
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"
}| Campo | Valores | Por defecto | Para qué |
|---|---|---|---|
currency |
Código ISO 4217 con dos decimales, por ejemplo USD |
Requerido | La moneda de amount. |
save_card |
true: la tarjeta queda guardada para el comprador; false: un solo uso |
true |
Qué pasa con la tarjeta nueva. |
page_origins |
1 a 5 orígenes https://…; http://localhost y http://127.0.0.1 para desarrollar |
Opcional | Solo esas páginas pueden usar el token. |
| Campo | Valores | Por defecto | Para qué |
|---|---|---|---|
currency |
Código ISO 4217 con dos decimales, por ejemplo USD |
Requerido | La moneda de amount. |
payment_method |
Id de una tarjeta guardada del mismo customer.id, activa |
Opcional | Cobra esa tarjeta. |
cvc_required |
true, false |
false (opcional) |
Pide el código de seguridad; solo con payment_method. |
page_origins |
1 a 5 orígenes https://…; http://localhost y http://127.0.0.1 para desarrollar |
Opcional | Solo esas páginas pueden usar el token. |
Paso 2Cobra en tu página
Monta el card element y confirma el pago con el intent_token. Los datos de la tarjeta se escriben en un iframe de n1co, y 3D Secure aparece cuando el banco lo pide.
Confirma el pago con el intent_token; la tarjeta guardada no necesita formulario. 3D Secure aparece cuando el banco lo pide.
HTML
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pagar</title>
</head>
<body>
<form id="payment-form">
<div id="card"></div>
<p id="payment-message" role="alert"></p>
<button id="pay" type="submit" disabled>Pagar $12.50</button>
<button id="pay" type="submit">Pagar $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: 'es' });
const pay = document.querySelector('#pay');
const message = document.querySelector('#payment-message');
// Tu endpoint crea el intent con tu secret_key y devuelve solo su 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') {
// Tu servidor confirma con el webhook antes de entregar el pedido.
window.location.assign('/checkout/gracias');
} else if (intent.status === 'processing') {
message.textContent = 'Estamos confirmando tu pago. No vuelvas a pagar; te avisaremos.';
} else {
message.textContent = intent.last_error?.message ?? 'No se pudo completar el pago.';
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 durante el renderizado en servidor: llámalo en el navegador.
const client = await loadN1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA', { environment: 'sandbox' });
if (client) {
const card = client.elements().create('card', { locale: 'es' });
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' u otro, como en la pestaña HTML.
});
}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 durante el renderizado en servidor; en Nuxt también puedes envolver este componente en <ClientOnly>.
client.value = await loadN1co('pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA', { environment: 'sandbox' });
if (!client.value || !cardBox.value) return;
card.value = client.value.elements().create('card', { locale: 'es' });
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 {
// Tu endpoint crea el intent con tu secret_key y devuelve solo su 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') {
// Tu servidor confirma con el webhook antes de entregar el pedido.
window.location.assign('/checkout/thanks');
} else if (intent.status === 'processing') {
message.value = 'Estamos confirmando tu pago. No vuelvas a pagar; te avisaremos.';
} else {
message.value = intent.last_error?.message ?? 'No se pudo completar el pago.';
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">Pagar $12.50</button>
<button type="submit" :disabled="paying">Pagar $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'; // primera línea de tu 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';
// Desde una app, tu servidor crea el intent sin page_origins: la página que paga es la de n1co.
export function App() {
return (
<N1coProvider publishableKey="pk_3x4mpl3pUbl1shabl3k3yAAAAAAAAAAA" environment="sandbox" locale="es">
<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' u otro, como en la pestaña HTML.
}
return (
<>
<CardField ref={card} onChange={(event) => setComplete(event.complete)} />
<Button title="Pagar" disabled={!complete} onPress={pay} />
</>
);
return <Button title="Pagar" onPress={pay} />;
}| Opción | Valores | Por defecto | Dónde |
|---|---|---|---|
environment |
'production', 'sandbox' |
'production' |
n1co(), loadN1co(), N1coProvider |
locale |
'es', 'en' |
'es' |
create('card', …), N1coProvider |
Métodos y opciones de cada SDK: Web y React Native.
Paso 3Confirma con el webhook
Tu servidor entrega el pedido cuando n1co le avisa del pago, con una firma válida. La verificación está en Configura tus webhooks.
Node.js
import express from 'express';
import { verifyWebhook } from './webhooks.mjs';
// express.raw: la firma se calcula sobre el cuerpo crudo, antes de 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') {
// Entrega el pedido de event.data.object.reference; descarta los event.id repetidos.
}
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")
{
// Entrega el pedido de data.object.reference; descarta los id repetidos.
}
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') {
// Entrega el pedido de $event['data']['object']['reference']; descarta los id repetidos.
}
http_response_code(204);PHP (SDK)
<?php
require __DIR__ . '/webhooks.php'; // $event, verificado por el SDK
if ($event->type === 'payment_intent.succeeded') {
// Entrega el pedido de $event->data->object->reference; descarta los $event->id repetidos.
}
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"))) {
// Entrega el pedido de data.object.reference; descarta los id repetidos.
}
return ResponseEntity.noContent().build();
}
}Ya cobraste tu primer pago.
Tu servidor creó el intent, tu página lo cobró y el webhook lo confirmó.