Building subscription management into your customer account pages

There is no one way of adding payment-method and subscription management interfaces, since each merchant will have their own requirements. Some may want to to limit the scope of the update (just changing the frequency and shipping address of a subscription, for example), while others may prefer to give the customer complete ownership of their subscription data (adding/removing items and cancelling, pausing and duplicating subscriptions).

Submarine exposes a Customer API to allow theme devs to handle subscriptions and payment methods, and abstracts a lot of the communication with it via a JS library — Submarine JS.

Pause Subscription

A paused subscription is placed in an indefinite (reversible) hiatus.

  • No subscription orders will be processed while a subscription is paused.
  • Paused subscriptions can be resumed.
  • Cancelled subscriptions cannot be paused.

Request Payload

{
  "subscription": {
    "status": "paused"
  }
}

Unsuccessful response

An invalid transition of status.

{
  "errors": [
    {
      "detail": "Cannot transition from 'cancelled' to 'paused'",
      "source": { 
        "pointer": "/subscription/status" 
      },
      "status": "422"
    }
  ]
}

Cancel subscription

A cancelled subscription is placed in a permanent (irreversible) hiatus.

  • No subscription orders will be processed while a subscription is cancelled.
  • Cancelling subscription is a one-way action - cancelled subscriptions cannot be resumed.

Request payload

{
  "subscription": {
    "status": "cancelled"
  }
}

Cancel subscription with reason

A cancelled subscription is placed in a permanent (irreversible) hiatus.

  • No subscription orders will be processed while a subscription is cancelled.
  • Cancelling a subscription is a one-way action — cancelled subscriptions cannot be resumed.
  • A user-supplied string can be passed in the optional status_reason_detail attribute — this will be recorded and displayed against the subscription, as well as passed in Shopify Flow triggers.

Request payload

{
  "subscription": {
    "status": "cancelled",
    "status_reason_detail": "I no longer want this subscription."
  }
}

Resume subscription

Customers are able to resume subscriptions via the API.

  • Only paused subscriptions can be resumed.
  • As soon as a subscription is resumed, Submarine will attempt to process any scheduled orders.

Request payload

{
  "subscription": {
    "status": "active"
  }
}

Unsuccessful response

An invalid transition of status.

{
  "errors": [
    {
      "detail": "Cannot transition from 'cancelled' to 'active'",
      "source": { 
        "pointer": "/subscription/status" 
      },
      "status": "422"
    }
  ]
}

Set next order date

Customers are allowed to arbitrarily set the date that the next subscription order should be processed.

  • The next order date should be in the future.
  • The entire schedule for future subscription orders will be updated to reflect the new date and the subscription frequency.
  • The date is actually a time, and should be in ISO 8601 format.
  • The date can be updated on its own, without changing the frequency.

Request payload

{
  "subscription": {
    "next_order_at": "2020-12-01T06:02:06Z"
  }
}

Unsuccessful responses

The supplied date is invalid, e.g. 'Next Wednesday'.

{
  "errors": [
    {
      "detail": "Invalid timestamp: 'Next Wednesday'",
      "source": { 
        "pointer": "/subscription/next_order_at" 
      },
      "status": "422"
    }
  ]
}

The supplied date is in the past.

{
  "errors": [
    {
      "detail": "Next order date cannot be in the past",
      "source": { 
        "pointer": "/subscription/next_order_at" 
      },
      "status": "422"
    }
  ]
}

Update subscription frequency

Customers can modify the frequency at which their subscription orders are generated.

  • Both the frequency and the next order date should be supplied in the request payload.
  • Frequencies should be supplied in the {magnitude}_{unit} format, e.g. 7_days, 1_month. Supported units (both singular and plural) are:
    • day/days
    • week/weeks
    • month/months
    • year/years
  • Non-production environments also support hourly frequencies, to help with UAT.
  • The same rules apply as regards the validation of the next order date.

Request payload

{
  "subscription": {
    "frequency": "7_days",
    "next_order_at": "2020-12-01T06:02:06Z"
  }
}

Unsuccessful responses

No next order date supplied

