Build a subscriptions integration

Implement recurring billing with managed or self-managed subscriptions using the Onerway Direct API. Requires PCI DSS compliance.

Overview

Use the Onerway Direct API to build a fully customized subscription flow. This approach gives you complete control over checkout UX and billing logic — choose between letting Onerway automatically handle renewals (managed) or triggering renewals yourself (self-managed). Because you directly handle cardholder data (CHD), this integration requires PCI DSS compliance.

Integration Complexity

Complexity
Integration Type
Custom payment flow
PCI Requirement
PCI DSS compliance required

Use Cases

  • Need full customization of payment forms and UX
  • Want precise control over the subscription lifecycle
  • Already PCI DSS compliant
  • Need flexible subscription management

Key Features

  • Automatic and manual renewal modes
  • Mandatory EMV 3DS for initial CIT transactions
  • Flexible billing cycle configuration
  • Full webhook event notifications
  • Subscription upgrades, downgrades, and cancellations

What you'll build

By the end of this guide, you'll have a working subscription system that:

  • Collects card details from customers and creates a subscription
  • Handles mandatory EMV 3DS for the initial Customer-Initiated Transaction (CIT)
  • Processes webhook notifications to activate or suspend service
  • (Self-managed) Triggers renewal Merchant-Initiated Transactions (MIT) on your own schedule

Choose your subscription mode before you begin — the integration steps differ depending on which mode you select.

Choose your subscription mode

Use case: Let Onerway handle all renewals automatically, reducing development and maintenance overhead.

Features:

  • ✅ Onerway automatically processes every billing cycle
  • ✅ Customer self-service portal (subscriptionManageUrl)
  • ✅ Automated payment notification emails
  • ⚠️ Initial subscription triggers two webhooks: BIND_CARD (store credential tokenization result) + SALE (initial CIT result)

Workflow:

Core concepts

API object definitions

ObjectDescription
CustomerRepresents the customer in your system, identified by merchantCustId. All subscriptions and renewals for a customer are linked through this identifier.
SubscriptionSubscription agreement containing product, billing cadence, billing anchor, trial settings, mode, expiration, and notification email
ContractSubscription contract identified by , used to reference MIT charges
TokenStored credential token identified by , used for subsequent billing

Key response fields

After a successful initial subscription, track these fields:

Field

contractId

Description

Subscription contract ID — required for renewals and cancellations

Field

tokenId

Description

Stored credential token — required for self-managed MIT renewals

Field

subscriptionManageUrl

Description

Customer self-service portal URL (managed subscriptions only)

Field

status

Description

Transaction result:

  • "S" success
  • "F" failure
  • "R" requires 3DS challenge/redirect (final outcome delivered asynchronously via webhook)
Field

dataStatus

Description

Contract status:

  • 0 Pending
  • 1 Active
  • 2 Disabled
  • 3 Canceled
Field

subscriptionStatus

Description

Current subscription state (trialing, active, pastdue, etc.); see mapping table below

Subscription status lifecycle

Without trial: paymentdueactivepastdue/pausedcanceled/ended
With trial: trialingactivepastdue/pausedcanceled/ended
subscriptionStatusDescriptiondataStatus
paymentdueAwaiting payment: initial CIT not completed0 Pending
trialingTrial period: zero-amount verification, charge after trial0 Pending
activeActive: charge successful1 Active
pastdueOverdue: renewal charge failed1 Active
pausedPaused1 Active
canceledCanceled by customer3 Canceled
endedEnded: naturally expired2 Disabled
For the full request/response field reference, see the API docs:

Integration steps

1. Prerequisites

1.1 Confirm PCI compliance

If you are not PCI DSS certified (typically SAQ D scope for direct card capture), use Checkout or SDK instead.

1.2 Obtain API credentials

See Set Up Your Development Environment to get your merchantNo, appId, and Secret Key.

2. Create a customer record

Onerway identifies customers via merchantCustId. No Onerway API call is needed to create a customer — simply generate a unique identifier in your system and pass it when creating the subscription:

