CyberSource UC Signing · HMAC HTTP-Signature Checkout returns · jwt (capture context)

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',
));

Payment flow

Reverse authorization

reverseAuthorization(ReversalRequest): PaymentResult

Releases an auth hold without capturing. A partial money releases only part of the hold. Use it when an order is abandoned after authorization.

$result = $gateway->reverseAuthorization(new ReversalRequest(
    transactionId: '7040000000000000001',
    money: Money::minor(15000, 'EGP'),
    idempotencyKey: 'reverse:auth-123',
));

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)
));
What's new