{
  "errors": [
    {
      "detail": "Must be supplied when changing frequency",
      "source": { 
        "pointer": "/subscription/next_order_at" 
      },
      "status": "422"
    }
  ]
}

Supplied frequency is not supported by Submarine.

{
  "errors": [
    {
      "detail": "Unsupported frequency: 2_decades",
      "source": { 
        "pointer": "/subscription/frequency" 
      },
      "status": "422"
    }
  ]
}

Update customer details

Customer contact details are stored against a subscription, and can be updated.

  • Submarine does not send transactional emails or SMS.
  • These details are scoped to the subscription, and are separate from the Shopify customer details.

Request payload

{
  "subscription": {
    "customer": {
      "email": "[email protected]",
      "name": "Submarine User",
      "phone": "0400123456"
    }
  }
}

Update payment method

Out of the box, the payment method selected at checkout will be used for all subscription orders. This can be switched to a different Submarine payment method at any time.

  • If creating a new payment method, a payment token should be generated first (using your payment processor client-side library, e.g. Stripe Elements or Braintree Hosted Fields).
  • That token should be supplied to Submarine's Payment Methods API, in order to create a new Submarine payment method.
  • The Submarine payment method ID should be supplied to the update-subscription endpoint.

Request payload

{
  "subscription": {
    "payment_method_id": 12345678
  }
}

Unsuccessful response
The supplied payment method cannot be found, or it is inactive.

{
  "errors": [
    {
      "detail": "Cannot find active customer payment method: 12345678",
      "source": { 
        "pointer": "/subscription/payment_method_id" 
      },
      "status": "422"
    }
  ]
}

Update notes

A standard note, as well as more sophisticated note attributes, can be attached to a subscription.

  • The note supports Markdown.
  • The note attributes should be name/value pairs. Some or all of them can be configured to propagate to the generated subscription orders.

Request payload

{
  "subscription": {
    "note": "This is a **bold note**.",
    "note_attributes": [
      {
        "name": "delivery_date",
        "value": "2020-12-25"
      }
    ]
  }
}

Update nickname

Customers can give subscriptions a nickname, to help them more easily distinguish between multiple active subscriptions.

Request payload

{
  "subscription": {
    "nickname": "Milo's monthly snacks"
  }
}

Update line items

The price and quantity of line items can be updated.

  • Only quantity and variant ID are required fields.
  • Line-item properties can be set on subscription items. Some or all of them can be configured to propagate to the generated subscription orders.
  • If your Submarine instance is configured to respect a variant's availability status, this will be policed when setting line items via the API.
  • Line items are identified by variant ID.
  • Subscription items that are not referenced in the request payload are left untouched.
  • To remove a subscription item, set the quantity to zero.
  • When a subscription's items are updated, Submarine will reach out to Shopify to determine if the change has had an affect on the available shipping rates.
  • The title and SKU of the subscription line item will be updated to the current title and SKU of the Shopify variant.
  • If you omit the properties attribute for a line item that has existing properties, those properties will be retained. Explicitly provide a properties attribute with an empty array value [] to clear the properties on the line item.

Request payload

{
  "subscription": {
    "line_items": [
      {
        "quantity": 2,
        "variant_id": 100001
      },
      {
        "price": 21.50,
        "quantity": 1,
        "variant_id": 100002
      },
      {
        "properties": [
          {
            "name": "inscription",
            "value": "Thanks for the cheese!"
          }
        ],
        "quantity": 1,
        "variant_id": 100003
      }
    ]
  }
}

Unsuccessful responses

The Shopify product variant cannot be found.

{
  "errors": [
    {
      "detail": "Cannot find variant: 100001",
      "source": { 
        "pointer": "/subscription/line_items/0/variant_id" 
      },
      "status": "422"
    }
  ]
}

The Shopify product variant is unavailable (e.g. the product is unpublished).

{
  "errors": [
    {
      "detail": "Variant is unavailable: 100002",
      "source": { 
        "pointer": "/subscription/line_items/1/variant_id" 
      },
      "status": "422"
    }
  ]
}

Delete line items

To delete subscription items, set the quantity to zero.

  • Deletions can be combined with non-destructive item updates.

