Skip to content

Transactions ​

Introduction ​

A transaction is a single payment you start from your own server. You choose the payment method on your checkout, send the amount and the order it pays for, and ING Pay answers with the transaction it opened and, usually, where to send the payer next. The payment itself finishes later, on the payer's side, and you learn the outcome from a notification. If you would rather not build the method picker yourself, a payment link or the paywall hands that part to ING Pay; Choosing an Integration compares them.

Creating a Transaction ​

To start a transaction, pass a CreateTransactionRequest to createTransaction. It needs the amount, the currency, your order's identifier, the payment method, and the two addresses the payer returns to:

php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Api\CreateTransactionRequest;
use Routegroup\IngPay\Types\Currency;
use Routegroup\IngPay\Types\PaymentMethod;
use Routegroup\IngPay\Types\PaymentMethodCode;

$response = $api->createTransaction(new CreateTransactionRequest([
    'amount' => 4999,
    'currency' => Currency::PLN,
    'orderId' => 'ORDER-1042',
    'paymentMethod' => PaymentMethod::PAY_BY_LINK,
    'paymentMethodCode' => PaymentMethodCode::ING,
    'successReturnUrl' => 'https://shop.example/orders/1042/paid',
    'failureReturnUrl' => 'https://shop.example/orders/1042/failed',
    'customer' => [
        'firstName' => 'Anna',
        'lastName' => 'Kowalska',
        'email' => 'anna@example.com',
    ],
]));

$transactionId = $response->transaction->id;
php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Api\CreateTransactionRequest;
use Routegroup\IngPay\Types\Currency;
use Routegroup\IngPay\Types\PaymentMethod;
use Routegroup\IngPay\Types\PaymentMethodCode;

$response = app(Api::class)->createTransaction(new CreateTransactionRequest([
    'amount' => 4999,
    'currency' => Currency::PLN,
    'orderId' => 'ORDER-1042',
    'paymentMethod' => PaymentMethod::PAY_BY_LINK,
    'paymentMethodCode' => PaymentMethodCode::ING,
    'successReturnUrl' => route('orders.paid', 1042),
    'failureReturnUrl' => route('orders.failed', 1042),
    'customer' => [
        'firstName' => 'Anna',
        'lastName' => 'Kowalska',
        'email' => 'anna@example.com',
    ],
]));

$transactionId = $response->transaction->id;

Your merchant and service ID are added from your configuration as the request is sent, so the request never carries them. The response holds the transaction that was opened, with its id. Store it next to your order: it is how you will recognise the transaction in every notification and every later call.

NOTE

Amounts are integers in the currency's smallest unit. 4999 in PLN is 49.99 zł.

BLIK With a Code ​

If your checkout collects the six-digit BLIK code itself, send it as blikCode:

php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Api\CreateTransactionRequest;
use Routegroup\IngPay\Types\Currency;
use Routegroup\IngPay\Types\PaymentMethod;
use Routegroup\IngPay\Types\PaymentMethodCode;

$response = $api->createTransaction(new CreateTransactionRequest([
    'amount' => 4999,
    'currency' => Currency::PLN,
    'orderId' => 'ORDER-1043',
    'paymentMethod' => PaymentMethod::BLIK,
    'paymentMethodCode' => PaymentMethodCode::BLIK,
    'blikCode' => '777123',
    'successReturnUrl' => 'https://shop.example/orders/1043/paid',
    'failureReturnUrl' => 'https://shop.example/orders/1043/failed',
    'customer' => [
        'firstName' => 'Anna',
        'lastName' => 'Kowalska',
        'email' => 'anna@example.com',
    ],
]));
php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Api\CreateTransactionRequest;
use Routegroup\IngPay\Types\Currency;
use Routegroup\IngPay\Types\PaymentMethod;
use Routegroup\IngPay\Types\PaymentMethodCode;

