Developer Docs
Storefront API
API References
Subscription Management

Subscription Management

The Management group lets an authenticated customer mutate their own subscription contracts: pause/resume/cancel, swap products, change frequency, edit shipping and payment, manage lines, and act on billing cycles.

All paths are relative to the base URL:

https://<your-app-domain>/storefront-api/v1

Every management endpoint requires both auth layers on every request:

X-Joy-App-Id: <app-id>
X-Joy-Secret-Key: sk_live_xxx     # server-side only — never expose in the browser
X-Session-Id: <session-id>

The shopId is always derived from the session, never from a header. {contractId} is the numeric subscriptionContractId. Accessing a contract that is not owned by the authenticated customer returns 404 NOT_FOUND (this also covers contracts that do not exist, to prevent IDOR).

Write methods (POST / PUT / DELETE) are subject to the write rate-limit dimension (20 requests / 60s per customer, burst 5). Exceeding any limit returns 429 RATE_LIMITED. Many of these endpoints return { "success": true } with no data on success.

Endpoint summary

MethodPathDescription
PUT/subscriptions/{contractId}/statusPause / resume / cancel.
POST/subscriptions/{contractId}/cancelCancel with a reason code.
PUT/subscriptions/{contractId}/productSwap product / variant.
PUT/subscriptions/{contractId}/frequencyChange delivery frequency.
GET/subscriptions/{contractId}/shipping-addressGet current shipping address.
PUT/subscriptions/{contractId}/shipping-addressUpdate shipping address.
GET/subscriptions/{contractId}/payment-methodGet masked payment metadata.
PUT/subscriptions/{contractId}/payment-methodUpdate payment method.
PUT/subscriptions/{contractId}/shipping-optionChange shipping rate.
PUT/subscriptions/{contractId}/subscription-discountApply a subscription discount.
POST/subscriptions/{contractId}/linesAdd a recurring line.
DELETE/subscriptions/{contractId}/lines/{lineId}Remove a recurring line.
PUT/subscriptions/{contractId}/billing-cycleSkip / reschedule / resume / order-now a cycle.
PUT/subscriptions/{contractId}/upcoming-orders/{cycleIndex}/lines/{lineId}Update an upcoming-order line.
DELETE/subscriptions/{contractId}/upcoming-orders/{cycleIndex}/lines/{lineId}Remove an upcoming-order line.
POST/subscriptions/{contractId}/upcoming-orders/productsAdd a one-off product to an upcoming order.
POST/subscriptions/{contractId}/discountApply a discount code.
DELETE/subscriptions/{contractId}/discountRemove an applied discount.

Status — pause / resume / cancel

PUT /subscriptions/{contractId}/status

Body

FieldTypeRequiredValues
statusstringyespause | resume | cancel
{ "status": "pause" }

Response

{ "success": true }
curl -X PUT "https://<your-app-domain>/storefront-api/v1/subscriptions/123456789/status" \
  -H "X-Joy-App-Id: <app-id>" \
  -H "X-Joy-Secret-Key: sk_live_xxx" \
  -H "X-Session-Id: <session-id>" \
  -H "Content-Type: application/json" \
  -d '{ "status": "pause" }'

Cancel with a reason

POST /subscriptions/{contractId}/cancel

Cancel with a structured reason. reasonCode is threaded into the outbound subscription_contracts/cancel webhook payload.

Body

FieldTypeRequiredNotes
reasonCodestringyestoo_expensive | no_longer_needed | switching | quality_issue | other
commentstringnoMax 500 chars.
retentionOfferAcceptedbooleanno
{ "reasonCode": "too_expensive", "comment": "Found a cheaper option" }

Response

{ "success": true, "data": { "contractId": 123456789, "status": "cancelled" } }

Swap product / variant

PUT /subscriptions/{contractId}/product

Extra planData fields are forwarded verbatim to the swap service.

Body

FieldTypeRequiredNotes
newVariantIdintegeryesTarget variant.
lineIdstringnoLine to swap.
currentVariantIdintegerno
newProductIdintegerno
sellingPlanIdstringno
planDataobjectnoPlan config; discountConfig.enabled is normalized server-side.
{ "newVariantId": 4455667788, "lineId": "gid://shopify/SubscriptionLine/1" }

Response

{ "success": true }

Change frequency

PUT /subscriptions/{contractId}/frequency

Body

FieldTypeRequiredNotes
input.intervalstringyesDAY | WEEK | MONTH | YEAR
input.intervalCountintegeryesPositive.
previousPlanobjectno
initPlanobjectno
baseCurrencystringno
returnedCurrencystringno
{ "input": { "interval": "MONTH", "intervalCount": 2 } }

Response

{ "success": true, "data": { } }

Shipping address

Get current address

GET /subscriptions/{contractId}/shipping-address

Returns the current delivery address (PII projection).

{
  "success": true,
  "data": {
    "firstName": "…", "lastName": "…",
    "address1": "…", "address2": null,
    "city": "…", "province": "…", "provinceCode": "…",
    "country": "…", "countryCode": "…",
    "zip": "…", "phone": "…", "company": null
  }
}

Update address

PUT /subscriptions/{contractId}/shipping-address

Body

FieldTypeRequired
input.address1stringyes
input.citystringyes
input.zipstringyes
input.firstName, input.lastName, input.address2, input.province, input.provinceCode, input.country, input.countryCode, input.phone, input.companystringno
{ "input": { "address1": "123 Main St", "city": "Hanoi", "zip": "100000" } }

Response

{ "success": true }

Payment method

Get masked metadata

