Singapay Home Page
Logo Icon
  1. Subscription (Recurring)
  2. Create Plan

Information

MethodPathFormatAuthentication
POST/api/v2.0/recurring/plansjsonOAuth 2.0 with Access Token

Creates a new subscription plan for the merchant. The plan starts in pending_card_linking status and returns a payment_link_url that must be opened by the customer to enter their card details once. The first cycle is charged as part of the card linking flow; subsequent cycles are auto-charged by SingaPay on the configured schedule.

Amount vs. Items: Send exactly ONE of amount (amount-only plan) or items[] (itemized plan). Sending both in the same request returns a 422 validation error.


Request Details

Headers Structure

FieldValueTypeMandatoryDescriptionExample
AuthorizationBearer {access_token}AlphanumericMandatoryBearer token obtained from the access token endpoint.Bearer eyJ0eXAiOiJKV1{…}
X-PARTNER-IDAlphanumericMandatoryYour API Key from the merchant dashboardpk_live_abc123def456
Content-Typeapplication/jsonAlphabeticMandatorySpecifies JSON as the request body format.application/json

Body Structure

ParameterTypeMandatoryValidationDescriptionExample
nameStringRequiredmax: 255Display name of the subscription plan.Premium Monthly
subscription_idStringOptionalmax: 100, unique per merchantGlobally unique merchant-supplied identifier for this plan. Auto-generated if omitted.PLAN-20260420-001
merchant_reff_noStringOptionalmax: 255Merchant-side reference label (non-unique). Echoed back on every cycle webhook for easy matching in the merchant’s system.SUB-CUST-ACME-001
amountNumberConditionalmin: card channel minimum (IDR)Per-cycle charge in IDR. Required when items is not provided. Mutually exclusive with items.150000
itemsArrayConditionalmin: 1 itemLine items for an itemized plan. Required when amount is not provided. Mutually exclusive with amount. Per-cycle charge = Σ(qty × price).see example below
> items[].item_nameStringRequiredmax: 191Line item name.Premium Seat
> items[].item_typeStringOptionalmax: 50Free-form item category (defaults to product).service
> items[].quantityIntegerRequiredmin: 1Quantity of the line item.2
> items[].unit_priceNumberRequiredmin: 0Unit price in IDR.75000
currencyStringOptionalsize: 3 (ISO 4217)Currency code. Defaults to IDR.IDR
customer_nameStringRequiredmax: 191Customer’s full name (used for invoicing and notifications).John Doe
customer_emailStringRequiredemail, max: 191Customer’s email address.john@example.com
customer_phoneStringRequiredmax: 50Customer’s phone number.08123456789
customer_idStringOptionalmax: 100Customer identifier in your system. Auto-generated if omitted.CUST-001
account_idStringRequiredULID; must belong to the merchantMerchant Account ULID the plan is scoped to.01K5G4FZZ18DMK0M5QTR8Y9QY9
scheduleObjectRequired-Recurring schedule configuration.-
> schedule.intervalIntegerRequiredmin: 1Number of interval_units between charges.1
> schedule.interval_unitStringRequiredday, week, monthTime unit for the interval.month
> schedule.total_intervalIntegerOptionalmin: 1Total number of cycles to charge. Omit for an open-ended subscription.12
> schedule.start_timeStringRequireddate, on or after todayFirst cycle start date (YYYY-MM-DD).2026-05-01
payment_typeStringOptionalcredit_card, gopayRestrict the customer checkout to a specific method. If omitted, all configured methods on the account are offered.credit_card
return_urlStringOptionalurl, max: 2048URL the customer is redirected to after the one-time card linking flow (success, failure, or cancel).https://merchant.com/callback
retry_policyObjectOptional-Failed-cycle retry policy. Defaults applied when omitted.-
> retry_policy.max_attemptsIntegerOptional1–5 (default: 3)Automatic retry attempts after the initial charge fails.3
> retry_policy.interval_daysIntegerOptional1–7 (default: 3)Days to wait between consecutive retry attempts.3
> retry_policy.failed_payment_actionStringOptionalcontinue_plan, stop_planBehavior after retries are exhausted. Defaults to stop_plan.stop_plan
charge_immediatelyBooleanOptional-When true, the plan runs the first charge immediately after card linking regardless of schedule.start_time.false
allow_manual_paymentBooleanOptional-When true, the merchant dashboard allows manual charging outside the automatic schedule.false
allow_user_notificationBooleanOptionaldefault: falseWhen true, the plan is created with email notifications enabled for every subscription event (plan created, cycle charged, payment failed, plan cancelled, etc.) addressed to customer_email. When false or omitted, no notification preferences are created — the merchant can still configure them later from the dashboard.true
metadataObjectOptional-Free-form metadata stored with the plan.-
> metadata.descriptionStringOptionalmax: 1000Plan description (surfaced on invoices and dashboards).Premium monthly subscription