$response = app(Api::class)->createTransaction(new CreateTransactionRequest([
    'amount' => 4999,
    'currency' => Currency::PLN,
    'orderId' => 'ORDER-1043',
    'paymentMethod' => PaymentMethod::BLIK,
    'paymentMethodCode' => PaymentMethodCode::BLIK,
    'blikCode' => '777123',
    'successReturnUrl' => route('orders.paid', 1043),
    'failureReturnUrl' => route('orders.failed', 1043),
    'customer' => [
        'firstName' => 'Anna',
        'lastName' => 'Kowalska',
        'email' => 'anna@example.com',
    ],
]));

The payer confirms the payment in their banking app, so there is nowhere to send them, and the response carries no action.

Sending the Payer On ​

Most transactions answer with an action, which tells you what the payer has to do next. A redirect sends them to url, usually their bank. A transfer asks them to wire the money themselves, to the account in ban with the transfer title in title. No action at all means there is nothing for the payer to do on your side:

php
use Routegroup\IngPay\Response\TransactionResponse;
use Routegroup\IngPay\Types\ActionType;

$action = $response->action;

$next = match ($action?->type) {
    ActionType::REDIRECT => ['redirect', $action->url],
    ActionType::TRANSFER => ['show', "Pay to {$action->ban}, title: {$action->title}"],
    null => ['wait', null],
};
php
use Routegroup\IngPay\Response\TransactionResponse;
use Routegroup\IngPay\Types\ActionType;

$action = $response->action;

$next = match ($action?->type) {
    ActionType::REDIRECT => redirect()->away((string) $action->url),
    ActionType::TRANSFER => view('checkout.transfer', ['account' => $action->ban, 'title' => $action->title]),
    null => view('checkout.waiting'),
};

WARNING

A payer arriving at your successReturnUrl has not proved anything: the address can be opened by hand. Fulfil the order when a signed notification says the transaction is settled, or when you have read it back yourself.

Reading a Transaction ​

To check where a transaction stands, ask for it by its ID:

php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Types\TransactionStatus;

$transaction = $api->getTransaction($transactionId)->transaction;

if ($transaction->status === TransactionStatus::SETTLED) {
    // the money is in; fulfil the order
}
php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Types\TransactionStatus;

$transaction = app(Api::class)->getTransaction($transactionId)->transaction;

if ($transaction->status === TransactionStatus::SETTLED) {
    // the money is in; fulfil the order
}

A new transaction is pending. It ends as settled once the money is in, as rejected if the payer's bank refused it, or as cancelled if it expired unpaid. Every status is listed under TransactionStatus.

Reading is the fallback, not the main road. Notifications tell you the moment a status changes, so poll only when one is overdue.

In Laravel ​

The Api is bound in the container, built from services.ing_pay, and sends through Laravel's HTTP client. Resolve it with app(Api::class) or inject it into a controller. Nothing else in this guide changes: the requests, responses and statuses are the same classes.

Because requests go through the HTTP client, Http::fake() intercepts them in your tests, and every factory carries your configured merchant and service while tests run.

Testing ​

Your code depends on Api, so a test builds one over a FakeClient. The fake answers with what you give it and records what was sent, and every response has a factory that builds a valid answer:

php
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Api\CreateTransactionRequest;
use Routegroup\IngPay\Http\FakeClient;
use Routegroup\IngPay\Lib\Config;
use Routegroup\IngPay\Lib\Url;
use Routegroup\IngPay\Response\TransactionResponse;

$client = FakeClient::answering(
    TransactionResponse::factory()->make()->toArray(),
);

$api = new Api($client, $config, new Url($config));

$api->createTransaction(CreateTransactionRequest::factory()->make(['amount' => 4999]));

expect($client->lastRequest()?->body['amount'])->toBe(4999);
php
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Routegroup\IngPay\Api;
use Routegroup\IngPay\Api\CreateTransactionRequest;
use Routegroup\IngPay\Response\TransactionResponse;

Http::fake([
    '*' => Http::response(TransactionResponse::factory()->make()->toArray()),
]);

app(Api::class)->createTransaction(CreateTransactionRequest::factory()->make(['amount' => 4999]));

Http::assertSent(fn (Request $request): bool => $request['amount'] === 4999);

A fake that runs out of answers throws instead of inventing one, so a call you did not expect fails the test. Testing covers queued answers, factories and shared state in full.