n1coePayDocs API reference

PHP SDK

n1co/epay calls the ePay API from your PHP server, with typed models, exceptions by status and webhook verification.

  1. Install n1co/epay with Composer (PHP 8.2+ with ext-curl and ext-json).
  2. Create the client with your secret key and the development host.
  3. Create intents with paymentIntents->create() and verify webhooks with Webhook::constructEvent().
composer require n1co/epay

Create 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 400 to every failure. Answer 400 only for WebhookSignatureException. If your own code fails, answer 5xx and n1co retries.
  • The webhook behind CSRF. In Laravel, put it in routes/api.php or exclude it from validateCsrfTokens.
  • 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.