Skip to content

Pay.nl Client Library

Overview

Standalone PHP client for the Pay.nl payment gateway API.

Package: atraxion/paynl-client

Installation

composer require atraxion/paynl-client

Configuration

use Atraxion\PayNl\Config;
use Atraxion\PayNl\PayNlClient;

$config = new Config(
    token: 'AT-1234-5678',
    serviceId: 'SL-1234-5678',
    sandbox: false, // true for testing
    timeout: 30
);

$client = new PayNlClient($config);

Core Features

Create Payment

use Atraxion\PayNl\Request\CreatePaymentRequest;
use Atraxion\PayNl\Model\Customer;
use Atraxion\PayNl\Model\Address;

$request = new CreatePaymentRequest(
    amount: 12500, // €125.00 in cents
    currency: 'EUR',
    returnUrl: 'https://shop.example.com/payment/return',
    exchangeUrl: 'https://shop.example.com/payment/webhook',
    description: 'Order #12345',
    reference: '12345',
    customer: new Customer(
        firstName: 'Jan',
        lastName: 'Janssen',
        email: 'jan@example.com',
        phone: '+31612345678',
        language: 'nl'
    ),
    billingAddress: new Address(
        street: 'Hoofdstraat',
        houseNumber: '1',
        zipCode: '1234AB',
        city: 'Amsterdam',
        country: 'NL'
    ),
    paymentMethodId: 10, // iDEAL
    issuerId: 'ABNANL2A' // ABN AMRO
);

$payment = $client->createPayment($request);

// Redirect customer
header('Location: ' . $payment->getPaymentUrl());

Get Payment Status

use Atraxion\PayNl\Enum\PaymentStatus;

$status = $client->getPaymentStatus('1234567890X12345');

if ($status->getState() === PaymentStatus::PAID) {
    // Payment successful
    $paidAmount = $status->getAmountPaid();
    $paymentMethod = $status->getPaymentMethod();
}

Refund Payment

use Atraxion\PayNl\Request\RefundRequest;

$refund = $client->refund(new RefundRequest(
    transactionId: '1234567890X12345',
    amount: 5000, // €50.00 partial refund
    description: 'Partial refund for returned item'
));

// Full refund
$fullRefund = $client->refund(new RefundRequest(
    transactionId: '1234567890X12345'
    // amount omitted = full refund
));

Get Payment Methods

$methods = $client->getPaymentMethods();

foreach ($methods as $method) {
    echo $method->getId(); // 10
    echo $method->getName(); // 'iDEAL'
    echo $method->getMinAmount(); // 100 (€1.00)
    echo $method->getMaxAmount(); // 5000000 (€50,000)

    // Get issuers for iDEAL
    if ($method->hasIssuers()) {
        foreach ($method->getIssuers() as $issuer) {
            echo $issuer->getId(); // 'ABNANL2A'
            echo $issuer->getName(); // 'ABN AMRO'
        }
    }
}

Models

Payment

readonly class Payment
{
    public function __construct(
        public string $transactionId,
        public string $paymentUrl,
        public ?string $popupUrl = null,
        public ?string $qrUrl = null
    ) {}
}

PaymentStatus

readonly class PaymentStatus
{
    public function __construct(
        public string $transactionId,
        public PaymentStatus $state,
        public int $amount,
        public int $amountPaid,
        public string $currency,
        public ?string $paymentMethod,
        public ?string $paymentMethodName,
        public ?\DateTimeImmutable $paidAt,
        public array $refunds = []
    ) {}

    public function isPaid(): bool
    {
        return $this->state === PaymentStatus::PAID;
    }

    public function isPending(): bool
    {
        return $this->state === PaymentStatus::PENDING;
    }

    public function isCancelled(): bool
    {
        return in_array($this->state, [
            PaymentStatus::CANCELLED,
            PaymentStatus::EXPIRED,
            PaymentStatus::DENIED
        ]);
    }
}

PaymentStatus Enum

