Start here
Overview
You resolve one gateway from the factory and call its actions. Every method below takes a request
DTO and returns a result DTO, and reads the same across drivers — only
createCheckoutSession and the signing scheme differ.
$gateway = $gateways->make(GatewayName::CybersourceUnifiedCheckout);
// every action below is called on $gateway
An action a gateway doesn't support throws UnsupportedOperationException, so the
action list only shows what this driver actually implements.
Payment flow
Create checkout
createCheckoutSession(CheckoutSessionRequest): CheckoutSession
Starts a payment. The result carries what this gateway needs next — a jwt,
a redirectUrl, and a reference to reconcile against later.
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(10000, 'EGP'),
targetOrigins: ['https://shop.test'], // origin(s) that embed the widget
orderReference: 'ORDER-123', // reconcile — and default the charge idempotency key — off this
allowedPaymentTypes: ['PANENTRY', 'GOOGLEPAY'], // cards + wallets (defaults to ['PANENTRY'])
));
// load the widget script from $session->clientLibrary (pin it with
// $session->clientLibraryIntegrity), then initialise the widget with
// the capture context in $session->jwt
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(15000, 'EGP'),
orderReference: 'ORDER-124',
returnUrl: 'https://shop.test/return',
customer: new Customer(email: 'ada@shop.test'),
));
// redirect the payer to $session->redirectUrl
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(15000, 'EGP'),
orderReference: 'ORDER-125',
paymentMethod: 'card',
options: new PaymobCheckoutOptions(integrationId: 111111, iframeId: 222222),
));
// redirect to $session->redirectUrl (the Paymob iframe)
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(25000, 'USD'),
orderReference: 'ORDER-126',
description: 'Gold Plan',
returnUrl: 'https://shop.test/return',
options: new PaylinkCheckoutOptions(iframe: true), // embeddable checkout URL
));
// embed $session->redirectUrl in an <iframe> (or redirect without the option)
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(12030, 'SAR'),
orderReference: 'ORDER-127',
returnUrl: 'https://shop.test/return',
paymentMethod: 'invoice', // 'invoice' | 'paylink' | 'managed' — selects the integration type
options: new PaytabsCheckoutOptions(
iframe: true, // embed the page in an <iframe> instead of redirecting
tokenise: 2, // save the card as a reusable token
agreement: new PaytabsAgreement(/* … */), // repeat billing — PayTabs auto-bills the schedule
splitPayout: [new PaytabsSplitPayout(/* … */)], // split the settled funds across beneficiaries
webhookUrl: 'https://shop.test/ipn',
),
));
// redirect to $session->redirectUrl (hosted page); $session->reference is the tran_ref
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(10000, 'USD'),
orderReference: 'ORDER-127',
description: 'Gold Plan',
returnUrl: 'https://shop.test/return', // PayPal redirects here after approval
paymentMethod: 'authorize', // omit to capture on approval (intent CAPTURE)
options: new PayPalCheckoutOptions( // typed options — no ambiguous array
cancelUrl: 'https://shop.test/cancel',
brandName: 'Example',
userAction: PayPalUserAction::PayNow,
),
));
// redirect to $session->redirectUrl (PayPal approval page);
// $session->reference is the order id — pass it to charge() after approval
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(25000, 'USD'),
orderReference: 'ORDER-128', // becomes the MPGS order id
description: 'Goods and Services',
options: new MpgsCheckoutOptions(
operation: 'PURCHASE', // PURCHASE | AUTHORIZE | VERIFY
merchantName: 'Example LLC',
returnUrl: 'https://shop.test/return',
),
));
// launch Hosted Checkout on the front end with $session->reference (the session id),
// loading the checkout script from $session->clientLibrary
Payment flow
Confirm orchestrated payment
confirmOrchestratedPayment(ConfirmOrchestratedPaymentRequest): OrchestratedPaymentResult
The orchestrated (autoProcessing) alternative to charge: pass a
completeMandate to createCheckoutSession so the widget runs
Decision Manager, 3-D Secure, authorization, and TMS tokenization client-side and
resolves with a signed result JWT, then verify that JWT here and trust it — no
/pts/v2/payments call and no transaction-search lag.
// 1. Mint a capture context that tells the widget to auto-complete the payment.
$session = $gateway->createCheckoutSession(new CheckoutSessionRequest(
money: Money::minor(10000, 'EGP'),
targetOrigins: ['https://shop.test'],
orderReference: 'ORDER-123',
completeMandate: MandateCompletionType::Capture, // Capture (sale) | Auth (hold only)
));
// hand $session->jwt to checkout.mount(); it resolves with a signed result JWT.
// 2. Verify that result JWT against the capture context and trust the outcome.
$result = $gateway->confirmOrchestratedPayment(new ConfirmOrchestratedPaymentRequest(
resultJwt: $resultJwt, // returned by checkout.mount() on the front end
captureContextJwt: $session->jwt, // source of the RS256 verification key (flx.jwk)
expectedMoney: Money::minor(10000, 'EGP'),
orderReference: 'ORDER-123',
));
// $result->status is Captured (Authorized for an AUTH mandate). For a real card,
// $result->instrumentIdentifierId / paymentInstrumentId / customerId are the reusable
// TMS token for later installments; a wallet sets $result->isWallet with no token.
Payment flow
DCC rate
requestDccRate(DccRateRequest): DccQuote
Quote a Dynamic Currency Conversion rate so a foreign cardholder pays in their own
currency. Thread the returned DccQuote into charge — and
capture / refund / reverseAuthorization — so the
same quoted rate is echoed across the lifecycle. Set money to the quote's
convertedAmount.
$quote = $gateway->requestDccRate(new DccRateRequest(
money: Money::minor(48000, 'EGP'), // 480.00 EGP
cardNumber: '4111111111111111',
));
if ($quote->offered) {
$gateway->charge(new ChargeRequest(
transientToken: $tokenFromWidget,
money: $quote->convertedAmount, // billing currency, quoted rate
dcc: $quote,
));
}
Payment flow
Charge
charge(ChargeRequest): PaymentResult
Charge server-to-server against the one-time reference the checkout step returned — the token or approved-order id captured up front. The idempotency key defaults to the order reference, so a retried charge for the same order is deduplicated.
$result = $gateway->charge(new ChargeRequest(
transientToken: $tokenFromWidget,
money: Money::minor(10000, 'EGP'),
orderReference: 'ORDER-123', // = idempotency key
));
$result = $gateway->charge(new ChargeRequest(
transientToken: $paymentToken, // Own Form browser-generated token
money: Money::minor(9500, 'SAR'),
orderReference: 'ORDER-127',
));
// 3-D Secure card → status Pending + $result->raw['redirect_url'] to send the payer
$result = $gateway->charge(new ChargeRequest(
transientToken: $session->reference, // the approved PayPal order id
money: Money::minor(10000, 'USD'),
capture: true, // capture the order; false authorizes a hold instead
));
// status Captured (or Authorized when capture: false);
// $result->transactionId is the capture / authorization id for follow-ons
$result = $gateway->charge(new ChargeRequest(
transientToken: $sessionId, // the MPGS Hosted Session id holding the card
money: Money::minor(10000, 'USD'),
orderReference: 'ORDER-128', // the MPGS order id
idempotencyKey: 'txn-1', // becomes the MPGS transaction id
));
// PAY → status Captured; capture: false authorises instead (AUTHORIZE)
$result = $gateway->charge(new ChargeRequest(
transientToken: $opaqueDataValue, // the Accept.js opaqueData dataValue (nonce)
money: Money::minor(5000, 'USD'),
orderReference: 'ORDER-129', // Authorize.Net invoiceNumber / refId
capture: true, // false authorises only (authOnlyTransaction)
));
// status Captured (Authorized when capture: false); $result->transactionId is the transId
Fraud screening
Device fingerprinting
Decision Manager profiles the shopper's device so a charge can be fraud-screened. It has two halves: a profiling tag you embed on the checkout page, and a session id you send on the request. The browser tag is yours to embed — this package never renders HTML.
org_id is CyberSource's standard Decision Manager profiling org id (shared, not a
per-merchant secret): 1snn5n9w in test, k8vif92e in production.
session_id is your merchant id concatenated with a fresh per-page-load
crypto.randomUUID() (max 88 chars, [A-Za-z0-9_-]). Add it above
</body> — tags.js supersedes the legacy check.js:
const orgId = isTestMode ? '1snn5n9w' : 'k8vif92e';
const sessionId = crypto.randomUUID(); // the <session id>; send THIS to the API
const tag = document.createElement('script');
tag.src = 'https://h.online-metrix.net/fp/tags.js?org_id=' + orgId +
'&session_id=' + encodeURIComponent(merchantId + sessionId);
document.head.appendChild(tag);
On the request, send only the <session id> part as
deviceFingerprintId — it maps to deviceInformation.fingerprintSessionId.
It is accepted on charge, chargeStoredCredential, and
enrollPayerAuth:
$result = $gateway->charge(new ChargeRequest(
transientToken: $tokenFromWidget,
money: Money::minor(10000, 'EGP'),
deviceFingerprintId: $sessionId, // the UUID from the tag (the <session id> part), NOT merchantId + UUID
));
Set useRawFingerprintSessionId: true only when you sent the session id to the tag
without the merchant-id prefix; for the standard tag above, leave it false.
In the orchestrated flow (a completeMandate on
createCheckoutSession) there is no fingerprintSessionId to send — the
widget runs Decision Manager itself. Toggle it with decisionManager on
CheckoutSessionRequest (default true), emitted as
completeMandate.decisionManager.
Payment flow
Capture
capture(CaptureRequest): PaymentResult
Settles a prior authorization. Pass a key unique to the operation; partial captures each need their own.
$result = $gateway->capture(new CaptureRequest(
transactionId: '7040000000000000001',
money: Money::minor(15000, 'EGP'),
idempotencyKey: 'capture:invoice-123',
));
Payment flow
Void
void(VoidRequest): PaymentResult
Cancels an authorization before it settles. Nothing is captured.
$result = $gateway->void(new VoidRequest(
transactionId: '7040000000000000001',
idempotencyKey: 'void:invoice-123',
));
Payment flow
Refund
refund(RefundRequest): RefundResult
Returns settled funds and accepts partials. Retrying with the same key is a no-op at the gateway — never a double refund — so give each partial its own key.
$result = $gateway->refund(new RefundRequest(
transactionId: '7040000000000000001',
money: Money::minor(2500, 'EGP'),
idempotencyKey: 'refund:invoice-123:1',
));
After the payment
Get transaction
getTransaction(id): TransactionSnapshot
Fetches the authoritative status by gateway transaction id. Returns a
TransactionSnapshot with a normalized PaymentStatus, the amount and the
order reference. The Reconcile action below batches this lookup across many ids.
$snapshot = $gateway->getTransaction('7040000000000000001');
// $snapshot->status, $snapshot->amount, $snapshot->orderReference
After the payment
Search transaction
searchTransaction(ref): TransactionSnapshot
Looks the payment up by your own order reference instead of the gateway id — handy when you only kept the reference you sent.
$snapshot = $gateway->searchTransaction('ORDER-124');
// same TransactionSnapshot, resolved from your reference
After the payment
Reconcile
reconcile(GatewayName, ids): ReconciliationOutcome[]
Reconcile a batch of transaction ids against this gateway in one call. Each id is fetched via
getTransaction and returned as a ReconciliationOutcome — a failed lookup
is captured as an error instead of aborting the batch, so every id is accounted for. Inject the
TransactionReconciler use-case; it's registered in the container.
$outcomes = $reconciler->reconcile(GatewayName::CybersourceUnifiedCheckout, [
'7040000000000000001',
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::Fawry, [
'ORDER-124', 'ORDER-125',
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::Paymob, [
'123456789',
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::Paylink, [
'INV-0001',
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::Paytabs, [
'TST2000000000001',
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::PayPal, [
'7NK74838L4813105R', // a PayPal order id
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::Mpgs, [
'ORDER-128', // MPGS order ids
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
$outcomes = $reconciler->reconcile(GatewayName::AuthorizeNet, [
'40000000001', // Authorize.Net transaction ids (transId)
]);
foreach ($outcomes as $outcome) {
// $outcome->reconciled(), $outcome->snapshot?->status, $outcome->error
}
After the payment
Verify webhook
verifyWebhook(payload, headers): WebhookEvent
Verify the signature first — it's unauthenticated until you do, and carries no timestamp. Then apply your own idempotency on the transaction or invoice id.
$event = $gateway->verifyWebhook($request->getContent(), $request->headers->all());
if ($event->verified) {
// $event->eventType, $event->transactionId, $event->status
}
Cross-cutting
Events
Event::listen(PaymentEvent::class, listener): void
Every driver emits a typed domain event after each operation. All events implement the
PaymentEvent interface, so one listener receives them all — or target a single
event type. Events fire on completion (success or decline); the result carries the outcome.
Payloads are queue-safe: ids, amount and result, never the raw request or card data.
// one subscriber, every event — match on the concrete type
final class PaymentEventSubscriber
{
public function handle(PaymentEvent $event): void
{
match (true) {
$event instanceof PaymentCaptured => $this->markOrderPaid($event->orderReference, $event->result),
$event instanceof PaymentRefunded => $this->recordRefund($event->transactionId, $event->result),
$event instanceof WebhookReceived => $this->applyWebhook($event->webhook),
default => null, // charge/void/vault/checkout — ignored here
};
}
}
// register once; receives every event via the interface
Event::listen(PaymentEvent::class, PaymentEventSubscriber::class);
// config/gateway.php — on by default; 'log' attaches the redaction-safe audit listener
'events' => ['enabled' => true, 'log' => false]
Cross-cutting
Operation logging
LoggingGateway(driver, logger): PaymentGatewayInterface
Wrap every driver in a LoggingGateway that logs each operation — charge, capture,
refund, getTransaction, verifyWebhook, … — with its duration and a safe correlation context
(gateway, order/transaction ids, amount) through your PSR-3 logger. The context carries no PAN,
cvv or tokens, and the underlying LogsAction trait masks sensitive keys as a backstop.
Distinct from http.logging, which logs the lower-level HTTP request/response metadata.
With gateway.logging.operations enabled the factory wraps every driver for you. To
compose it yourself — outside Laravel, or around a driver you built — wrap it with any PSR-3
logger:
use Hyprpay\Payments\Application\PaymentGatewayFactory;
use Hyprpay\Payments\Infrastructure\Gateway\LoggingGateway;
use Illuminate\Support\Facades\Log;
final class PaymentGatewayProvider
{
public function __construct(private PaymentGatewayFactory $factory) {}
/**
* Resolve a PayPal gateway that logs every operation with its duration.
*
* Wraps the driver in a LoggingGateway, so each call is recorded as
* [LoggingGateway] {operation} through the "payments" channel with a
* masked, PAN-free context. Enabling gateway.logging.operations makes the
* factory do this automatically, so this wrapper becomes unnecessary.
*/
public function payments(): PaymentGatewayInterface
{
return new LoggingGateway(
$this->factory->make(GatewayName::PayPal),
Log::channel('payments'),
);
}
}
Each call then lands in your PSR-3 log at info — the message plus a structured context:
[paypal] charge
{
"gateway": "paypal",
"order_reference": "ORDER-123",
"amount": "100.00",
"currency": "USD",
"duration_ms": 84.2
}
The log is identified by gateway + operation; the message carries the operation, the context the
gateway. Request-scoped fields — request_id, ip, url — aren't added by the SDK (it stays
framework-agnostic and runs in CLI/queue where there is no request). Add them once to your app's
log context so they land on every line, these included; the timestamp is already stamped by the
logger. For the initiator's own name, use the LogsAction trait in your action class —
there the action field is your class.
// e.g. in middleware — attaches request_id/ip/url to every subsequent log line
Log::shareContext([
'request_id' => (string) Str::uuid(),
'ip' => $request->ip(),
'url' => $request->fullUrl(),
]);
// or tag one wrapper with static extra fields via the constructor hook:
new LoggingGateway($driver, $logger, ['component' => 'checkout']);
The SDK logs to its own daily file — storage/logs/hyprpay-2026-08-08.log by default,
kept out of your app log. With shared context in place the line reads — timestamp from the
logger, request_id/ip/url from your shared context, the rest from the SDK:
[2026-08-08 10:15:42] production.INFO: [paypal] charge
{
"request_id": "9b1e5b1e-3c2a-4f77-9c1e-2b0f5a7d1e42",
"ip": "203.0.113.7",
"url": "https://shop.test/checkout",
"gateway": "paypal",
"order_reference": "ORDER-123",
"amount": "100.00",
"currency": "USD",
"duration_ms": 84.2
}
Cross-cutting
Credential resolver
CredentialResolver::resolve(GatewayName): GatewayCredentials
The factory never reads keys itself — it asks a CredentialResolver port. The default
ConfigCredentialResolver loads the gateway.gateways.{key} block from
Laravel config and hydrates a GatewayCredentials DTO, throwing
MissingCredentialsException when the block is missing, blank or incomplete. Rebind the
port to source credentials from anywhere — a per-tenant table, a secrets manager, an encrypted
vault — and every driver the factory builds picks them up, no driver code touched.
use Hyprpay\Payments\Domain\Contract\CredentialResolver;
use Hyprpay\Payments\Domain\Enum\GatewayName;
use Hyprpay\Payments\Domain\Exception\MissingCredentialsException;
use Hyprpay\Payments\Domain\ValueObject\GatewayCredentials;
// resolve the active tenant's keys instead of static config
final readonly class TenantCredentialResolver implements CredentialResolver
{
public function __construct(private TenantContext $tenant) {}
public function resolve(GatewayName $gateway): GatewayCredentials
{
$secrets = $this->tenant->current()
->gatewaySecrets($gateway->value)
?? throw MissingCredentialsException::forGateway($gateway);
return GatewayCredentials::fromConfig($secrets);
}
}
// swap the port once, in a service provider — the factory now uses it everywhere
$this->app->bind(CredentialResolver::class, TenantCredentialResolver::class);
Cross-cutting
HTTP client
HttpClient::send(HttpRequest): HttpResponse
Every driver reaches its gateway API through the HttpClient port — one
send(HttpRequest): HttpResponse method, no Laravel HTTP types leaking into the domain.
The container assembles the default stack for you from the http.* config: a
LaravelHttpClient transport, optionally wrapped by RateLimitingHttpClient
and LoggingHttpClient, with RetryingHttpClient on the outside. Each
decorator is just another HttpClient, so you can add your own — tracing, a circuit
breaker, a signing proxy — by extending the binding, keeping the retry/rate-limit/logging stack the
SDK already built.
use Hyprpay\Payments\Domain\Contract\HttpClient;
use Hyprpay\Payments\Domain\Http\HttpRequest;
use Hyprpay\Payments\Domain\Http\HttpResponse;
// a decorator that records every outbound call on your APM span
final readonly class TracingHttpClient implements HttpClient
{
public function __construct(private HttpClient $inner) {}
public function send(HttpRequest $request): HttpResponse
{
return Tracer::span("http {$request->method} {$request->url}", fn () => $this->inner->send($request));
}
}
// wrap whatever the SDK already built — its stack stays underneath yours
$this->app->extend(HttpClient::class, fn (HttpClient $inner) => new TracingHttpClient($inner));
In tests, bind the same port to FakeHttpClient: it records every request and replays
queued responses in order (falling back to a 200 {}), so you assert on what the driver
sent without touching the network.
use Hyprpay\Payments\Domain\Contract\HttpClient;
use Hyprpay\Payments\Infrastructure\Http\FakeHttpClient;
$http = (new FakeHttpClient())
->queueJson(['status' => 'captured']);
$this->app->instance(HttpClient::class, $http);
// ... exercise a gateway action, then assert on the captured request
$this->assertSame(1, $http->requestCount());
$this->assertSame('POST', $http->lastRequest()->method);
Cross-cutting
Configuration
config('gateway.*'): mixed
Publish it with php artisan vendor:publish --tag=gateway-config. Every key, its
env var, and default, broken down below.
/**
* config/gateway.php — every setting at a glance.
*
* default string Gateway used when make() is called without one. (GATEWAY_DEFAULT)
*
* http.timeout int Per-request timeout, seconds. (GATEWAY_HTTP_TIMEOUT = 30)
* http.retries int Retries for transient failures (408/429/5xx). (GATEWAY_HTTP_RETRIES = 2)
* http.retry_base_delay_ms int Base backoff in ms, doubled per retry. (GATEWAY_HTTP_RETRY_BASE_MS = 200)
* http.logging bool Log HTTP request/response metadata — no bodies. (GATEWAY_HTTP_LOGGING = false)
* http.rate_limit bool Token-bucket throttle for outbound requests. (GATEWAY_HTTP_RATE_LIMIT = false)
* http.rate_limit_max_requests int Bucket size / requests per window. (GATEWAY_HTTP_RATE_LIMIT_MAX = 10)
* http.rate_limit_per_seconds int Refill window length, seconds. (GATEWAY_HTTP_RATE_LIMIT_PER = 1)
*
* commands.reconcile bool Register the gateway:reconcile:{X} commands. (GATEWAY_RECONCILE_COMMANDS = true)
*
* events.enabled bool Wrap drivers to emit payment domain events. (GATEWAY_EVENTS = true)
* events.log bool Attach the redaction-safe audit-logging listener. (GATEWAY_EVENTS_LOG = false)
*
* logging.operations bool Wrap drivers in a LoggingGateway (per-call logs). (GATEWAY_LOG_OPERATIONS = false)
* logging.channel string Log channel; null → dedicated daily hyprpay log. (GATEWAY_LOG_CHANNEL)
* logging.days int Retention for the daily hyprpay log. (GATEWAY_LOG_DAYS = 14)
* logging.level string Minimum level for the hyprpay channel. (GATEWAY_LOG_LEVEL = debug)
*
* gateways.{key} array Per-gateway credentials — merchant id, secret, host, locale, currency.
*/
Cross-cutting
AI docs
docs/guides/ai/ — machine-consumable SDK reference
A 100%-coverage, machine-consumable reference for AI assistants lives under
docs/guides/ai/: every one of the package's 186 classes, plus the full operation
contract, request/result DTOs, value objects, enums, exceptions, events, ports, all eight
gateways, and the config surface. Load it as context when an AI is helping you use the SDK —
class-index.md lists every class so nothing is left undocumented.
Start at the AI docs index or the complete class index.
3-D Secure
Enroll payer auth
enrollPayerAuth(PayerAuthEnrollRequest): PayerAuthResult
Starts the 3-D Secure challenge for a transient token before you charge it.
$auth = $gateway->enrollPayerAuth(new PayerAuthEnrollRequest(/* … */));
// $auth->status drives the challenge on the front end
3-D Secure
Validate payer auth
validatePayerAuth(ValidatePayerAuthRequest): PayerAuthResult
Confirms the challenge result once the payer completes it — check it before charging.
$result = $gateway->validatePayerAuth(new ValidatePayerAuthRequest(/* … */));
MPGS: pass 3-D Secure browser device data via a BrowserDeviceData on
ValidatePayerAuthRequest.device — the user agent, browserDetails
(screen size, colour depth, language, time zone, challenge window size), and client IP. MPGS
forwards it as device on the AUTHENTICATE_PAYER call so the issuer can
risk-assess and grant a frictionless (no-challenge) authentication more often.
$result = $gateway->validatePayerAuth(new ValidatePayerAuthRequest(
authenticationTransactionId: $authTxnId,
money: Money::minor(10000, 'USD'),
device: new BrowserDeviceData(
ipAddress: $request->ip(),
userAgent: $request->userAgent(),
colorDepth: 24, screenHeight: 640, screenWidth: 480,
language: 'en-US', timeZone: 273, javaScriptEnabled: true,
challengeWindowSize: 'FULL_SCREEN',
),
));
Stored credentials
Vault instrument
vaultInstrument(TokenizeInstrumentRequest): VaultedInstrument
Tokenizes a card and returns a VaultedInstrument you keep for later charges. Charge it
later with chargeStoredCredential.
$vaulted = $gateway->vaultInstrument(new TokenizeInstrumentRequest(/* … */));
// keep $vaulted->paymentInstrumentId
$vaulted = $gateway->vaultInstrument(new TokenizeInstrumentRequest(
cardNumber: '4111111111111111',
expirationMonth: '02',
expirationYear: '2027',
));
// keep $vaulted->paymentInstrumentId (PayPal vault id) and $vaulted->customerId
$vaulted = $gateway->vaultInstrument(new TokenizeInstrumentRequest(
cardNumber: '5123450000000008',
expirationMonth: '05',
expirationYear: '2027',
));
// keep $vaulted->paymentInstrumentId (the MPGS token)
$vaulted = $gateway->vaultInstrument(new TokenizeInstrumentRequest(
cardNumber: '4111111111111111',
expirationMonth: '05',
expirationYear: '2027',
billTo: new BillingAddress( // PayLink requires the cardholder name + country/address/city
firstName: 'Jane', lastName: 'Roe',
country: 'SA', address1: '1 Main St', locality: 'Riyadh',
),
));
// keep $vaulted->paymentInstrumentId (the PayLink card token);
// revoke it later with $gateway->deleteToken($token)
$vaulted = $gateway->vaultInstrument(new TokenizeInstrumentRequest(
transientToken: $opaqueDataValue, // Accept.js nonce — PAN-free (or pass cardNumber/expirationMonth/expirationYear)
));
// CIM profile ids: $vaulted->customerId (customerProfileId) + $vaulted->paymentInstrumentId (paymentProfileId)
Stored credentials
Charge stored credential
chargeStoredCredential(StoredCredentialChargeRequest): PaymentResult
Charges a saved token for merchant- or customer-initiated (MIT/CIT) transactions. The driver stamps the right network stored-credential metadata for the initiator and settles the charge.
$result = $gateway->chargeStoredCredential(new StoredCredentialChargeRequest(
paymentInstrumentId: $vaulted->paymentInstrumentId,
money: Money::minor(9900, 'EGP'),
initiator: CredentialInitiator::Merchant, // MIT
));
$result = $gateway->chargeStoredCredential(new StoredCredentialChargeRequest(
paymentInstrumentId: $savedToken, // token from a tokenise checkout
money: Money::minor(50000, 'SAR'),
initiator: CredentialInitiator::Merchant, // recurring; Customer → ecom
));
// create the token by passing new PaytabsCheckoutOptions(tokenise: 2) on any checkout
$result = $gateway->chargeStoredCredential(new StoredCredentialChargeRequest(
paymentInstrumentId: $vaulted->paymentInstrumentId, // the PayPal vault id
money: Money::minor(50000, 'USD'),
initiator: CredentialInitiator::Merchant, // MIT → RECURRING; Customer → ONE_TIME
));
// vault the card first with vaultInstrument() to get $vaulted->paymentInstrumentId
$result = $gateway->chargeStoredCredential(new StoredCredentialChargeRequest(
paymentInstrumentId: $vaulted->paymentInstrumentId, // the MPGS token
money: Money::minor(10000, 'USD'),
initiator: CredentialInitiator::Merchant, // MIT → adds the stored-credential agreement
orderReference: 'ORDER-129',
));
$result = $gateway->chargeStoredCredential(new StoredCredentialChargeRequest(
paymentInstrumentId: $vaulted->paymentInstrumentId, // the PayLink card token
money: Money::minor(10000, 'USD'),
initiator: CredentialInitiator::Merchant, // MIT rebill; Customer → CIT
orderReference: 'ORDER-9',
));
// billing is reused from tokenize time — no cardholder/address is resent
$result = $gateway->chargeStoredCredential(new StoredCredentialChargeRequest(
paymentInstrumentId: $vaulted->paymentInstrumentId, // CIM customerPaymentProfileId
customerId: $vaulted->customerId, // CIM customerProfileId — required
money: Money::minor(9900, 'USD'),
initiator: CredentialInitiator::Merchant, // MIT → isSubsequentAuth; Customer → isStoredCredentials (CIT)
));