// Generate and store a unique customer identifier
String merchantCustId = "CUST-" + UUID.randomUUID();
userRepository.saveMerchantCustId(userId, merchantCustId);
merchantCustId is your system's unique identifier for the customer. All subscriptions and renewals for the same user are linked through it.

3. Create subscription

The two subscription modes are distinguished by the selfExecute field: "1" for managed, "2" for self-managed.

Onerway handles all renewal cycles automatically. Trial periods and customer notification emails are optional.

Optional fields:
  • Use either cycleCount (total billing cycles) or expireDate (contract end date), not both.
  • notificationEmail sends payment notifications to your customer.
  • Set mode to "2" for an immediate CIT charge (required for card-based subscriptions). "1" is a zero-amount verification mode only supported for specific local payment methods such as DANA or WeChat Pay — credit cards do not support this value.
Map<String, Object> subscription = new HashMap<>();
subscription.put("requestType", "0");          // Initial purchase
subscription.put("merchantCustId", merchantCustId);
subscription.put("selfExecute", "1");          // Managed: Onerway auto-renews
subscription.put("mode", "2");                 // "1"=zero-auth, "2"=immediate charge
subscription.put("frequencyType", "M");        // D=day, M=month, Y=year
subscription.put("frequencyPoint", "1");       // Every 1 month
subscription.put("cycleCount", 12);            // Total billing cycles (or use expireDate)
subscription.put("notificationEmail", email);  // Optional: customer email notifications
A complete transaction request also requires merchantNo, merchantTxnId, orderAmount, orderCurrency, cardInfo, billingInformation, shippingInformation, and a request signature. See the API ReferencePayments API for the full parameter list.

4. Handle 3DS verification

Required: All initial subscription transactions must undergo

3DS verification flow:

  1. Check the response: If status == "R", retrieve the redirectUrl
  2. Redirect the user: Send the user to redirectUrl to complete verification
  3. User completes verification: Returns to your returnUrl
  4. Await notification: The final authorization/capture result is delivered asynchronously via webhook (status == "R" does not indicate success)
// Check whether a 3DS redirect is needed
if ("R".equals(status)) {
    return Map.of("action", "redirect", "url", redirectUrl);
}
// Perform redirect (use replace to avoid adding an intermediate page to history)
if (result.action === 'redirect') {
  window.location.replace(result.url);
}
Trigger the 3DS redirect synchronously inside a click event handler. Navigations initiated inside async/await or .then() callbacks may be blocked as popups in Safari and other browsers.
Alternatively, perform the redirect server-side with an HTTP 302 response. This avoids all browser popup restrictions.

5. Receive webhook notifications

Set up a webhook endpoint to receive payment notifications from Onerway. Use the status, subscriptionStatus, and scenarios fields to drive your business logic. Treat delivery as at-least-once and implement idempotent processing with transactionId.

Response requirement: Return the transactionId value (plain text) only after successful processing. transactionId is unique per transaction, and retries for the same transaction use the same value. If Onerway receives no valid response, it retries after 30 minutes, up to 2 times.
@PostMapping(value = "/webhook", produces = "text/plain")
public String handleWebhook(@RequestBody Map<String, Object> data) {
    String transactionId = (String) data.get("transactionId");

    // 1. Verify signature (see API docs)
    if (!verifySignature(data)) {
        return "";
    }

    // 2. Idempotency guard: transactionId is the deduplication key
    if (alreadyProcessed(transactionId)) {
        return transactionId;
    }

    try {
        // 3. Parse key fields
        String txnType = (String) data.get("txnType");
        String status = (String) data.get("status");                    // S=success, F=failure
        String merchantCustId = (String) data.get("merchantCustId");

        // 4. Dispatch by transaction type
        if ("BIND_CARD".equals(txnType)) {
            handleBindCard(data, status, merchantCustId);
        } else if ("SALE".equals(txnType)) {
            handleSale(data, status, merchantCustId);
        }

        // 5. Mark success and acknowledge
        markProcessed(transactionId);
        return transactionId;
    } catch (Exception ex) {
        // Return empty response to trigger Onerway retry
        log.error("Webhook processing failed, transactionId={}", transactionId, ex);
        return "";
    }
}