enum PaymentStatus: string
{
    case PENDING = 'pending';
    case PAID = 'paid';
    case CANCELLED = 'cancelled';
    case EXPIRED = 'expired';
    case DENIED = 'denied';
    case REFUND = 'refund';
    case PARTIAL_REFUND = 'partial_refund';
    case AUTHORIZE = 'authorize';
    case CHARGEBACK = 'chargeback';
}

Webhook Handling

use Atraxion\PayNl\Webhook\WebhookParser;
use Atraxion\PayNl\Webhook\WebhookValidator;

// Parse incoming webhook
$parser = new WebhookParser();
$webhook = $parser->parse($_POST);

// Validate webhook signature (recommended)
$validator = new WebhookValidator($config);
if (!$validator->isValid($_POST, $_SERVER['HTTP_PAYNL_SIGNATURE'] ?? '')) {
    throw new \RuntimeException('Invalid webhook signature');
}

// Handle webhook
switch ($webhook->getAction()) {
    case 'pay':
        // Payment received
        $transactionId = $webhook->getTransactionId();
        $orderId = $webhook->getOrderId();
        break;

    case 'refund':
        // Refund processed
        break;

    case 'chargeback':
        // Chargeback received
        break;
}

// Always respond with TRUE
echo 'TRUE';

Error Handling

use Atraxion\PayNl\Exception\ApiException;
use Atraxion\PayNl\Exception\AuthenticationException;
use Atraxion\PayNl\Exception\ValidationException;

try {
    $payment = $client->createPayment($request);
} catch (AuthenticationException $e) {
    // Invalid API token
    $this->logger->error('Pay.nl auth failed', ['error' => $e->getMessage()]);
} catch (ValidationException $e) {
    // Invalid request parameters
    $errors = $e->getValidationErrors();
} catch (ApiException $e) {
    // Other API error
    $this->logger->error('Pay.nl API error', [
        'code' => $e->getCode(),
        'message' => $e->getMessage(),
        'response' => $e->getResponse()?->getBody(),
    ]);
}

Testing

Mock Responses

use Atraxion\PayNl\Testing\MockPayNlClient;

$mockClient = new MockPayNlClient();

$mockClient->addResponse('createPayment', new Payment(
    transactionId: 'TEST123',
    paymentUrl: 'https://pay.nl/test'
));

// Use in tests
$payment = $mockClient->createPayment($request);
assert($payment->transactionId === 'TEST123');

Sandbox Mode

$config = new Config(
    token: 'AT-1234-5678',
    serviceId: 'SL-1234-5678',
    sandbox: true // Uses sandbox API
);

Gherkin Scenarios

Feature: Pay.nl Client
  As a developer
  I want to integrate Pay.nl payments
  Using a clean client library

  Scenario: Create iDEAL payment
    Given valid Pay.nl credentials
    When I create a payment for €125.00 with iDEAL
    Then I should receive a transaction ID
    And I should receive a payment URL

  Scenario: Get payment status after success
    Given a successful payment "TX123"
    When I check the payment status
    Then status should be "paid"
    And paid amount should match

  Scenario: Handle webhook for payment
    Given a payment webhook arrives
    When I parse the webhook
    Then I should receive transaction details
    And I should validate the signature

  Scenario: Partial refund
    Given a paid transaction for €100
    When I refund €30
    Then refund should be created
    And transaction status should be "partial_refund"

  Scenario: Invalid credentials
    Given invalid Pay.nl credentials
    When I try to create a payment
    Then AuthenticationException should be thrown

Configuration Reference

Parameter Type Required Default Description
token string Yes - API token (AT-xxxx-xxxx)
serviceId string Yes - Service ID (SL-xxxx-xxxx)
sandbox bool No false Use sandbox API
timeout int No 30 Request timeout in seconds
baseUrl string No auto Override API base URL

API Endpoints Used

Endpoint Method Purpose
/v2/Transaction/start POST Create payment
/v2/Transaction/status GET Get status
/v2/Transaction/refund POST Create refund
/v2/Transaction/getPaymentMethods GET List methods
/v2/Transaction/cancel POST Cancel payment