PHP SDK
n1co/epay calls the ePay API from your PHP server, with typed models, exceptions by status and webhook verification.
- Install
n1co/epaywith Composer (PHP 8.2+ withext-curlandext-json). - Create the client with your secret key and the development host.
- Create intents with
paymentIntents->create()and verify webhooks withWebhook::constructEvent().
composer require n1co/epayCreate the intent in your checkout endpoint:
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
]);
// 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',
]);
header('Content-Type: application/json');
echo json_encode(['client_secret' => $intent->client_secret]);The page confirms the payment as in Charge.
Webhooks#
Verify the raw body with your whsec_… secret:
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') {
// Fulfill the order in $event->data->object; skip repeated $event->id values.
}
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);
}
// Handle $event->type…
return new Response('', Response::HTTP_NO_CONTENT);
}
}Plain PHP
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;
}
// Handle $event->type…
http_response_code(204);Errors#
Each HTTP status has its exception, and all of them extend N1coException with code(), status(), details() and requestId(). Branch on code(), never on the message.
<?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 and code
} catch (N1coException $e) {
error_log($e->code() . ' ' . $e->status() . ' ' . $e->requestId());
}getCode() is the HTTP status, not the API code. The codes are in Errors.
Common mistakes#
- Answering
400to every failure. Answer400only forWebhookSignatureException. If your own code fails, answer5xxand n1co retries. - The webhook behind CSRF. In Laravel, put it in
routes/api.phpor exclude it fromvalidateCsrfTokens. - Retrying a POST blindly. The SDK never retries a POST: the API has no idempotency keys. After a
ConnectionException, read the object or wait for its webhook. - Leaving out
base_url. Without it, the client uses the production host, which arrives with the production release.
The client options, the models and GET retries are in the package README.