Skip to content

Payment (DOKU)

Payment is DOKU. The backend creates the payment and verifies the callback; the storefront opens DOKU's checkout widget.

How this page was verified

Read on 2026-09-15 from the backend payments module and the storefront DOKU hook.

Backend

The payments module has no entities of its own — payment data lives on the order. It is REST-only (not in the GraphQL include).

  • PaymentsService (src/payments/services/payments.service.ts) is the DOKU client: postPaymentRequest and checkPaymentStatus. Base URL DOKU_BASEURL; credentials DOKU_CLIENT_ID and DOKU_SECRET_KEY; each request is signed HMAC-SHA256 (HMACSHA256=<base64>). The payment window is DOKU_PAYMENT_DUE_DATE_MINUTE.
  • Callback URLs are built from APP_FRONTEND_STOREFRONT_BASE_URL; DOKU's notify goes to ${APP_BACKEND_BASE_URL}/webhooks/payments.
  • webhook-payments.service.ts handleEvent: on transaction.status === 'SUCCESS' it moves the order to processing. The backend is created with rawBody: true so it can verify the DOKU signature.

The order is created with a payment link during createOrder() — see Cart and checkout.

Storefront

src/hooks/useDokuCheckoutPayment.tsx loads DOKU's Jokul Checkout script (sandbox vs production URL by NEXT_PUBLIC_DOKU_ENV) and calls window.loadJokulCheckout(paymentUrl) with the paymentLinkUrl returned by frontValidateMemberCartCheckout. On close the customer lands on /user/my-order?status=pending_payment. The same hook powers "pay again" from an order detail (src/modules/my-order/components/order-item-detail.tsx).

Unpaid orders

An order left pending_payment past its window is cancelled by the order expiry cron, which re-checks DOKU first.

Gotchas

  • Signature needs the raw body. The backend keeps rawBody: true for exactly this — do not strip it.
  • In dev, DOKU is the simulator. The simulator can force DOKU to error; a dev payment failure may be the toggle, not the code.
  • The inbound production callback path fans out through the webhook receiver → SNS, not straight to the backend.