HyperPay Payment Gateway Integration Cheatsheet
1. Overview
Section titled “1. Overview”What is HyperPay
Section titled “What is HyperPay”- HyperPay is a GCC-focused payment service provider (PSP) that exposes REST payment APIs on top of the OPP/oppwa gateway, using base URLs like
https://eu-test.oppwa.comfor sandbox andhttps://oppwa.comfor production. - Integrations are built around creating a checkout session (
/v1/checkouts) and then executing or verifying the payment using that checkout identifier or aresourcePathreturned from the widget/SDK.
Supported payment methods
Section titled “Supported payment methods”Commonly enabled brands (configured per merchant/entity):
- Card schemes:
VISA,MASTER,AMEX. - Local Saudi methods:
MADA. - Wallets:
STC_PAY,APPLEPAY(via specific entities and brand values). - Brands are specified either in the hosted widget (
data-brands="VISA MASTER MADA STC_PAY APPLEPAY") or at the SDK level, and mapped to specific entity IDs on your backend.
Typical architecture
Section titled “Typical architecture”-
Hosted Checkout / Payment Widgets
- Backend calls
POST /v1/checkoutsto create a checkout session. - Frontend loads
paymentWidgets.jsfromhttps://test.oppwa.com/v1/paymentWidgets.js?checkoutId={id}and renders a hosted card/wallet form with configured brands. - Customer completes payment on HyperPay-hosted UI; browser is redirected to your
shopperResultUrlwithidandresourcePathparameters for finalization.
- Backend calls
-
Server-to-Server (S2S) / Direct API
- Your backend collects card details (or uses stored registrations) and sends them directly to HyperPay via REST endpoints such as
/v1/paymentsor/v1/registrations/{id}/payments. - Requires full PCI-DSS scope (SAQ D) and is typically avoided unless you are already a card-data environment.
- Your backend collects card details (or uses stored registrations) and sends them directly to HyperPay via REST endpoints such as
-
Mobile SDK / Native Apps
- Your backend still creates the checkout via
POST /v1/checkouts. - The mobile SDK (Android/iOS/Flutter) takes
checkoutIdand brands and opens a native payment UI; upon completion it returns aresourcePathor success flag and you verify server-side.
- Your backend still creates the checkout via
Key terminology
Section titled “Key terminology”- entityId: Identifier of a merchant entity and payment method configuration (you usually have separate entity IDs for
MADA,VISA/MASTER,APPLEPAY, etc.). - checkoutId (
id): ID returned fromPOST /v1/checkouts, used by widgets/SDKs to render the payment form and later to query status (/v1/checkouts/{id}/payment). - paymentId: Identifier of an executed payment used for capture/refund/reversal APIs (
/v1/payments/{paymentId}). - resourcePath: OPP/HyperPay-relative path returned to your frontend (e.g.
/v1/checkouts/{id}/payment), which your backend later calls withGET {resourcePath}?entityId=...to verify payment. - result.code: Gateway-level result code (e.g.
000.200.100for “successfully created checkout”) indicating the outcome of an operation.
2. Integration Modes
Section titled “2. Integration Modes”| Mode | How it works | PCI impact | Typical use cases |
|---|---|---|---|
| Hosted Payment Page / Payment Widgets | Backend creates checkout; frontend loads paymentWidgets.js and renders iframe/embedded form for brands | Minimal (SAQ A) as card data never touches your servers | Web checkouts, marketplaces, SaaS portals |
| Server-to-Server (Direct API) | Backend collects card details and calls /v1/payments (or registrations) directly | High (SAQ D) – you handle PAN/CVV | Legacy systems, when you already are PCI-DSS certified to store/process card data |
| Mobile SDK | Backend creates checkout, mobile SDK renders native UI, then calls your server with resourcePath to finalize | Mobile app is partly in PCI scope but server can remain SAQ A–like model | Native mobile apps needing better UX and wallets (Apple Pay, Google Pay, mada, STC Pay) |
Pros / Cons & when to use
Section titled “Pros / Cons & when to use”Hosted Payment Page / Widgets
- ✅ Card and wallet data never pass through your backend; lowest PCI burden.
- ✅ Quickest integration path; HyperPay handles 3DS flows and scheme UX.
- ✅ Easy to support multiple brands (MADA, STC Pay, Apple Pay) via
data-brands. - ⚠️ Less control over UI/UX; customizations limited to themes/options.
- Use for most web applications, especially when you do not want PCI scope.
Server-to-Server (Direct API)
- ✅ Full control over UX and routing (e.g., custom 3DS flows, card-on-file logic).
- ✅ Easier to orchestrate multi-gateway routing in a central payment service.
- ⚠️ Requires secure collection and transmission of card data; PCI-DSS SAQ D applies and you must avoid storing CVV beyond authorization.
- ⚠️ Increased liability and audit surface.
- Use only if you are already PCI-certified and have a strong reason to own card entry.
Mobile SDK
- ✅ Best native UX; SDK handles wallet integrations and 3DS per-platform.
- ✅ SDK never exposes raw card data to the app in most flows; it talks directly to HyperPay.
- ⚠️ Requires integrating platform-specific SDKs and handling custom URL schemes (
shopperResultUrl). - Use when you have native apps and want first-class Apple Pay / mada / STC Pay UX.
3. Authentication & Configuration
Section titled “3. Authentication & Configuration”Base URLs & environments
Section titled “Base URLs & environments”- Sandbox base URL:
https://eu-test.oppwa.comwithPOST /v1/checkoutsand related endpoints. - Live base URL:
https://oppwa.com(or a region-specific equivalent) with identical path structure. - Payment widget script URLs map 1:1:
https://test.oppwa.com/v1/paymentWidgets.jsvshttps://oppwa.com/v1/paymentWidgets.js.
Auth models
Section titled “Auth models”Two patterns are visible in production code:
-
Bearer token (recommended)
Authorization: Bearer {REST_API_TOKEN}in HTTP headers.- Body contains
entityId,amount,currency,paymentType, and optional customer/billing fields.
-
UserId/password (legacy)
- Some older examples pass
authentication.userId,authentication.password, andauthentication.entityIdas body/query parameters to/v1/checkoutsand status endpoints. - Prefer migrating to Bearer token where available; confirm with HyperPay support.
- Some older examples pass
Core configuration parameters
Section titled “Core configuration parameters”HYPERPAY_BASE_URL–https://eu-test.oppwa.com(test) vshttps://oppwa.com(live).HYPERPAY_TOKEN– REST API token used as Bearer token.- Entity IDs by brand / use-case:
HYPERPAY_CREDIT_ID(VISA/MASTER),HYPERPAY_MADA_ID,HYPERPAY_APPLE_ID.
- Currency (e.g.,
SAR) and mode flags (testvslive) held in config.
Headers & security requirements
Section titled “Headers & security requirements”- Use
Authorization: Bearer {token}for all backend-to-HyperPay calls where supported. - Use
Content-Type: application/x-www-form-urlencodedor JSON depending on the specific endpoint and documentation; most public examples use URL-encoded form data for/v1/checkouts. - Ensure TLS 1.2+ support in your JVM and outbound proxies; many gateways reject TLS 1.0/1.1.
Environment separation
Section titled “Environment separation”- Keep separate config for
testandlive:- Tokens, entity IDs, base URLs, webhook URLs.
4. Payment Flow (Step-by-step)
Section titled “4. Payment Flow (Step-by-step)”High-level steps (HPP / Widgets / SDK)
Section titled “High-level steps (HPP / Widgets / SDK)”- Create checkout on merchant backend:
POST {baseUrl}/v1/checkoutswithentityId,amount,currency,paymentType, etc. - Return
checkoutIdto frontend/mobile client. - Present payment UI:
- Web: load
paymentWidgets.jsand render<form class="paymentWidgets" data-brands="...">. - Mobile: call SDK’s
checkoutReadyUI/paywithcheckoutIdand brands.
- Web: load
- Customer completes payment and is redirected to your
shopperResultUrlor in-app callback. - Backend verifies status by calling
GET {baseUrl}{resourcePath}?entityId=...orGET /v1/checkouts/{checkoutId}/payment?entityId=...using same entityId and credentials. - Backend updates order status and returns success/failure to UI.
Sequence diagram – Hosted Payment Widgets
Section titled “Sequence diagram – Hosted Payment Widgets”User Browser -> Merchant FE: Click "Pay"Merchant FE -> Merchant BE: POST /orders/{orderId}/payMerchant BE -> HyperPay: POST {baseUrl}/v1/checkouts (Bearer token, entityId, amount, currency, paymentType=DB)HyperPay -> Merchant BE: 200 { id: checkoutId, result.code: "000.200.100" }Merchant BE -> Merchant FE: checkoutIdMerchant FE -> Browser: Render checkout page with paymentWidgets.js?checkoutId={checkoutId}User Browser -> HyperPay Widget: Enter card / wallet details, submitHyperPay Widget -> HyperPay: Process payment (3DS, routing, etc.)HyperPay -> User Browser: Redirect to shopperResultUrl?id={checkoutId}&resourcePath={path}User Browser -> Merchant FE: GET /payment/result?{query}Merchant FE -> Merchant BE: GET /api/payments/verify?resourcePath=...Merchant BE -> HyperPay: GET {baseUrl}{resourcePath}?entityId={entityId}HyperPay -> Merchant BE: 200 { payment result }Merchant BE -> Merchant FE: Payment result (success/pending/failed)Merchant FE -> User Browser: Show confirmation pageSequence diagram – Mobile SDK
Section titled “Sequence diagram – Mobile SDK”Mobile App -> Merchant BE: POST /api/hyperpay/checkouts (amount, currency, brand)Merchant BE -> HyperPay: POST {baseUrl}/v1/checkoutsHyperPay -> Merchant BE: 200 { id: checkoutId }Merchant BE -> Mobile App: checkoutIdMobile App -> HyperPay SDK: startPayment(checkoutId, brands)HyperPay SDK <-> HyperPay: Card/wallet UI + processingHyperPay SDK -> Mobile App: onComplete(isSuccess, resourcePath)Mobile App -> Merchant BE: POST /api/hyperpay/verify { resourcePath, orderId }Merchant BE -> HyperPay: GET {baseUrl}{resourcePath}?entityId=...HyperPay -> Merchant BE: 200 { payment result }Merchant BE -> Mobile App: Payment result5. Java Implementation (Spring Boot)
Section titled “5. Java Implementation (Spring Boot)”Design principles
Section titled “Design principles”- Hide HyperPay specifics behind a
HyperpayClientandPaymentServiceinterface; expose domain-centric methods likeauthorizePayment,capturePayment,getPaymentStatus. - Keep environment, entityId, and tokens in configuration properties and do not scatter them in business code.
- Standardize error mapping (e.g., map
result.codeprefixes into internal enums).
Configuration (application.yml)
Section titled “Configuration (application.yml)”hyperpay: base-url: https://eu-test.oppwa.com token: ${HYPERPAY_TOKEN} mode: test # or live currency: SAR entities: visa-master: ${HYPERPAY_CREDIT_ID} mada: ${HYPERPAY_MADA_ID} applepay: ${HYPERPAY_APPLE_ID} connect: read-timeout-ms: 5000 connect-timeout-ms: 2000WebClient configuration
Section titled “WebClient configuration”@Configurationpublic class HyperpayClientConfig {
@Bean public WebClient hyperpayWebClient(@Value("${hyperpay.base-url}") String baseUrl, @Value("${hyperpay.token}") String token) { return WebClient.builder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .clientConnector(new ReactorClientHttpConnector( HttpClient.create() .responseTimeout(Duration.ofSeconds(5)) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000))) .build(); }}public record CreateCheckoutRequest( BigDecimal amount, String currency, String paymentType, // DB, PA, etc. String entityId, String merchantTransactionId, String customerEmail) {}
public record CreateCheckoutResponse( String id, // checkoutId String resultCode, String resultDescription) {}
public record PaymentStatusResponse( String id, String resultCode, String resultDescription, String paymentBrand, String descriptor) {}HyperpayClient interface
Section titled “HyperpayClient interface”public interface HyperpayClient { Mono<CreateCheckoutResponse> createCheckout(CreateCheckoutRequest request); Mono<PaymentStatusResponse> getPaymentStatusByCheckoutId(String checkoutId, String entityId); Mono<PaymentStatusResponse> getPaymentStatusByResourcePath(String resourcePath, String entityId);}HyperpayClient implementation
Section titled “HyperpayClient implementation”@Servicepublic class HyperpayClientImpl implements HyperpayClient {
private final WebClient client;
public HyperpayClientImpl(WebClient hyperpayWebClient) { this.client = hyperpayWebClient; }
@Override public Mono<CreateCheckoutResponse> createCheckout(CreateCheckoutRequest req) { MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("entityId", req.entityId()); form.add("amount", req.amount().setScale(2, RoundingMode.HALF_UP).toPlainString()); form.add("currency", req.currency()); form.add("paymentType", req.paymentType()); form.add("merchantTransactionId", req.merchantTransactionId()); if (req.customerEmail() != null) { form.add("customer.email", req.customerEmail()); }
return client.post() .uri("/v1/checkouts") .body(BodyInserters.fromFormData(form)) .retrieve() .onStatus(HttpStatusCode::isError, this::mapError) .bodyToMono(JsonNode.class) .map(json -> new CreateCheckoutResponse( json.path("id").asText(), json.path("result").path("code").asText(), json.path("result").path("description").asText() )); }
@Override public Mono<PaymentStatusResponse> getPaymentStatusByCheckoutId(String checkoutId, String entityId) { String uri = String.format("/v1/checkouts/%s/payment?entityId=%s", checkoutId, entityId); return doGetStatus(uri); }
@Override public Mono<PaymentStatusResponse> getPaymentStatusByResourcePath(String resourcePath, String entityId) { // resourcePath already starts with /v1/... String uri = String.format("%s?entityId=%s", resourcePath, entityId); return doGetStatus(uri); }
private Mono<PaymentStatusResponse> doGetStatus(String uri) { return client.get() .uri(uri) .retrieve() .onStatus(HttpStatusCode::isError, this::mapError) .bodyToMono(JsonNode.class) .map(json -> new PaymentStatusResponse( json.path("id").asText(), json.path("result").path("code").asText(), json.path("result").path("description").asText(), json.path("paymentBrand").asText(), json.path("descriptor").asText() )); }
private Mono<? extends Throwable> mapError(ClientResponse response) { return response.bodyToMono(String.class) .defaultIfEmpty("") .flatMap(body -> Mono.error( new HyperpayApiException( "HyperPay API error: status=%s body=%s".formatted(response.statusCode(), body)) )); }}
public class HyperpayApiException extends RuntimeException { public HyperpayApiException(String message) { super(message); }}Service-layer usage
Section titled “Service-layer usage”@Servicepublic class PaymentService {
private final HyperpayClient hyperpayClient; private final HyperpayProperties props;
public PaymentService(HyperpayClient hyperpayClient, HyperpayProperties props) { this.hyperpayClient = hyperpayClient; this.props = props; }
public Mono<String> createCheckoutForOrder(Order order, PaymentBrand brand) { String entityId = props.entityIdForBrand(brand); CreateCheckoutRequest req = new CreateCheckoutRequest( order.getAmount(), props.currency(), "DB", // direct debit (sale) entityId, order.getExternalReference(), order.getCustomer().getEmail() );
return hyperpayClient.createCheckout(req) .flatMap(resp -> { if (!resp.resultCode().startsWith("000.200.")) { return Mono.error(new IllegalStateException("Failed to create checkout: " + resp.resultDescription())); } return Mono.just(resp.id()); }); }
public Mono<PaymentStatus> finalizePayment(String resourcePath, PaymentBrand brand) { String entityId = props.entityIdForBrand(brand); return hyperpayClient.getPaymentStatusByResourcePath(resourcePath, entityId) .map(this::mapToDomainStatus); }
private PaymentStatus mapToDomainStatus(PaymentStatusResponse resp) { String code = resp.resultCode(); if (code.startsWith("000.000") || code.startsWith("000.100")) { return PaymentStatus.SUCCESS; } else if (code.startsWith("000.200")) { return PaymentStatus.PENDING; } else { return PaymentStatus.FAILED; } }}Exception handling & retries
Section titled “Exception handling & retries”- Wrap all HyperPay calls in domain-specific exceptions (
HyperpayApiException,PaymentFailedException). - Use
retryWhenwith backoff for transient HTTP 5xx/timeouts; avoid retrying 4xx/validation errors. - Always implement idempotency at the order/payment layer so that retries do not double-charge.
6. Webhooks (Asynchronous Notifications)
Section titled “6. Webhooks (Asynchronous Notifications)”HyperPay/OPP-based gateways support asynchronous notifications either via browser redirects with resourcePath (already shown) and, depending on account configuration, via dedicated webhooks / notification URLs.
Setting up notification URL
Section titled “Setting up notification URL”- Configure a global notification URL in the HyperPay/OPP merchant dashboard if available.
- Use a dedicated HTTPS endpoint, e.g.
/api/webhooks/hyperpay, with:- POST only.
- Strict IP allow-list or HMAC-style shared secret.
- Short timeouts and quick 200 OK response.
Spring Boot webhook controller (resourcePath model)
Section titled “Spring Boot webhook controller (resourcePath model)”@RestController@RequestMapping("/api/webhooks/hyperpay")public class HyperpayWebhookController {
private final PaymentService paymentService;
public HyperpayWebhookController(PaymentService paymentService) { this.paymentService = paymentService; }
// For browser redirect: /payment/result?resourcePath=...&id=... @GetMapping("/result") public Mono<ResponseEntity<Void>> paymentResult(@RequestParam("resourcePath") String resourcePath, @RequestParam("brand") String brand) { PaymentBrand paymentBrand = PaymentBrand.fromExternal(brand); return paymentService.finalizePayment(resourcePath, paymentBrand) .doOnNext(status -> {/* update order, emit events, etc. */}) .thenReturn(ResponseEntity.status(HttpStatus.FOUND) .location(URI.create("/payments/thank-you")) .build()); }
// For pure webhook-style JSON notifications if enabled later @PostMapping public ResponseEntity<Void> webhook(@RequestBody Map<String, Object> payload, @RequestHeader Map<String, String> headers) { // Validate signature / shared secret in headers if configured // Extract resourcePath or payment.id then trigger verification flow return ResponseEntity.ok().build(); }}Idempotency strategies for callbacks
Section titled “Idempotency strategies for callbacks”- Store a unique
checkoutIdorpaymentIdper order and enforceUNIQUEconstraint to prevent double-posting. - Maintain a
payment_eventstable keyed by HyperPayid/ndc/checkoutIdso that processing a notification twice becomes a no-op. - Use at-least-once semantics; design logic to be idempotent so that both webhook and frontend-triggered verification can run safely.
Signature / authenticity validation
Section titled “Signature / authenticity validation”- Pattern across payment gateways:
- Shared secret in HTTP header (e.g.,
X-Notification-Secret) or an HMAC over the payload.
- Shared secret in HTTP header (e.g.,
- When HyperPay provides such a secret:
- Persist it in secure config.
- On each webhook, recompute HMAC using payload and compare in constant time.
- Reject any request without valid auth.
7. Payment Status & Error Handling
Section titled “7. Payment Status & Error Handling”Result codes
Section titled “Result codes”OPP-style result codes follow a AAA.BBB.CCC pattern with ranges indicating success, pending, or error.
Typical examples:
000.200.100– “successfully created checkout” (checkout creation OK).000.000.xxx/000.100.xxx– successful payments / successful request.000.200.xxx– transaction pending / initialized / QR scanned; treat as PENDING.800.xxx.xxx– various bank/issuer declines (e.g., CVV wrong, insufficient funds, etc.).
Mapping to internal domain
Section titled “Mapping to internal domain”public enum PaymentStatus { SUCCESS, PENDING, FAILED; }
public PaymentStatus mapResultCode(String code) { if (code == null) return PaymentStatus.FAILED; if (code.startsWith("000.000") || code.startsWith("000.100")) { return PaymentStatus.SUCCESS; } else if (code.startsWith("000.200")) { return PaymentStatus.PENDING; } else { return PaymentStatus.FAILED; }}Success vs pending vs failure flows
Section titled “Success vs pending vs failure flows”- SUCCESS: Mark order as paid, persist gateway metadata (
paymentBrand,descriptor, masked PAN if provided), emit domain events. - PENDING: Show a pending screen; rely on webhook/cron retries to poll status until it reaches a terminal state (
SUCCESSorFAILED). - FAILED: Surface a retry option; do not automatically create new checkouts using the same
checkoutId.
Retry strategies
Section titled “Retry strategies”-
For checkout creation (
POST /v1/checkouts):- Retry on timeouts and 5xx with exponential backoff.
- Use a client-side idempotency key (e.g., order ID) to ensure only a single order-level payment is in-flight.
-
For payment status (
GET ...payment):- Safe to retry multiple times; idempotent by design.
-
For user-facing retries (user clicks “Pay” again):
- First check whether there is already a successful payment for that order.
- If previous state is PENDING, consider polling before creating a new checkout.
8. Security & Compliance
Section titled “8. Security & Compliance”PCI-DSS considerations
Section titled “PCI-DSS considerations”- Using hosted widgets/SDKs ensures card data goes directly to HyperPay; your backend only sees tokens (checkoutId, paymentId, resourcePath), keeping you closer to SAQ A instead of SAQ D.
- Direct S2S integrations involving PAN/CVV place your environment in scope for PCI DSS and forbid storing CVV after authorization; CVV should exist only transiently in RAM and be discarded immediately after processing.
- Any stored PAN must be encrypted at rest and protected with strong key management and multiple authenticators.
Tokenization & registrations
Section titled “Tokenization & registrations”- HyperPay supports
registrationsendpoints (/v1/registrationsand/v1/registrations/{id}/payments) to store cards as tokens and charge them later without handling PAN each time. - Your system should store only opaque HyperPay registration IDs and never raw PAN or CVV.
Avoiding sensitive data storage
Section titled “Avoiding sensitive data storage”- Never log PAN, CVV, or full cardholder names; ensure log masking at both application and infrastructure layers.
- Ensure request dumps and APM traces scrub sensitive fields.
HTTPS and secure headers
Section titled “HTTPS and secure headers”- Enforce TLS 1.2+ to HyperPay endpoints.
- For your own payment pages and webhooks, enable:
Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options(or CSP frame-ancestors when embedding widgets), andContent-Security-Policytuned to allow only HyperPay widget origins.
9. Production Best Practices
Section titled “9. Production Best Practices”Logging & observability
Section titled “Logging & observability”- Log at
INFOlevel:checkoutId, entityId alias, orderId, mappedPaymentStatus, and high-level result description.
- Log at
DEBUGlevel:- Full HyperPay response JSON with sensitive fields redacted.
- Emit metrics:
- Checkout creation latency and error rates.
- Payment success/failure rates by brand.
- Webhook processing latency and error rates.
Idempotency keys
Section titled “Idempotency keys”- Introduce an
Idempotency-Keyheader or internal idempotency token for operations like “create payment for order X” so that retries do not double-charge. - Persist idempotency keys with the payment record and enforce uniqueness at the DB level.
Timeouts, retries, circuit breakers
Section titled “Timeouts, retries, circuit breakers”-
Outbound HTTP:
- Connect timeout ≈ 2–3 seconds, read timeout ≈ 5–10 seconds.
- Limit max total retries to a small number (2–3) for idempotent operations only.
-
Use Resilience4j for:
- Circuit breaker per HyperPay host (open on repeated 5xx/timeouts).
- Bulkhead/isolation to prevent payment spikes from exhausting HTTP connection pool.
Example Resilience4j config:
resilience4j.circuitbreaker: instances: hyperpay: sliding-window-type: COUNT_BASED sliding-window-size: 20 failure-rate-threshold: 50 wait-duration-in-open-state: 30s permitted-number-of-calls-in-half-open-state: 5
resilience4j.retry: instances: hyperpay: max-attempts: 3 wait-duration: 500ms retry-exceptions: - java.io.IOException - org.springframework.web.reactive.function.client.WebClientRequestExceptionHandling partial failures
Section titled “Handling partial failures”- If checkout creation succeeds but persisting the order fails, reconcile using
merchantTransactionIdor checkoutId search in HyperPay backoffice. - If HyperPay confirms payment success but your DB update fails, persist a compensating event or use an outbox pattern; never re-charge the customer on recovery.
10. Testing Strategy
Section titled “10. Testing Strategy”Sandbox usage
Section titled “Sandbox usage”- Use test environment (
https://eu-test.oppwa.com) with dedicated test entities and sample card numbers as provided by HyperPay or integration libraries. - Some community packages list test cards (e.g., Visa 4111111111111111 with CVV 123 and future expiry) used in sandbox.
Mocking HyperPay APIs
Section titled “Mocking HyperPay APIs”- For unit tests, mock
HyperpayClientinterface. - For integration tests, run a lightweight HTTP server (WireMock/Testcontainers) on
localhostthat mimics/v1/checkoutsand/v1/checkouts/{id}/paymentbehavior. - Record real sandbox responses and replay them during tests.
Integration & E2E tests
Section titled “Integration & E2E tests”- Test matrix:
- Brands: VISA/MASTER, MADA, STC Pay, Apple Pay.
- Scenarios: success, issuer decline (
800.xxx.xxx), pending, 3DS challenge, user cancel.
- On mobile, verify that
checkoutIdis obtained from your server and not directly from HyperPay, and thatresourcePathis always passed back to the backend for verification.
Edge cases
Section titled “Edge cases”- Network timeouts when creating checkout or checking status.
- Duplicate notifications (same
resourcePathoridprocessed multiple times). - Browser closed before redirect; rely on webhook or scheduled polling.
- User double-clicks “Pay”; ensure idempotency by order/checkout link.
11. Common Pitfalls
Section titled “11. Common Pitfalls”- ⚠️ Incorrect entityId usage: Using a VISA/MASTER entityId for MADA or Apple Pay will lead to brand or acquirer failures; maintain a clear mapping of brand → entityId.
- ⚠️ Mismatched environments: Using test
checkoutIdwith live widget script (or vice versa) causes invalid session errors; make sure base URLs and scripts are consistent (test.oppwa.comvsoppwa.com). - ⚠️ Async vs sync confusion: Relying solely on frontend success callbacks without server-side verification of
resourcePathcan cause false positives; always verify on the backend. - ⚠️ Payment status misinterpretation: Treating
000.200.xxxas success instead of pending can cause premature order confirmation; map ranges correctly. - ⚠️ Storing CVV or PAN in logs: Violates PCI DSS and increases breach impact; scrub sensitive data aggressively.
12. Quick Reference Section
Section titled “12. Quick Reference Section”Important endpoints (HyperPay / OPP style)
Section titled “Important endpoints (HyperPay / OPP style)”| Purpose | Method & Path | Notes |
|---|---|---|
| Create checkout | POST /v1/checkouts | Returns id (checkoutId); result.code=000.200.100 for success. |
| Payment status by checkout | GET /v1/checkouts/{checkoutId}/payment?entityId=... | Returns payment result for a given checkout. |
| Payment status by resourcePath | GET {resourcePath}?entityId=... | resourcePath comes from redirect/SDK (e.g. /v1/checkouts/{id}/payment). |
| Payment widgets script (test) | https://test.oppwa.com/v1/paymentWidgets.js?checkoutId={id} | Used to render hosted form. |
| Payment widgets script (live) | https://oppwa.com/v1/paymentWidgets.js?checkoutId={id} | Production equivalent. |
| Card registrations | POST /v1/registrations, POST /v1/registrations/{id}/payments | Create token and charge stored card. |
Required parameters for POST /v1/checkouts (typical set)
Section titled “Required parameters for POST /v1/checkouts (typical set)”Common fields seen in production examples (actual required list depends on your merchant configuration):
entityId– entity ID for selected brand (MADA vs VISA/MASTER etc.).amount– decimal amount (e.g.,100.00).currency– ISO currency, e.g.SAR.paymentType–DB(debit/sale) orPA(preauthorization).merchantTransactionId– your idempotent transaction reference.- Optional customer/billing fields:
customer.email,customer.givenName,billing.city,billing.country, etc.
Sample cURL – create checkout (Bearer token)
Section titled “Sample cURL – create checkout (Bearer token)”TOKEN="${HYPERPAY_TOKEN}"BASE_URL="https://eu-test.oppwa.com"ENTITY_ID="${HYPERPAY_CREDIT_ID}"
curl -X POST "${BASE_URL}/v1/checkouts" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "entityId=${ENTITY_ID}" \ -d "amount=100.00" \ -d "currency=SAR" \ -d "paymentType=DB" \ -d "merchantTransactionId=ORDER-12345"Example response shape:
{ "buildNumber": "", "id": "8ac7a4c780affe890180b1df0f4b0e29", "result": { "code": "000.200.100", "description": "successfully created checkout" }, "ndc": "8ac7a4c780affe890180b1df0f4b0e29", "script_url": "https://test.oppwa.com/v1/paymentWidgets.js?checkoutId=8ac7a4c780affe890180b1df0f4b0e29"}Sample cURL – get payment status by checkoutId
Section titled “Sample cURL – get payment status by checkoutId”TOKEN="${HYPERPAY_TOKEN}"BASE_URL="https://eu-test.oppwa.com"ENTITY_ID="${HYPERPAY_CREDIT_ID}"CHECKOUT_ID="8ac7a4c780affe890180b1df0f4b0e29"
curl -X GET "${BASE_URL}/v1/checkouts/${CHECKOUT_ID}/payment?entityId=${ENTITY_ID}" \ -H "Authorization: Bearer ${TOKEN}"Status code / result code cheat table
Section titled “Status code / result code cheat table”| Category | HTTP status | Example result.code | Meaning / handling |
|---|---|---|---|
| Checkout created | 200 | 000.200.100 | Checkout created; proceed to widget/SDK. |
| Payment success | 200 | 000.000.xxx, 000.100.1xx | Payment successful; mark order as paid. |
| Payment pending | 200 | 000.200.xxx | Transaction pending/initialized; keep polling or wait for webhook. |
| Bank/issuer decline | 200 | 800.xxx.xxx | Declined; show error and allow retry with different method. |
| Client error | 4xx | A000x (in some APIs) | Signature/parameter/timestamp errors; fix request. |
| Server error | 5xx | N/A | Gateway/internal error; retry with idempotency, circuit breaker. |
This cheatsheet intentionally focuses on practical integration details visible across real-world HyperPay/OPP implementations and should be complemented with the latest official merchant documentation and credentials from HyperPay support for production use.