Use the SDK subscription model when you need to complete the initial subscription payment and recurring renewals within your own checkout page. Compared with a standard one-time payment, SDK subscriptions have these key differences:
subProductType to "SUBSCRIBE"subscription parameter (a JSON string) to define subscription rulesstatus = "R")BIND_CARD and SALE notificationsAlways treat webhooks as the transaction source of truth ( ).
Integration profile
Best-fit scenarios
Key capabilities
subscription.selfExecute determines who executes renewals:
subscriptionManageUrl)Minimal parameter skeleton (example)
{
"requestType": "0",
"merchantCustId": "CUST-10001",
"selfExecute": "1",
"mode": "2",
"frequencyType": "M",
"frequencyPoint": "1",
"cycleCount": 12
}
Implement in this order:
scenarios)The server request is similar to a standard SDK transaction. Key differences:
subProductType to "SUBSCRIBE"subscription is required and must be a JSON stringmerchantCustId explicitly to bind customer and subscription relationship{
"merchantNo": "800209",
"merchantTxnId": "SUB-ORDER-20260313-0001",
"merchantTxnTime": "2026-03-13 10:00:00",
"merchantCustId": "CUST-10001",
"orderAmount": "5.00",
"orderCurrency": "USD",
"productType": "CARD",
"subProductType": "SUBSCRIBE",
"txnType": "SALE",
"txnOrderMsg": "{\"appId\":\"YOUR_APP_ID\",\"returnUrl\":\"https://merchant.example.com/return\",\"notifyUrl\":\"https://merchant.example.com/webhook\"}",
"billingInformation": "{\"email\":\"customer@example.com\",\"country\":\"US\"}",
"shippingInformation": "{\"email\":\"customer@example.com\",\"country\":\"US\"}",
"subscription": "{\"requestType\":\"0\",\"merchantCustId\":\"CUST-10001\",\"productName\":\"Pro Plan Monthly\",\"frequencyType\":\"M\",\"frequencyPoint\":\"1\",\"cycleCount\":12,\"selfExecute\":\"1\",\"mode\":\"2\",\"notificationEmail\":\"customer@example.com\"}",
"sign": "YOUR_SIGNATURE"
}
After order creation succeeds, return these fields to the frontend for SDK initialization:
transactionIdredirectUrlsubscription) Server-sideTo avoid confusion, interpret parameters as shared fields + mode-specific fields.
Shared fields (recommended to confirm first in both modes)
requestType
Required
Request type. Initial subscription is typically 0; use other values based on specific API capabilities.
merchantCustId
Required
Merchant-unique customer identifier. It must be stable and traceable over time.
selfExecute
Required
1 for managed, 2 for self-managed.
mode
Conditionally required
Subscription authorization mode. Credit card subscriptions usually use immediate billing mode.
Managed subscription fields (selfExecute = "1")
frequencyType
Conditionally required
Billing cycle unit, commonly D / M / Y.
frequencyPoint
Conditionally required
Cycle value, used with frequencyType to define billing cadence.
cycleCount / expireDate
One required
Total cycle count or termination date. Provide at least one.
trialDays / trialEnd
Optional (choose one)
Trial duration in days or trial end date.
trialFromPlan
Optional
Whether the trial is counted within the subscription plan cycles.
notificationEmail
Optional
Subscription notification email (recommended).
productName
Optional
Distinguishes multiple subscription plans under the same customer.
Self-managed subscription fields (selfExecute = "2")
frequencyType
Required
Typically configured by day (commonly D).
frequencyPoint
Required
Suggested billing interval value. Actual charges are scheduled by the merchant.
expireDate
Required
Contract termination date. No renewals should be initiated after this date.
productName
Optional
Recommended for distinguishing multiple subscription plans.
bindCard
Optional
If enabled, handle the dual-notification scenario: BIND_CARD + SALE.
subscription must be a JSON string, not an object. Before signing the request, verify escaping and field ordering against your signing implementation requirements.const pacypay = new Pacypay(transactionId, {
container: 'onerway_checkout',
locale: 'zh-cn',
environment: 'sandbox',
mode: 'CARD',
redirectUrl: redirectUrlFromServer,
config: {
subProductType: 'SUBSCRIBE'
},
onPaymentCompleted: function (result) {
const { respCode, respMsg, data } = result || {};
if (respCode !== '20000') {
console.error('Subscription request failed:', respMsg);
return;
}
if (data?.status === 'R' && data?.redirectUrl) {
window.location.replace(data.redirectUrl);
return;
}
if (data?.status === 'S') {
// For frontend UX feedback only. Use webhook as the final state source of truth.
console.log('Subscription submitted successfully. Waiting for webhook confirmation.');
}
},
onError: function (error) {
console.error('SDK error:', error);
}
});
When the SDK callback returns status = "R":
redirectUrlreturnUrlstatus = "R" only means additional authentication is required. It does not mean failure. Do not close the order or roll back the subscription flow at this stage.Responsibility boundary (Must / Must not):
| Role | Must | Must not |
|---|---|---|
| Client | Show in-progress state, handle 3DS redirect, and report required logs | Do not activate membership or fulfill orders directly from SDK callback |
| Server | Verify signatures, process idempotently, persist state, and drive business state machine | Do not change business state without webhook signature verification |
In SDK subscription scenarios, webhooks may include:
txnType = "BIND_CARD": card-binding result (typically for credential setup)txnType = "SALE": initial payment or renewal charge resultIf subscription-related binding is enabled, the initial stage may return two notifications (BIND_CARD + SALE) and arrival order is not guaranteed.
@PostMapping(value = "/webhook", produces = "text/plain")
public String handleWebhook(@RequestBody Map<String, Object> body) {
String transactionId = (String) body.get("transactionId");
String txnType = (String) body.get("txnType");
String status = (String) body.get("status");
if (!verifySignature(body)) return "";
if (alreadyProcessed(transactionId, txnType)) return transactionId;
if ("BIND_CARD".equals(txnType) && "S".equals(status)) {
saveTokenId((String) body.get("merchantCustId"), (String) body.get("tokenId"));
}
if ("SALE".equals(txnType)) {
handleSubscriptionSale(body); // Parse scenarios / subscriptionStatus / contractId
}
markProcessed(transactionId, txnType);
return transactionId;
}
transactionId + txnType as the idempotency key to avoid collisions between notification types under the same transaction.Use webhook field scenarios to identify subscription lifecycle events:
SUBSCRIPTION_INITIAL
Initial subscription
Activate service for the first time and persist contractId / tokenId.
SUBSCRIPTION_RENEWAL
Recurring renewal
Extend service validity and update next billing time.
SUBSCRIPTION_CARD_REPLACEMENT
Payment method replacement
Update credentials while preserving subscription continuity.
SUBSCRIPTION_CHANGED
Upgrade/downgrade or plan change
Adjust entitlement, price, or billing cadence.
SUBSCRIPTION_CANCELED
Subscription canceled
Stop future renewals and process entitlements by policy.
SUBSCRIPTION_ENDED
Subscription ended
End naturally and disable automatic renewals.
subProductType
Request + SDK config
Must be SUBSCRIBE.
subscription
Request
Defines subscription rules (JSON string).
transactionId
Response, callback, webhook
Transaction identifier and idempotency key.
redirectUrl
Response, SDK callback
SDK initialization endpoint or 3DS redirect target.
contractId
SALE notification/result
Subscription contract identifier (required for renewal/change operations).
tokenId
BIND_CARD / SALE
Reference for follow-up charges or credential updates.
subscriptionManageUrl
Managed mode response/notification
Customer self-service subscription management entry.
subscriptionStatus
SALE notification
Current subscription status (for example, active, trialing).
scenarios
SALE notification
Distinguishes subscription event types.
SDK subscriptions also support wallet payments. Core rules stay the same:
subProductType = "SUBSCRIBE" (DIRECT for non-subscription transactions)Prerequisites
Initialization differences
mode: 'GooglePay'config options include googlePayButtonType, googlePayButtonColor, and googlePayEnvironmentconst pacypay = new Pacypay(transactionId, {
container: 'onerway_checkout',
locale: 'zh',
environment: 'sandbox',
mode: 'GooglePay',
redirectUrl: redirectUrlFromServer,
config: {
subProductType: 'SUBSCRIBE',
googlePayButtonType: 'subscribe',
googlePayButtonColor: 'black',
googlePayEnvironment: 'TEST'
}
});
Notification identification
Use walletTypeName = "GooglePay" in webhooks and process business logic with the same subscription event flow (scenarios).
contractId, tokenId, and subscriptionManageUrl, and bind it to merchantCustId.Save and reuse payment methods
Use the Onerway Web SDK to save card credentials during checkout and reuse tokenId for future purchases.
Customize look and feel
Customize the visual appearance of the Onerway Web SDK checkout using built-in themes, CSS variables, custom stylesheets, and fine-grained style overrides.