// Handle card-binding notification (BIND_CARD does not include contractId)
private void handleBindCard(Map<String, Object> data, String status, String merchantCustId) {
    if (!"S".equals(status)) {
        log.warn("Card binding failed: merchantCustId={}", merchantCustId);
        return;
    }
    saveTokenId(merchantCustId, (String) data.get("tokenId"));
}

// Handle charge notification
private void handleSale(Map<String, Object> data, String status, String merchantCustId) {
    String scenarios = (String) data.get("scenarios");
    String subscriptionStatus = (String) data.get("subscriptionStatus");
    String contractId = (String) data.get("contractId");
    String tokenId = (String) data.get("tokenId");

    // Save subscription credentials first (idempotent)
    saveSubscription(merchantCustId, contractId, tokenId);

    // Check transaction result
    if (!"S".equals(status)) {
        handlePaymentFailure(merchantCustId, subscriptionStatus);
        return;
    }

    // Dispatch by subscription event type
    switch (scenarios) {
        case "SUBSCRIPTION_INITIAL":
            handleInitialSubscription(merchantCustId, subscriptionStatus);
            break;
        case "SUBSCRIPTION_RENEWAL":
            handleRenewalSuccess(merchantCustId, contractId);
            break;
        case "SUBSCRIPTION_CANCELED":
        case "SUBSCRIPTION_ENDED":
            revokeAccess(merchantCustId);
            break;
    }
}

// Initial subscription: activate service based on subscriptionStatus
private void handleInitialSubscription(String merchantCustId, String subscriptionStatus) {
    switch (subscriptionStatus) {
        case "active":   grantFullAccess(merchantCustId); break;
        case "trialing": grantTrialAccess(merchantCustId); break;
        // paymentdue: awaiting payment, do not activate yet
    }
}

// Renewal succeeded
private void handleRenewalSuccess(String merchantCustId, String contractId) {
    extendUserAccess(merchantCustId);
    subscriptionRepository.updateNextBillingDate(contractId);
}

// Payment failed
private void handlePaymentFailure(String merchantCustId, String subscriptionStatus) {
    if ("pastdue".equals(subscriptionStatus)) {
        sendPaymentReminder(merchantCustId);  // Renewal failed — send dunning notification
    }
}

Webhooks by subscription mode:

TimingManaged SubscriptionSelf-Managed Subscription
Initial subscriptionBIND_CARD + SALE (one each)BIND_CARD + SALE (one each)
Subsequent renewalsSALE (auto-triggered by Onerway)SALE (triggered after merchant calls renewal API)
Key fields:
FieldDescription
statusTransaction result: S success / F failure
scenariosSubscription event: SUBSCRIPTION_INITIAL, SUBSCRIPTION_RENEWAL, etc.
subscriptionStatusSubscription state: active, trialing, pastdue, etc.
contractIdContract ID (returned in SALE only, not in BIND_CARD)
tokenIdPayment token (returned in both notification types)
transactionIdUnique payment order ID per transaction; use as the webhook idempotency key
BIND_CARD and SALE may arrive in either order during initial subscription — ensure idempotent handling. For the full field list and signature verification, see the NotificationPayments API.

Monitor webhook events

Verify the following events fire correctly during testing:

EventWhen it fires
BIND_CARDAfter the customer submits card details
SALE + SUBSCRIPTION_INITIALAfter 3DS completes and the initial charge succeeds
SALE + SUBSCRIPTION_RENEWALAfter each renewal cycle
SALE + SUBSCRIPTION_CANCELEDAfter a cancellation request is processed

Provision and revoke access

Use the subscriptionStatus from the SALE webhook to manage customer access:

subscriptionStatusRecommended action
activeGrant full access
trialingGrant trial access
paymentdueAwaiting payment — do not activate
pastdueMaintain access + send dunning notification (grace period policy)
pausedSuspend access, retain data
canceled / endedRevoke access