Deprecated flat retry fields: retry_count, retry_interval_days, and failed_payment_action are still accepted as top-level fields for backward compatibility with pre-release integrations, but new integrations should use the nested retry_policy object shown above. The nested form is symmetric with API responses and webhook payloads.

Request Example — Amount-Only Plan

{
    "name": "Premium Monthly",
    "subscription_id": "PLAN-20260420-001",
    "merchant_reff_no": "SUB-CUST-ACME-001",
    "amount": 150000,
    "currency": "IDR",
    "customer_name": "John Doe",
    "customer_email": "john@example.com",
    "customer_phone": "08123456789",
    "customer_id": "CUST-001",
    "account_id": "01K5G4FZZ18DMK0M5QTR8Y9QY9",
    "schedule": {
        "interval": 1,
        "interval_unit": "month",
        "total_interval": 12,
        "start_time": "2026-05-01"
    },
    "payment_type": "credit_card",
    "return_url": "https://merchant.com/callback",
    "retry_policy": {
        "max_attempts": 3,
        "interval_days": 3,
        "failed_payment_action": "stop_plan"
    },
    "allow_user_notification": true,
    "metadata": {
        "description": "Premium monthly subscription"
    }
}

Request Example — Itemized Plan

{
    "name": "Team Plan",
    "merchant_reff_no": "SUB-CUST-ACME-TEAM",
    "items": [
        {
            "item_name": "Premium Seat",
            "item_type": "service",
            "quantity": 3,
            "unit_price": 75000
        },
        {
            "item_name": "Premium Support",
            "item_type": "service",
            "quantity": 1,
            "unit_price": 50000
        }
    ],
    "customer_name": "John Doe",
    "customer_email": "john@example.com",
    "customer_phone": "08123456789",
    "account_id": "01K5G4FZZ18DMK0M5QTR8Y9QY9",
    "schedule": {
        "interval": 1,
        "interval_unit": "month",
        "start_time": "2026-05-01"
    },
    "payment_type": "credit_card",
    "return_url": "https://merchant.com/callback"
}

cURL Example:

curl -X POST "https://your-domain.com/api/v2.0/recurring/plans" \
  -H "X-PARTNER-ID: {api_key}" \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Premium Monthly",
    "merchant_reff_no": "SUB-CUST-ACME-001",
    "amount": 150000,
    "customer_name": "John Doe",
    "customer_email": "john@example.com",
    "customer_phone": "08123456789",
    "account_id": "01K5G4FZZ18DMK0M5QTR8Y9QY9",
    "schedule": {
        "interval": 1,
        "interval_unit": "month",
        "start_time": "2026-05-01"
    },
    "payment_type": "credit_card",
    "return_url": "https://merchant.com/callback"
  }'

Response Details

Response Structure

FieldTypeMandatoryDescriptionExample
response_codeStringMandatoryResponse code. SP000 on success.SP000
response_messageStringMandatoryHuman-readable response message.Successfully
dataObjectMandatoryPlan payload.-
> idStringMandatorySubscription Plan ID (26-char ULID). Use this on all subsequent GET/PATCH/Cancel calls.01JAB3CD4E5F6G7H8J9K0M1N2
> nameStringMandatoryPlan display name.Premium Monthly
> amountStringMandatoryPer-cycle charge, formatted as a string to preserve precision.“150000”
> currencyStringMandatoryISO 4217 currency code.IDR
> created_atStringMandatoryPlan creation timestamp (ISO 8601).2026-04-20T10:00:00+07:00
> scheduleObjectMandatorySchedule configuration + live progress.-
>> schedule.intervalIntegerMandatoryInterval value.1
>> schedule.interval_unitStringMandatoryday | week | month.month
>> schedule.current_intervalIntegerMandatoryNumber of cycles produced so far.0
>> schedule.total_intervalInteger/NullOptionalTotal configured cycles, or null for open-ended.12
>> schedule.start_timeStringMandatoryFirst cycle start (ISO 8601).2026-05-01T00:00:00+07:00
>> schedule.previous_payment_atString/NullOptionalLast successful charge timestamp (ISO 8601).null
>> schedule.next_payment_atString/NullOptionalNext scheduled charge timestamp (ISO 8601).2026-05-01T00:00:00+07:00
> statusStringMandatoryPlan status (see lifecycle section on the Subscription landing page).pending_card_linking
> payment_typeString/NullOptionalActive payment method for the plan, or null before card linking completes.credit_card
> retry_policyObjectMandatoryResolved retry policy (applied defaults included).-
>> retry_policy.max_attemptsIntegerMandatoryResolved max retry attempts.3
>> retry_policy.interval_daysIntegerMandatoryResolved retry interval (days).3
>> retry_policy.failed_payment_actionStringMandatorycontinue_plan | stop_plan.stop_plan
> metadataObjectMandatoryPlan metadata container.-
>> metadata.descriptionString/NullOptionalPlan description.Premium monthly subscription
>> metadata.extraObjectOptionalFree-form merchant metadata stored on the plan.
> subscription_idStringMandatoryGlobally unique plan reference.PLAN-20260420-001
> merchant_reff_noString/NullOptionalMerchant-side reference label.SUB-CUST-ACME-001
> payment_link_urlStringMandatoryOpen this URL in the customer’s browser to complete card linking + initial charge.https://pay.singapay.id/link/abc123
> parent_plan_idString/NullOptionalPrevious plan this one superseded via upgrade. null for plans created directly.null
> created_fromString/NullOptionalOrigin marker: upgrade, downgrade, or null for directly-created plans.null

