Skip to content

Developer notes

Technical reference for developers who customize PayU for CS-Cart and Multi-Vendor.

Item Value
Add-on id kv_payu
Function prefix fn_kv_payu_
Language variables kv_payu.*
CSS scope .kv-payu
PHP namespace Tygh\Addons\KvPayu
Preset payment (generated by kv new --kind integration)
Class Role
Client\BaseClient HTTP (cURL, TLS verification, timeouts), JSON, OAuth2 client-credentials with an encrypted token cache, error mapping, masked logging
Client\Api PayU REST API 2.1: OAuth, create order, retrieve order, refunds, payment methods (see “Official API reference” below)
Client Client per company, the connection test
Accounts, Secret Credentials (test and live) sealed with AES-256-GCM; one account per store owner or per vendor
Config, Access Settings, owner mode, who may see and manage what
Queue, Jobs, JobException Job queue: back-off 5 min / 15 min / 1 h / 3 h / 12 h, per-job isolation, overlap lock; handlers in Jobs::handle(), recurring work in Jobs::schedule()
Log Event log with masking, filters, CSV, retention
Webhook OpenPayu-Signature check and event ids (order id + status); the controller kv_payu.webhook?c=<account> stores the event as a job
Table Purpose
?:kv_payu_accounts Credentials per company (secrets sealed), last connection check, token cache
?:kv_payu_log Log
?:kv_payu_jobs Queue
?:kv_payu_transactions, ?:kv_payu_refunds Payments and refunds

PHP: change_order_status_post (automatic refund when an order reaches the chosen status), prepare_checkout_payment_methods (hides the method while it cannot start payments). Template hooks: orders:tabs_extra (payment tab), addons:manage / addons:tabs_content (licence), index:styles.

Official API reference (every call and field, with its source)

Section titled “Official API reference (every call and field, with its source)”

Source of truth: PayU Europe developer documentation, https://developers.payu.com/europe/ (API reference https://developers.payu.com/europe/api/, “Payment lifecycle” /europe/docs/payment-flows/lifecycle/, “Sandbox” /europe/docs/testing/sandbox/, “References” /europe/docs/get-started/integration-overview/references/, “Accept payment” /europe/docs/get-started/integration-overview/accept-payment/). The notification signature algorithm list is checked against the official PHP SDK, https://github.com/PayU-EMEA/openpayu_php (lib/OpenPayU/Util.php). Checked 2026-11-07.

What Value Source
Hosts production https://secure.payu.com, sandbox https://secure.snd.payu.com API reference, Servers
OAuth POST /pl/standard/user/oauth/authorize, form fields grant_type=client_credentials, client_id, client_secret; answer access_token, token_type, expires_in (43199 s), grant_type; later calls send Authorization: Bearer <token> API reference; Accept payment step 1
Create order POST /api/v2_1/orders, Content-Type: application/json. Required: customerIp, merchantPosId, description (max 4000), currencyCode (ISO 4217), totalAmount (string, minor units), products[] (name, unitPrice, quantity). Used optional: extOrderId (max 1024, unique per POS), notifyUrl (max 1024), continueUrl, buyer (email, firstName, lastName, language) API reference, Create an Order
Create order answer HTTP 201, or 302 when redirects are not followed (the client never follows them): status.statusCode = SUCCESS, orderId, extOrderId, redirectUri (the PayU payment page) API reference; Accept payment step 2 and 3
customerIp IPv4 dotted decimal or IPv6 as eight groups of four hex digits (the add-on expands the short form) API reference
Amounts integer minor units as a string: 1000 = 10.00 EUR, also for currencies without subunits API reference
Retrieve order GET /api/v2_1/orders/{orderId}; answer orders[] (orderId, extOrderId, merchantPosId, currencyCode, totalAmount, status as a string) and properties[] (PAYMENT_ID) API reference, Retrieve an Order
Order statuses PENDING, WAITING_FOR_CONFIRMATION (auto-receive off), COMPLETED, CANCELED Payment lifecycle
Notification POST to the notifyUrl of the order, body {order{orderId, extOrderId, merchantPosId, totalAmount, currencyCode, status, ...}, localReceiptDateTime, properties[]}; PayU expects HTTP 200, otherwise repeats (up to 20 times: now, 1, 2, 5, 10, 30 min, 1, 2, 3, 6, 9, 12, 15, 18, 21, 24, 36, 48, 60, 72 h); duplicates with the same status can occur Payment lifecycle
Signature header OpenPayu-Signature: sender=checkout;signature=<hex>;algorithm=MD5;content=DOCUMENT; expected = hash(algorithm, raw body + second key), compared in constant time. Accepted algorithms: MD5, SHA-256, SHA-384, SHA-512 (the SDK also reads SHA-1, which the add-on refuses) Payment lifecycle; openpayu_php Util::verifySignature
Refund POST /api/v2_1/orders/{orderId}/refunds, body {"refund": {"description" (required), "amount" (string, minor units; omitted = full), "extRefundId", "currencyCode"}}; answer {orderId, refund{refundId, description, amount, extRefundId, currencyCode, creationDateTime, status: PENDING, statusDatetime}, status{statusCode, statusDesc}}; identical data returns the existing refund (idempotent) API reference, Refunds
Refund read GET /api/v2_1/orders/{orderId}/refunds/{refundId}; refund statuses PENDING, FINALIZED, CANCELED API reference
Payment methods GET /api/v2_1/paymethods (used by “Test connection”) API reference
Error body {"status": {"statusCode", "severity", "code", "codeLiteral", "statusDesc"}}; OAuth errors {"error", "error_description"} API reference
Currencies CHF, CZK, DKK, EUR, GBP, HUF, NOK, PLN, RON, SEK, USD (settled 1:1); cards in any currency, other methods only CZK, EUR, PLN References
TLS TLS 1.2 or newer, SNI required Accept payment, Transmission requirements

