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:
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;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:
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',
],
]));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:
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],
};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:
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
}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:
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);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.