Response Example — Success (HTTP 201)

Success: Plan created. Redirect the customer to payment_link_url to complete card linking.

{
    "response_code": "SP000",
    "response_message": "Successfully",
    "data": {
        "id": "01JAB3CD4E5F6G7H8J9K0M1N2",
        "name": "Premium Monthly",
        "amount": "150000",
        "currency": "IDR",
        "created_at": "2026-04-20T10:00:00+07:00",
        "schedule": {
            "interval": 1,
            "interval_unit": "month",
            "current_interval": 0,
            "total_interval": 12,
            "start_time": "2026-05-01T00:00:00+07:00",
            "previous_payment_at": null,
            "next_payment_at": "2026-05-01T00:00:00+07:00"
        },
        "status": "pending_card_linking",
        "payment_type": "credit_card",
        "retry_policy": {
            "max_attempts": 3,
            "interval_days": 3,
            "failed_payment_action": "stop_plan"
        },
        "metadata": {
            "description": "Premium monthly subscription",
            "extra": {
                "payment_type": "credit_card",
                "return_url": "https://merchant.com/callback",
                "api_created": true
            }
        },
        "subscription_id": "PLAN-20260420-001",
        "merchant_reff_no": "SUB-CUST-ACME-001",
        "payment_link_url": "https://pay.singapay.id/link/abc123",
        "parent_plan_id": null,
        "created_from": null
    }
}

Error Response — Validation (HTTP 422)

Error: Sent both amount and items in the same request.

{
    "message": "The amount field prohibits items from being present.",
    "errors": {
        "amount": [
            "The amount field prohibits items from being present."
        ],
        "items": [
            "The items field prohibits amount from being present."
        ]
    }
}

Error Response — Account Not Found (HTTP 404)

Error: account_id does not belong to the authenticated merchant.

{
    "response_code": "SP020",
    "response_message": "Merchant Account Not Found",
    "data": {}
}

Post-Create Flow

  1. Redirect the customer: Open payment_link_url in the customer’s browser. This is a one-time flow that collects card details and performs 3DS if required.
  2. Initial charge: After successful card linking, the first cycle is charged immediately (if charge_immediately: true or start_time is today) or deferred to schedule.start_time.
  3. Plan activates: Plan status transitions to active once the first charge succeeds.
  4. Auto-charging: Subsequent cycles are charged automatically on the schedule using the tokenized card — no customer action required.
  5. Webhooks: Your configured subscription_cycle_notif_url receives a subscription.cycle.payment_success or subscription.cycle.payment_failed notification for every cycle attempt. See the Subscription Cycle webhook reference.

Auto-cancel on initial linking failure (charge_immediately = true only): If the customer submits card details and the issuer / acquirer rejects the charge, the plan is auto-cancelled instead of queueing retries — there is no saved card to retry against. The merchant receives a subscription.plan.status_changed webhook with plan.metadata.cancellation_reason set to initial_linking_failed. To re-attempt, create a new plan.


Important Notes

  • Card-Only Channel: Subscriptions currently run on credit card recurring. payment_type must be credit_card (or omitted to inherit the account default).
  • Card Minimum: The per-cycle charge must clear the card channel minimum. For itemized plans, the minimum is checked against the derived total (Σ quantity × unit_price).
  • Schedule Start: schedule.start_time must be today or in the future.
  • Plan ID Format: The plan id is a 26-character ULID. Use it on all subsequent GET, PATCH, and Cancel requests.
  • Customer Notifications: Customer email / phone are used for payment notifications and invoice delivery.