6. Renewals (self-managed subscriptions)

  • Managed subscriptions renew automatically — skip this step
  • Self-managed renewals use the stored contractId and tokenId — no raw card data is transmitted
  • Renewal API calls do not transmit PAN/CVV and do not trigger 3DS authentication
  • If your renewal flow does not collect/process/store card data, PCI DSS card-data scope is usually reduced; confirm with your QSA/compliance team

Self-managed subscriptions require a renewal scheduler in your system. Update business state only after receiving the webhook — do not update immediately after the API call:

@Scheduled(cron = "0 0 2 * * ?") // Run daily at 2 AM to process due renewals
public void processRenewals() {
    List<Subscription> dueSubscriptions =
        subscriptionRepository.findByNextBillingDateBefore(LocalDate.now());

    for (Subscription sub : dueSubscriptions) {
        Map<String, Object> subscription = new HashMap<>();
        subscription.put("requestType", "1");          // Renewal
        subscription.put("merchantCustId", sub.getMerchantCustId());
        subscription.put("contractId", sub.getContractId());
        subscription.put("tokenId", sub.getTokenId());
        // billingInformation can be empty: "{}"

        // Call renewal API (see API docs for full parameter list)
        httpClient.post(ONERWAY_API_URL, buildRequest(subscription));
        // Wait for the SALE webhook before updating business state
    }
}

// Update business state after receiving the renewal SALE webhook
private void handleRenewalSale(JSONObject data) {
    if ("S".equals(data.getString("status"))) {
        extendUserAccess(data.getString("merchantCustId"));
        subscriptionRepository.updateNextBillingDate(data.getString("contractId"));
    }
}

7. Cancel subscription

Allow customers to cancel their subscriptions from your interface. Update business state based on the webhook notification — do not revoke access immediately after the API call:

// Frontend: confirm and submit cancellation
async function cancelSubscription(contractId) {
  if (!confirm('Are you sure you want to cancel your subscription?')) return;
  await fetch('/api/cancel-subscription', {
    method: 'POST',
    body: JSON.stringify({ contractId })
  });
}
// Backend: call cancellation API (access is revoked when the SUBSCRIPTION_CANCELED webhook arrives)
@PostMapping("/api/cancel-subscription")
public ResponseEntity<?> cancelSubscription(@RequestBody Map<String, String> req) {
    Map<String, Object> cancelRequest = new HashMap<>();
    cancelRequest.put("contractId", req.get("contractId"));
    // Call cancellation API (see API docs for full parameter list)
    httpClient.post(ONERWAY_CANCEL_API_URL, buildRequest(cancelRequest));
    // Access is revoked when the SUBSCRIPTION_CANCELED webhook arrives
    return ResponseEntity.ok(Map.of("message", "Cancellation request submitted"));
}

Test your integration

Test card numbers

Use the following test cards to verify different scenarios (see Test CardPayments API for the full list):

Success scenarios:

Card NumberCountry3DS Behavior
4000 0209 5159 5032USChallenge (verification required)
2221 0081 2367 7736USChallenge (verification required)

Failure scenarios:

Card NumberCountryFailure Reason
4000 1284 4949 8204USDo not honor
4021 9371 9565 8141GBInsufficient funds

Use these test credentials for all cards above:

  • CVV: Any 3 digits
  • Expiry date: Any future date
  • Cardholder name: Any name

Test scenario checklist

Complete the following based on your chosen subscription mode:

Common (both modes):

Managed subscriptions (verify only):

Self-managed subscriptions (verify only):

Subscription event reference

Identify subscription event types via the scenarios field:

EventDescription
SUBSCRIPTION_INITIALInitial subscription purchase
SUBSCRIPTION_RENEWALPeriodic renewal
SUBSCRIPTION_CARD_REPLACEMENTPayment method update
SUBSCRIPTION_CHANGEDSubscription modification
SUBSCRIPTION_CANCELEDSubscription cancellation
SUBSCRIPTION_ENDEDSubscription expiration