| Hosts | Arch, Endpoints: Production https://api.scalapay.com, Sandbox https://integration.api.scalapay.com ("supports Fake Cards") | Config::LIVE, Config::TEST |
| Authentication | Arch: "Every API endpoint requires your secret API key to be included as a bearer token in the Authorization header", Authorization: Bearer <key>, 401 on failure; separate sandbox and production keys "usually prefixed with sp_" | Api::call(); one sealed key per mode |
| Public test key | API Simulator: "for our online standard, delay and multicapture calls our test API key is qhtfs87hjnc12kkos"; OC plugin: "Staging/Test API key: qhtfs87hjnc12kkos" | Config::PUBLIC_TEST_KEY, used in sandbox mode when no own key is stored (tests use it only against the local mock) |
| Errors | Arch, Error Response Structure: errorCode, errorId, message, httpStatusCode; "rely on the error code and HTTP status code"; 2XX ok, 4XX invalid, 5XX "unexpected error on our side"; Capture/Delay/Void/Refund 400 example api_validationerror with message.errors[{field, messages}]; 409 conflicting_operation_in_progress "Try again later." | Api::errorText(); retryable = no answer, 5XX, 429, 409 conflicting_operation_in_progress (Api::retryable()) |
| Idempotency-Key | Create, Capture, Delay, Void, Refund, Update: header Idempotency-Key, "Idempotency key used for retrying requests. Can be used for retrying failed requests." | 32-hex key stored BEFORE every money call (txn.idem_key, op.idem_key); a retry re-sends the same body with the same key |
POST /v2/orders | Create: required totalAmount, consumer, items, merchant, shipping; answer token, expires, checkoutUrl ("redirects the customer to Scalapay Checkout, where the payment is authorized") | Api::createOrder(), Payload::order() |
totalAmount, amounts | Create, schema amount {amount, currency}, "Monetary value. Example: 187.95." (string) | Money::obj(): decimal string with the ISO 4217 exponent of EUR; internally integers in minor units |
consumer | Create: phoneNumber ("Customer phone number with country code"), givenNames, surname, email, all required | From the order; phone normalised (Payload::phone()); an order without a phone is refused before any call |
shipping / billing | Create: shipping required phoneNumber, countryCode (iso-alpha-2), name, postcode, suburb, line1; billing optional, same fields | Shipping address of the order, else the payment address; billing when the order has one |
items | Create: quantity (integer), price, name, category, sku required; gtin, subcategory, brand, pageUrl, imageUrl optional | Order products: gross unit price (price + tax), first category name (else "General"), SKU (else model, else product id) |
discounts, shippingAmount, taxAmount | Create: optional | Negative order total lines (coupon, voucher, reward) as discounts; shipping and tax totals |
merchant | Create: redirectConfirmUrl, redirectCancelUrl required | Both point to .back (cancel adds &c=1); the return changes nothing by itself, the order is read back |
merchantReference | Create: "Merchant reference for tracking the order in your system"; Hooks data model: "The correlation id from the merchant, typically the order id" | The OpenCart order id |
type | Create: enum online, offline, link | online |
product | Create: enum pay-in-3, pay-in-4, later, pay-now-standalone; "Could be empty" | One checkout option per product the merchant ticks; none ticked = field omitted |
frequency, orderExpiryMilliseconds | Create: frequency "useful for pay later products. Do not include when using pay-in-3, pay-in-4"; expiry "determined by your Scalapay contract" | Not sent (contract defaults) |
GET /v2/payments/{token} | Get: examples carry token, created, status, captureStatus (captured / pending), product, totalAmount, orderDetails, lastEligibilityFeedback (e.g. DECLINED_FRAUD); unknown token 422 invalid_token "Order null not found." | Api::getOrder(); the only source of the state (Payments::sync()) |
| Statuses | Hooks: created, authorized, charged, refunded, expired; data model: "The list of statuses may change in the future, so it is not advised to hardcode the set of values to expect" | Webhook::state(): authorized, charged, refunded, expired are acted on; anything else stays pending (never cancelled) |
GET /v2/payments/references | Refs: query orderToken, merchantOrderReference, merchantProcessorReference; "maximum of 100 matching orders, sorted by creation date (newest first)"; answer [{orderToken, merchantOrderReference, merchantProcessorReference, createdAt}] | A create call without an answer: the token is found by the merchant reference (Payments::adopt()) |
POST /v2/payments/capture | Capture: body token (required), amount, merchantReference; "You can capture an amount up to the total specified during order creation"; answer {token, status, totalAmount, capturedAt}, example status: "APPROVED"; API calls page: capture "is mandatory to correctly finalise the order" | Api::capture(); success only on APPROVED, any other value is read back and only charged counts |
POST /v2/payments/{token}/delay | Delay: authorizationExpiryMilliseconds required, default 432000000; answer {token, status, authorizationExpiryAt} (example AUTHORIZED); Flows: "You can set to delay the capture up to 5 days after the order creation"; "Outstanding authorizations will be automatically VOIDED once the authorizationExpiryMilliseconds time passes" | Delayed modes; setting 1 to 5 days; capture_until from authorizationExpiryAt |
POST /v2/payments/{token}/void | Void: "Void any authorizations for the order"; body merchantReference; answer {token, merchantReference, voidedAt} | Voids delayed authorizations (cancel status or by hand) and authorizations that must not be booked (mismatch, duplicate, cancelled order) |
POST /v2/payments/{token}/refund | Refund: refundAmount required, merchantRefundReference; answer {token, amount, merchantReference, merchantRefundReference, refundToken, refundedAt} | Full and partial refunds; merchantRefundReference = oc<order>-r<op id>; the answered amount is compared with the requested one |
| Webhook URL | Listener authentication setup: "login to the Partner portal and, in the developers section, fill in the field" Webhook Url | Shown on the Connection tab; no registration API exists |
| Webhook body | Hooks data model: totalAmount, status, orderToken, merchantReference; examples (number totalAmount) | Only orderToken is used; the state comes from GET |
| Webhook signature | Messages validation: headers x-scalapay-hmac-v1, x-scalapay-timestamp; raw string {version}:{timestamp}:{JSON payload}; "Secret: Your merchant's API key. Algorithm: SHA-256. Digest: Hexadecimal"; PHP example $version = 'V1'; "convert headers to lowercase" | Webhook::verify() with hash_equals, keys of both modes tried when the token is unknown; headers read with the PHP-FPM fallbacks of Webhook::headers() |
| Webhook retries | Connection issues: success = HTTP 200; on failure "retried with an exponential backoff", "may not be processed by your servers in sequence" | 200 when handled or not ours, 401 bad signature, 400 unreadable, 503 when the read-back failed; order-independent because every hook only triggers a read-back |
| Widget script | Widget installation: <script type="module" src="https://cdn.scalapay.com/widget/scalapay-widget-loader.js?version=V5"> | Widget::SCRIPT |
| Widget tag | Widget configuration: merchant-token (required), amount-selectors or amount, min-amount, max-amount, locale (en, it, pt, de, fr, es), type (product, cart, checkout), environment (integration / production), amount-separator, alignment, dark-mode, logo-color | Widget::html(); amount passed as amount with separator ., or the merchant's selector |
| Widget test token | Widget installation: "During the testing process, the merchant token value has to be filled with 8KDS3SPEL"; environment integration for tests | Config::PUBLIC_TEST_TOKEN in sandbox mode |
| Widget placement | Guide: product page "below the price", cart page "below the cart total", checkout "next to the other payment methods" | Product view event (before <div id="product">), cart list event (below the totals table), checkout panel |
| Test cards | Test: success 5200828282828210, failure 4000000000000341 | docs/installation.md |
| Countries, currency, minimum | OC plugin: "The authorised countries are Austria, Belgium, Finland, France, Germany, Holland, Italy, Portugal and Spain", "The only authorised currency is Euro", "The minimum amount ... is set to 5 EUR. If you would like to modify the maximum amount, contact your Sale referent" | Config::COUNTRIES, Money::CURRENCIES = ['EUR'], default minimum 5.00 |