SDK de PHP
n1co/epay llama al API de ePay desde tu servidor PHP, con modelos tipados, excepciones por estado y verificación de webhooks.
- Instala
n1co/epaycon Composer (PHP 8.2+ conext-curlyext-json). - Crea el cliente con tu llave secreta y el host de desarrollo.
- Crea intents con
paymentIntents->create()y verifica webhooks conWebhook::constructEvent().
composer require n1co/epayCrea el intent en tu endpoint de checkout:
checkout.php
<?php
require __DIR__ . '/vendor/autoload.php';
use N1co\EPay\N1co;
$n1co = new N1co((string) getenv('N1CO_SECRET_KEY'), [
'base_url' => N1co::DEV_BASE_URL, // https://api-epay.dev.n1co.dev
]);
// 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',
]);
header('Content-Type: application/json');
echo json_encode(['client_secret' => $intent->client_secret]);La página confirma el pago como en Cobrar.
Webhooks#
Verifica el cuerpo crudo con tu secreto whsec_…:
Laravel
routes/api.php
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use N1co\EPay\Exception\WebhookSignatureException;
use N1co\EPay\Webhook;
Route::post('/webhooks/n1co', function (Request $request) {
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->headers->all(),
(string) config('services.n1co.webhook_secret'),
);
} catch (WebhookSignatureException) {
abort(400);
}
if ($event->type === 'payment_intent.succeeded') {
// Entrega el pedido de $event->data->object; descarta los $event->id repetidos.
}
return response()->noContent();
});Symfony
src/Controller/WebhookController.php
<?php
namespace App\Controller;
use N1co\EPay\Exception\WebhookSignatureException;
use N1co\EPay\Webhook;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class WebhookController
{
public function __construct(
#[Autowire(env: 'N1CO_WEBHOOK_SECRET')] private string $webhookSecret,
) {}
#[Route('/webhooks/n1co', methods: ['POST'])]
public function n1co(Request $request): Response
{
try {
$event = Webhook::constructEvent($request->getContent(), $request->headers->all(), $this->webhookSecret);
} catch (WebhookSignatureException) {
return new Response('', Response::HTTP_BAD_REQUEST);
}
// Procesa $event->type…
return new Response('', Response::HTTP_NO_CONTENT);
}
}PHP sin framework
webhook.php
<?php
require __DIR__ . '/vendor/autoload.php';
use N1co\EPay\Exception\WebhookSignatureException;
use N1co\EPay\Webhook;
try {
$event = Webhook::constructEvent(
file_get_contents('php://input'),
getallheaders(),
(string) getenv('N1CO_WEBHOOK_SECRET'),
);
} catch (WebhookSignatureException) {
http_response_code(400);
exit;
}
// Procesa $event->type…
http_response_code(204);Errores#
Cada estado HTTP tiene su excepción, y todas extienden N1coException con code(), status(), details() y requestId(). Decide con code(), nunca con el mensaje.
<?php
use N1co\EPay\Exception\InvalidRequestException;
use N1co\EPay\Exception\N1coException;
try {
$n1co->paymentIntents->create($params);
} catch (InvalidRequestException $e) {
error_log(json_encode($e->details())); // field y code
} catch (N1coException $e) {
error_log($e->code() . ' ' . $e->status() . ' ' . $e->requestId());
}getCode() es el estado HTTP, no el código del API. Los códigos están en Errores.
Lo que suele fallar#
- Responder
400a cualquier fallo. Responde400solo conWebhookSignatureException. Si falla tu propio código, responde5xxy n1co reintenta. - El webhook detrás de CSRF. En Laravel, ponlo en
routes/api.phpo exclúyelo devalidateCsrfTokens. - Reintentar un POST a ciegas. El SDK nunca reintenta un POST: el API no tiene claves de idempotencia. Tras una
ConnectionException, lee el objeto o espera su webhook. - Omitir
base_url. Sin él, el cliente usa el host de producción, que llega con el lanzamiento a producción.
Las opciones del cliente, los modelos y los reintentos de GET están en el README del paquete.