GET /subscriptions/{contractId}/payment-method

Masked payment metadata only — never PAN/CVV.

{
  "success": true,
  "data": { "id": "…", "brand": "visa", "last4": "4242", "expiryMonth": 12, "expiryYear": 2027 }
}

Update payment method

PUT /subscriptions/{contractId}/payment-method

Assign a tokenized payment method.

Body

FieldTypeRequiredNotes
input.paymentMethodIdstringyesTokenized payment method id.
customerPaymentMethodobjectno{ brand, last4, expiryMonth, expiryYear } for display mirroring.
{ "input": { "paymentMethodId": "<payment-method-id>" } }

Response

{ "success": true }

Change shipping option

PUT /subscriptions/{contractId}/shipping-option

Body

FieldTypeRequiredNotes
shippingOption.handlestringyes
shippingOption.title, shippingOption.price, shippingOption.codemixedno
{ "shippingOption": { "handle": "standard", "title": "Standard", "price": "5.00" } }

Response

{ "success": true }

Subscription discount

PUT /subscriptions/{contractId}/subscription-discount

Apply a subscription-level discount.

Merchant-gated: returns 403 FORBIDDEN unless the merchant enabled customerPortal.allowSubscriptionDiscount (disabled by default).

Body

FieldTypeRequiredValues
discountTypestringyesPERCENTAGE | FIXED_AMOUNT
discountValuenumberyesPositive.
applyToOrdersmixednoScope hint.
{ "discountType": "PERCENTAGE", "discountValue": 10 }

Response

{ "success": true }

Recurring lines

Add a line

POST /subscriptions/{contractId}/lines

Body

FieldTypeRequiredNotes
linesarrayyesAt least 1 line { variantId, quantity, productId?, sellingPlanId? }.
inputobjectno
currentLinesarraynoUsed for email diffing.
giftMetamixedno
{ "lines": [ { "variantId": 4455667788, "quantity": 1 } ] }

Response

{ "success": true, "data": { } }

Remove a line

DELETE /subscriptions/{contractId}/lines/{lineId}

lineId is a path param; the body carries optional metadata.

Body (optional)

FieldTypeNotes
sellingPlanIdstring
inputobject
currentLinesarray

Response

{ "success": true, "data": { } }

Billing cycle

PUT /subscriptions/{contractId}/billing-cycle

Skip / reschedule / resume / order-now a billing cycle.

Body

FieldTypeRequiredNotes
typestringyesskip | reschedule | resume | order_now
cycleIndexintegernoTarget cycle (at least 0).
newBillingDatestringconditionallyRequired when type is reschedule.

An unsupported type returns 400 INVALID_PARAM.

{ "type": "skip", "cycleIndex": 5 }
{ "type": "reschedule", "cycleIndex": 5, "newBillingDate": "2026-08-01T00:00:00.000Z" }

Response

{ "success": true, "data": { } }

Upcoming-order lines

Update an upcoming-order line

PUT /subscriptions/{contractId}/upcoming-orders/{cycleIndex}/lines/{lineId}

Edit a line on a specific upcoming order/cycle.

Path: contractId, cycleIndex (integer), lineId.

Body

FieldTypeRequiredNotes
input.quantityintegernoPositive.
input.sellingPlanIdstringno
input.variantIdintegerno
{ "input": { "quantity": 2 } }

Response

{ "success": true, "data": { } }

Remove an upcoming-order line

DELETE /subscriptions/{contractId}/upcoming-orders/{cycleIndex}/lines/{lineId}

Remove a line from a specific upcoming order/cycle. No body.

Response

{ "success": true }

Add a one-off product to an upcoming order

POST /subscriptions/{contractId}/upcoming-orders/products

Body

FieldTypeRequiredNotes
cycleIndexintegeryesTarget cycle (at least 0).
linesarrayyesAt least 1 line { variantId, quantity, productId?, sellingPlanId? }.
{ "cycleIndex": 5, "lines": [ { "variantId": 4455667788, "quantity": 1 } ] }

Response

{ "success": true, "data": { } }

Discount code

Apply a discount code

POST /subscriptions/{contractId}/discount

Body

FieldTypeRequiredNotes
discountCodestringyesThe code to redeem.
applyToOrdersmixednoScope hint (all | next).
isManualbooleannoManual vs automatic apply.
{ "discountCode": "SAVE10" }

Response

{ "success": true, "data": { } }

Remove an applied discount

DELETE /subscriptions/{contractId}/discount

Body

FieldTypeRequiredNotes
discountIdstringnoWhich applied discount to remove.
isManualbooleanno

Response

{ "success": true }

Errors

HTTPCodeWhen
400VALIDATION_FAILEDBody fails validation.
400INVALID_PARAMBad path param, or unsupported billing-cycle type.
401AUTH_REQUIRED / SESSION_EXPIRED / INVALID_APP_CREDENTIALSMissing/invalid session or app credentials.
403SHOP_MISMATCHApp shop and session shop differ.
403FORBIDDENAction disabled by the merchant (e.g. subscription-discount).
404NOT_FOUNDContract does not exist or is not owned by the customer (IDOR-safe).
429RATE_LIMITEDRate limit exceeded; see Retry-After, retryAfterSeconds, scope.
500INTERNAL_ERRORGeneric server error.

Error envelope:

{ "success": false, "error": { "code": "NOT_FOUND", "message": "..." } }
Product
Install on ShopifyWebsitePricing
Resources
DocumentationGetting StartedFAQsIntegrations
Company
Avada GroupContact Support
© 2026 Avada Group. All rights reserved.