Request payload

The subscription item for variant ID 10001 will be removed.

{
  "subscription": {
    "line_items": [
      {
        "quantity": 0,
        "variant_id": 100001
      },
      {
        "quantity": 10,
        "variant_id": 100002
      }
    ]
  }
}

Update shipping address

The shipping address is collected at checkout, but can be modified at any time.

  • Minimal validation is provided on the supplied address data to ensure that it's a Shopify friendly address.
    • Country code is checked.
    • Province code is checked against country requirements. Note that not all countries require a province code.
    • Zip is checked against country-specific patterns.
  • For country and province, only the codes need be supplied — Submarine will populate the long-form versions of each. For those countries that don't support province codes (e.g. the UK), the endpoint accepts a province attribute.
  • When the city, state or country of a shipping address is changed, Submarine will reach out to Shopify to determine if the change has had an affect on the available shipping rates for the subscription.

Request payload

{
  "subscription": {
    "shipping_method": {
      "shipping_address": {
        "first_name": "Submarine",
        "last_name": "User",
        "address1": "10 Gwynne Street",
        "city": "Cremorne",
        "province_code": "VIC",
        "zip": "3121",
        "country_code": "AU"
      }
    }
  }
}

Unsuccessful responses

Unknown country code.

{
  "errors": [
    {
      "detail": "Could not determine country from 'ZZ'",
      "source": { 
        "pointer": "/subscription/shipping_method/shipping_address/country_code" 
      },
      "status": "422"
    }
  ]
}

Missing province code for a country that requires one.

{
  "errors": [
    {
      "detail": "Addresses for 'AU' require a province code",
      "source": { 
        "pointer": "/subscription/shipping_method/shipping_address/province_code" 
      },
      "status": "422"
    }
  ]
}

Invalid zip code for supplied country.

{
  "errors": [
    {
      "detail": "Invalid zip: '12345678'",
      "source": { 
        "pointer": "/subscription/shipping_method/shipping_address/zip" 
      },
      "status": "422"
    }
  ]
}

Update shipping method

Similarly to the shipping address, a subscription's shipping method can be updated post-checkout.

  • Even though the payload accepts an array of shipping rates (for backwards compatibility), only one can be supplied.
  • The shipping-rate title must always be supplied. This should match one of the available shipping rates for the subscription.
  • Optionally, a (discounted) shipping-rate price can be supplied. This will override the price supplied by Shopify and will remain so long as the subscription's shipping address or line items are not changed.
  • To make a price override persist even when the shipping address or line items change, flag the rate as being sticky.

Request payload

{
  "subscription": {
    "shipping_method": {
      "shipping_rates": [
        {
          "discounted_price": 10.0,
          "title": "Standard Shipping"
        }
      ]
    }
  }
}

Unsuccessful responses

More than one shipping rate supplied in the array.

{
  "errors": [
    {
      "detail": "Only one shipping rate expected, received 2",
      "source": { 
        "pointer": "/subscription/shipping_rates" 
      },
      "status": "422"
    }
  ]
}

Could not find a shipping rate with the supplied title.

{
  "errors": [
    {
      "detail": "Cannot match 'Budget' against available rates: Standard, Express",
      "source": { 
        "pointer": "/subscription/shipping_rates" 
      },
      "status": "422"
    }
  ]
}

Set sticky shipping rate

In order for overridden shipping-rate prices to survive changes to shipping address and/or subscription items, it should be flagged as being sticky.

  • Sticky rates can only be reset via the Subscriptions API.

Request payload

{
  "subscription": {
    "shipping_method": {
      "sticky_rate": true
    }
  }
}

Custom fields

You can store arbitrary metadata against a subscription using custom fields. These are handled similarly to note attributes, but won't be persisted on any generated Shopify orders.

  • Updating one or more specific custom fields will not affect any other custom fields that may be attached to a subscription.
  • Once set, a custom field cannot be removed, but can be set to null.

Request payload

{
  "subscription": {
    "custom_fields": [
      {
        "name": "last_skipped_order_at",
        "value": "2021-03-25T00:00Z"
      }
    ]
  }
}