Public sandbox POS credentials of the PayU docs (Sandbox page): PLN POS 300746 (second key b6ca15b0d1020e8094d9b5f8d163db54, OAuth client secret 2ee86a66e5d97e3fadc400c9f19b065d), BGN POS 404815, RON POS 491057. OAuth client ID equals the POS ID. Test cards, e.g. 4444333322221111 (12/29, CVV 123) positive authorization. These are public test values of PayU, not secrets. The sandbox is for functional tests only (HTTP 429 on excessive traffic).

Flagged: not verified in the documentation pages we could read

Section titled “Flagged: not verified in the documentation pages we could read”

These points are handled defensively and are covered by the add-on’s read-back, never trusted from a body:

Resolved against the official OpenAPI file payu-api-ref.yaml (PayU GPO Europe REST API 2.1.0, https://developers.payu.com/europe/resources/payu-api-ref.yaml, checked 2026-11-07):

  • Refund notification body: {orderId, extOrderId, refund{refundId, extRefundId, amount, currencyCode, status: FINALIZED | CANCELLED, ...}}. The notification spells the canceled status CANCELLED, refund create/read spell it CANCELED; the add-on accepts both. A refund is found by refundId or, when our request got no answer, by extRefundId. The read-back (GET .../refunds/{refundId}) is a top-level object; the add-on accepts the wrapped form too.
  • REJECTED is not an order status of the spec (NEW, PENDING, WAITING_FOR_CONFIRMATION, COMPLETED, CANCELED); it only exists as a card authorization status. The mapping to failed is harmless and kept.
  • OAuth error body is {error, error_description} (400, 401); the add-on reads exactly these.
  • WAITING_FOR_CONFIRMATION (auto-receive off) is treated as pending. The add-on does not capture (PUT /api/v2_1/orders/{orderId}/status exists); it logs a warning and shows it on the order tab. Auto-receive is a documented requirement.
  • BGN is not a settlement currency in the References page (card table only) and Bulgaria uses the euro since 2026-01-01, so it is no longer offered; an order in an unsupported currency is converted to PLN.
  • Real PayU sandbox: not run (needs a registered sandbox merchant for a webhook-reachable URL); the public POS credentials were not used to place a live order. Tests use the mock built from the pages above.
  • Amounts are integer minor units sent as strings; products is one line with unitPrice = totalAmount, so the sums always agree.
  • extOrderId = kv<txn id>-<8 hex of the access key>: unique per attempt, derivable, so nothing extra is stored.
  • A payment becomes paid only after GET /orders/{orderId} returned COMPLETED and orderId, extOrderId, merchantPosId, totalAmount and currencyCode equal what the add-on created. A mismatch is logged, stored on the transaction and the job is not retried.
  • A notification body is only a hint (the job reads the order back). The same order + status is stored once (idempotent); the customer’s return and the cron (Jobs::schedule) recover a lost notification.
  • A refund row (with its extRefundId and description) is stored before the request. If the request ends without an answer (timeout, 5xx) the row stays unknown, nothing is counted, and every retry (click or queue) sends the same extRefundId, amount and description: the official spec says identical data returns the existing refund instead of creating one. While a row is unknown, only the same amount can be refunded. The refunded total changes in one atomic UPDATE ... refunded_minor = refunded_minor + n, a CANCELED/CANCELLED refund takes its amount back the same way, and refunds of one payment run under a named lock (Lock).
  • The customer’s return, the notification, the cron queue and the admin Sync all end in PaymentService::sync(), which runs under a lock per payment and re-reads the payment and the order inside it: stock and e-mails happen once.
  • Return while PayU is still pending: with the return the order moves N -> pending status (setting, default Open) and the customer is sent to kv_payu.waiting (polls kv_payu.status every 5 s, then kv_payu.finish). initialize() never starts a second PayU order for an order whose earlier payment is pending (the customer is sent to the open one) or paid.
  • Refunds move the owner’s money in owner mode: Access::refundAllowed() denies vendors there (button, controller and the automatic refund at an order status).

The base URL can be overridden for tests in config.local.php (development only):

$config['kv_payu_base_url'] = 'http://127.0.0.1:11981';

node tests/mock-server/server.js 11981 starts a local stand-in of the provider API; node tests/e2e/e2e.mjs runs the HTTP end-to-end test against it; php tests/unit.php runs the unit tests.

Admin templates live in design/backend/templates/addons/kv_payu/. Do not edit them in a store; they are replaced on update.