Super Simple Co / API referencev1 https://subscriptions.super-simple.co/api/v1
Help center Install

Quickstart

Read and change your subscriptions from your own systems, and get a signed message whenever one changes. Built for headless storefronts, ERPs, warehouse tools and anything else that should not need someone to open the app.

  1. Ask for access. The API is switched on store by store while it is new. If Settings > Integrations > API in the app says to contact support, message us in the app's live chat and we will turn it on.
  2. Create a token. Open Settings > Integrations > API, name the token after whatever will use it, tick what it may do (see scopes) and click Create token. The token is shown once, so copy it straight away.
  3. Make a call. Send the token as a bearer token. The example on the right checks it works.

Server side only. Never put a token in a theme, a browser or a mobile app. Anyone who finds it can change your subscriptions. No CORS headers are sent, on purpose.

Request
curl "https://subscriptions.super-simple.co/api/v1/ping" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/ping", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/ping",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "ok": true,
  "shop_domain": "your-store.myshopify.com",
  "token_name": "ERP sync",
  "scopes": ["subscriptions:read", "subscriptions:write"]
}

Authentication

Send your token in the Authorization header of every request. A token belongs to one store and can only ever see that store's subscriptions. A subscription id from another store behaves exactly like one that does not exist.

A missing, wrong, revoked or expired token all get the same 401, so guesses cannot tell which tokens are real.

Give each system its own token so you can revoke one without touching the rest. Revoke on the settings page stops a token on its next request. We never store the secret, so a lost token cannot be shown again: revoke it and create a new one.

Header
Authorization: Bearer sss_live_<tokenId>_<secret>

Scopes

A token carries only the permissions you ticked. A call that needs one the token lacks gets 403 forbidden.

ScopeAllows
subscriptions:readListing and fetching subscriptions
subscriptions:writePause, resume, cancel, skip, reschedule, change plan and products
webhooks:readListing endpoints, topics, samples and deliveries
webhooks:writeCreating, changing, deleting and testing endpoints

Conventions

  • JSON. Send Content-Type: application/json on bodies. Responses are JSON with Cache-Control: no-store.
  • snake_case keys. Dates are ISO 8601 in UTC. Money is a string amount with a currency_code.
  • Ids are numbers as strings, with the Shopify GraphQL id alongside as admin_graphql_api_id. Wherever an id goes in a path or body, send the bare number or the full GID.
  • Every change replies with the subscription as it now stands, so you never need a read straight after a write.
  • The shape only grows. We add fields, we do not rename or remove them, so ignore keys you do not recognise.
  • Request ids. Every response carries X-Request-Id. Quote it to support and we can find that exact call.

Pagination

Lists return up to 100 items. Pass the next_page_info from one response as page_info on the next. When has_next_page is false you have everything.

Paginated response
{
  "subscriptions": [ ... ],
  "page_info": {
    "has_next_page": true,
    "next_page_info": "eyJsYXN0X2lkIjo..."
  }
}

Rate limits

Each token has a short burst allowance (per 10 seconds) and a daily one. All of a store's tokens also share a ceiling, because every call spends your store's Shopify allowance. Every store starts on the standard allowance, shown when you create a token. Ask us if your integration needs more.

TierPer 10 secondsPer day
basic10200
standard20500
high401,000
All of a store's tokens, together603,000

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, describing whichever limit you will hit first. Going over returns 429 rate_limited with a Retry-After header in seconds. Wait that long, then retry.

Prefer webhooks to polling. A subscription that changes tells you, and costs you no calls.

Response headers
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 17
X-RateLimit-Reset: 1758300010
X-Request-Id: 3f2b8c1e-6a4d-4e0b-9c7a-1d5e8f2a9b34
Rate limited429
Retry-After: 6

{
  "error": {
    "code": "rate_limited",
    "message": "This token has used its API allowance. Retry after the reset, or use a higher tier."
  }
}

Retrying safely

If a request times out you cannot know whether it happened. Add an Idempotency-Key header (any unique string up to 255 characters, a UUID is ideal) to anything that changes a subscription. A retry with the same key returns the first answer instead of doing the work twice, with X-SSS-Idempotent-Replayed: true.

SituationAnswer
First use of the keyThe change runs
Same key, same requestThe stored response
Same key, different request422. We refuse rather than let you believe the second change happened
Same key, first still running409. Retry shortly

Keys are per token and last 24 hours. A 5xx answer is not remembered, so retrying after our error runs the change for real. The header is ignored on GET.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/pause" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f"

Errors

Every error has the same shape, with a stable code to switch on and a message for people.

HTTPcodeMeaning
400invalid_requestA body, id or filter is missing or malformed. The message says which.
401unauthorizedThe token is missing, wrong, revoked or expired.
403forbiddenThe token lacks the scope, or the API is not on for this store.
404not_foundNo such subscription, line, endpoint or path.
405method_not_allowedThat path does not take that HTTP method.
409conflictA state the request cannot have, for example skipping an order that is already skipped.
429rate_limitedSlow down. See Retry-After.
500internal_errorOur side, or Shopify's. Safe to retry with an idempotency key.
Error response404
{
  "error": {
    "code": "not_found",
    "message": "No subscription with that id."
  }
}

The subscription object

Every subscription endpoint, and every webhook, returns subscriptions in this shape.

  • statusstring

    ACTIVE, PAUSED, CANCELLED, EXPIRED or FAILED. Shopify's own values.

  • next_billing_dateISO 8601

    When the next order is due. null when there is none.

  • lines[].line_discounted_pricemoney

    The whole line after discounts, not per unit.

  • lines[].custom_attributesarray

    Properties the shopper or you set on the line. The app's own bookkeeping keys are left out.

  • lines[].bundleobject or null

    { "name", "role" } where role is "buy" for a qualifying item or "reward" for the discounted or free one.

  • bundles[]array

    The same grouping as a list: { "name", "line_ids", "reward_line_ids" }, so you can act on a whole bundle without matching lines yourself.

  • appobject

    What the app knows that Shopify does not: renewals completed, whether the next order is skipped, pause history, the scheduled resume date, and Shopify's projected next_order totals (null until one has been calculated).

Subscription
{
  "id": "1000001",
  "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
  "status": "ACTIVE",
  "created_at": "2026-08-01T09:30:00.000Z",
  "next_billing_date": "2026-11-01T09:30:00.000Z",
  "currency_code": "USD",
  "customer": {
    "id": "2000002",
    "admin_graphql_api_id": "gid://shopify/Customer/2000002",
    "email": "customer@example.com",
    "display_name": "Sample Customer",
    "locale": "en"
  },
  "note": null,
  "discounts": [
    {
      "id": "3000003",
      "title": "10% off for 3 orders",
      "type": "MANUAL",
      "target_type": "LINE_ITEM",
      "recurring_cycle_limit": 3,
      "value": { "type": "percentage", "percentage": 10 }
    }
  ],
  "delivery_price": { "amount": "4.00", "currency_code": "USD" },
  "billing_policy": {
    "interval": "MONTH",
    "interval_count": 1,
    "min_cycles": null,
    "max_cycles": null
  },
  "delivery_policy": { "interval": "MONTH", "interval_count": 1 },
  "lines": [
    {
      "id": "4000004",
      "admin_graphql_api_id": "gid://shopify/SubscriptionLine/4000004",
      "title": "House blend",
      "variant_title": "250g",
      "variant_id": "5000005",
      "product_id": "6000006",
      "quantity": 1,
      "selling_plan_id": "7000007",
      "selling_plan_name": "Every month",
      "current_price": { "amount": "18.00", "currency_code": "USD" },
      "line_discounted_price": { "amount": "16.20", "currency_code": "USD" },
      "custom_attributes": [{ "key": "Gift message", "value": "Enjoy!" }],
      "bundle": null
    }
  ],
  "bundles": [],
  "app": {
    "renewals": 2,
    "next_order_skipped": false,
    "pause_count": 0,
    "paused_at": null,
    "cancelled_at": null,
    "scheduled_resume_date": null,
    "billing_attempts": 0,
    "next_order": null
  }
}

List subscriptions

GET/subscriptions

Needs the subscriptions:read scope.

Returns your subscriptions, newest first, in pages of up to 100.

Query parameters

  • limitinteger

    1 to 100. Default 25.

  • page_infostring

    The next_page_info from the previous page.

  • statusstring

    One or more of ACTIVE, PAUSED, CANCELLED, EXPIRED, FAILED, comma separated.

  • customer_idstring

    A Shopify customer id.

  • customer_emailstring

    An exact, case insensitive match. Nobody with that address is an empty list, not an error. Cannot be combined with customer_id. Costs one extra Shopify call.

  • created_at_minISO 8601

    Only subscriptions created at or after this time.

  • created_at_maxISO 8601

    Only subscriptions created at or before this time.

Only filters Shopify can answer directly are offered. That keeps a scripted integration from using up your store's Shopify allowance.

Request
curl "https://subscriptions.super-simple.co/api/v1/subscriptions?status=ACTIVE,PAUSED&limit=50" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions?status=ACTIVE,PAUSED&limit=50", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/subscriptions",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
    params={"status": "ACTIVE,PAUSED", "limit": "50"},
)
data = res.json()
Response200
{
  "subscriptions": [
    { "id": "1000001", "status": "ACTIVE", ... }
  ],
  "page_info": {
    "has_next_page": true,
    "next_page_info": "eyJsYXN0X2lkIjo..."
  }
}

Get a subscription

GET/subscriptions/:id

Needs the subscriptions:read scope.

Returns one subscription. An id that belongs to another store returns 404, the same as one that does not exist.

Request
curl "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Pause a subscription

POST/subscriptions/:id/pause

Needs the subscriptions:write scope.

Pauses billing. No orders are created until it is resumed.

Body parameters

  • reasonstring

    Kept in the subscription's timeline.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/pause" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"reason":"Customer is travelling"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/pause", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "reason": "Customer is travelling"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/pause",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "reason": "Customer is travelling"
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Resume a subscription

POST/subscriptions/:id/resume

Needs the subscriptions:write scope.

Resumes a paused subscription. It never charges at once. Without a date, a billing date that has already passed moves to the next cycle and any other date is kept.

Body parameters

  • next_billing_dateISO 8601

    When the next order should be created. Must be in the future.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/resume" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"next_billing_date":"2026-12-01T09:00:00Z"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/resume", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "next_billing_date": "2026-12-01T09:00:00Z"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/resume",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "next_billing_date": "2026-12-01T09:00:00Z"
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Cancel a subscription

POST/subscriptions/:id/cancel

Needs the subscriptions:write scope.

Cancels the subscription. This cannot be undone through the API.

Body parameters

  • reasonstring

    Kept in the subscription's timeline.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/cancel" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"reason":"Requested by email"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "reason": "Requested by email"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/cancel",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "reason": "Requested by email"
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Bring back a skipped order

POST/subscriptions/:id/unskip

Needs the subscriptions:write scope.

Undoes a skip. Takes no body. Returns 409 conflict if the next order is not skipped.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/unskip" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/unskip", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/unskip",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Change the next billing date

POST/subscriptions/:id/reschedule

Needs the subscriptions:write scope.

Moves the next order to another date and time.

Body parameters

  • next_billing_dateISO 8601required

    The new date and time of the next order.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/reschedule" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"next_billing_date":"2026-11-15T09:00:00Z"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/reschedule", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "next_billing_date": "2026-11-15T09:00:00Z"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/reschedule",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "next_billing_date": "2026-11-15T09:00:00Z"
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Change the delivery frequency

POST/subscriptions/:id/plan

Needs the subscriptions:write scope.

Moves the subscription to another selling plan. A plan the merchant has switched off for the API (Offers, the option, Where it is offered) is refused with 400, and so is a plan whose bulk minimum the current quantities do not meet.

Body parameters

  • selling_plan_idstringrequired

    The selling plan to move to. The current one is on lines[].selling_plan_id.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/plan" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"selling_plan_id":"7000008"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/plan", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "selling_plan_id": "7000008"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/plan",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "selling_plan_id": "7000008"
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Add products

POST/subscriptions/:id/lines

Needs the subscriptions:write scope.

Adds products to the subscription.

Body parameters

  • variant_idsarray of idsrequired

    Product variant ids to add.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"variant_ids":["5000005","5000006"]}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "variant_ids": [
      "5000005",
      "5000006"
    ]
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "variant_ids": [
        "5000005",
        "5000006"
      ]
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Change a product

PATCH/subscriptions/:id/lines/:lineId

Needs the subscriptions:write scope.

Changes one line. Send quantity or variant_id, never both. To swap the product and change the quantity, make two calls: swap first, then set the quantity.

Body parameters

  • quantityinteger

    The new quantity. A whole number of 1 or more.

  • variant_idstring

    Swap this line to another variant.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X PATCH "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines/4000004" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f" \
  -d '{"quantity":3}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines/4000004", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
  },
  body: JSON.stringify({
    "quantity": 3
  }),
});
const data = await res.json();
import os, requests

res = requests.patch(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines/4000004",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
        "Idempotency-Key": "9f1c2a7e-4b0d-4f2a-9c3e-8a1b2c3d4e5f",
    },
    json={
      "quantity": 3
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Remove a product

DELETE/subscriptions/:id/lines/:lineId

Needs the subscriptions:write scope.

Removes a line. No body.

Accepts an Idempotency-Key header. Answers with the subscription as it now stands.

Request
curl -X DELETE "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines/4000004" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines/4000004", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.delete(
    "https://subscriptions.super-simple.co/api/v1/subscriptions/1000001/lines/4000004",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "subscription": {
    "id": "1000001",
    "admin_graphql_api_id": "gid://shopify/SubscriptionContract/1000001",
    "status": "ACTIVE",
    "next_billing_date": "2026-11-01T09:30:00.000Z",
    ...
  }
}

Webhooks

Rather than asking us what changed, have us tell you. Register an HTTPS URL and the topics you care about, and we send a signed POST each time one happens. The body contains the subscription exactly as the API returns it.

  • Your URL must use https on port 443 and be reachable from the internet. Addresses that point inside a network (private ranges, localhost, .local) are refused, and redirects are not followed.
  • Delivery is at least once. The same event can arrive twice, so ignore a repeated X-SSS-Delivery-Id.
  • Answer with any 2xx quickly and do your work afterwards. Anything else counts as a failure and is retried.

Set endpoints up in Settings > Integrations > API in the app, or through the endpoints below. Send yourself a test event before real ones flow.

The payload

Every delivery has the same envelope, plus an object per topic under data. The envelope only grows, so ignore keys you do not know.

occurred_at is when the event happened. state_as_of is when we read the subscription. A pause that was resumed a minute later is still delivered as a pause, with a subscription that reads active. Use event for what changed and data.subscription for what is true now. created_at equals occurred_at and is kept for older integrations.

event.initiator says who made the change, for example customer_portal, shopify_admin or api. previous_status and new_status appear on status changes.

Headers

  • X-SSS-Topic

    The topic, for example subscription.paused.

  • X-SSS-Shop-Domain

    Your myshopify.com domain.

  • X-SSS-Delivery-Id

    Unique per delivery. Use it to ignore repeats.

  • X-SSS-Event-Id

    The event this delivery carries.

  • X-SSS-Timestamp

    Unix seconds when we signed it.

  • X-SSS-Hmac-SHA256

    The base64 signature. See verifying.

Delivery body
{
  "id": "evt_...",
  "topic": "subscription.paused",
  "created_at": "2026-10-30T12:00:00.000Z",
  "occurred_at": "2026-10-30T12:00:00.000Z",
  "state_as_of": "2026-10-30T12:00:07.000Z",
  "shop_domain": "your-store.myshopify.com",
  "api_version": "v1",
  "event": {
    "initiator": "customer_portal",
    "reason": { "key": "too_many", "text": "I have too much at home" },
    "previous_status": "ACTIVE",
    "new_status": "PAUSED"
  },
  "data": {
    "subscription": { "id": "1000001", "status": "PAUSED", ... }
  }
}

Topics

Topic names never change once published, so it is safe to switch on them in your code. Fetch the live list from GET /webhook_topics and a real example of any from GET /webhook_topics/:topic/sample.

TopicSent when
subscription.createdA subscription is created
subscription.updatedA subscription is changed in some other way
subscription.pausedIt is paused
subscription.resumedIt is resumed, by hand or automatically
subscription.cancelledIt is cancelled, by hand or automatically
subscription.failedIt failed after its payment retries ran out
subscription.completedIt reached its last order and expired
subscription.renewedA renewal order was created
subscription.billing_retry_scheduledA payment was declined and another attempt is scheduled
subscription.renewal_upcomingA renewal is coming up, at the customer reminder's lead time. data.renewal has next_billing_date and days_until_renewal
subscription.next_billing_date_changedThe next billing date moved
subscription.order_skippedAn order was skipped
subscription.order_unskippedA skipped order was brought back
subscription.line_changedA product was added, removed, swapped or its quantity changed
subscription.plan_changedThe delivery frequency changed
subscription.price_changedProduct prices were updated. data.price_change.lines[] lists the lines
subscription.payment_method_updatedThe payment method was replaced
subscription.shipping_address_updatedThe shipping address changed
subscription.pause_reason_collectedA customer gave a reason for pausing. data.pause has reason and comment
retention.offer_presentedA customer was shown a retention offer. data.retention has incentive_type, incentive_value, offered_during
retention.offer_acceptedThey accepted it (same fields)
retention.offer_declinedThey declined it. data.retention.reason
retention.incentive_appliedAn accepted incentive was applied. Adds recurring_cycle_limit
loyalty.reward_appliedA loyalty reward was applied. data.loyalty has cycle and reward_types[]
journey.step_appliedA journey step changed a subscription after an order. data.journey has id, name, order_number, steps[]
notification.sentThe app sent a customer a notification. data.notification has type and channel. The address is never included

There is no topic for a single failed payment attempt. A failure shows up as subscription.billing_retry_scheduled and, if the retries run out, subscription.failed.

Verifying signatures

Anyone who knows your URL can post to it, so check every delivery really came from us before you act on it. The signature is the base64 HMAC SHA256 of timestamp + "." + raw body, keyed with your endpoint's secret.

  • The key is the whole secret as we showed it, including the sss_whsec_ prefix. Do not strip or decode it.
  • Use the raw body exactly as received. Re-encoding parsed JSON changes the bytes and the signature will not match.
  • Reject any delivery whose timestamp is more than five minutes from your clock. The timestamp is signed, so this stops a captured delivery being replayed.
  • Compare in constant time.
Verify a delivery
# Reproduce a signature by hand to debug a mismatch.
# SECRET is the whole string, including the sss_whsec_ prefix.
printf '%s.%s' "$TIMESTAMP" "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" -binary \
  | base64
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.SUPR_WEBHOOK_SECRET; // sss_whsec_...
const app = express();

// Keep the raw bytes: express.json() would re-encode them.
app.post("/hooks/supr", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("x-sss-timestamp");
  const signature = req.get("x-sss-hmac-sha256") ?? "";
  const raw = req.body.toString("utf8");

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.${raw}`)
    .digest("base64");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  res.sendStatus(200); // answer first, work after
  const event = JSON.parse(raw);
  // switch (event.topic) { ... }
});
import base64, hashlib, hmac, os, time
from flask import Flask, request, abort

SECRET = os.environ["SUPR_WEBHOOK_SECRET"].encode()  # sss_whsec_...
app = Flask(__name__)

@app.post("/hooks/supr")
def supr():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    timestamp = request.headers.get("X-SSS-Timestamp", "")
    signature = request.headers.get("X-SSS-Hmac-SHA256", "")

    if abs(time.time() - int(timestamp or 0)) > 300:
        abort(400)

    digest = hmac.new(SECRET, timestamp.encode() + b"." + raw, hashlib.sha256).digest()
    if not hmac.compare_digest(base64.b64encode(digest).decode(), signature):
        abort(401)

    event = request.get_json()
    # handle event["topic"]
    return "", 200

Retries

A delivery that fails (a non 2xx answer, a timeout, or a connection error) is retried after 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then marked dead. That is about eight attempts over roughly 21 hours.

If an endpoint fails 20 times in a row we switch it off, so a retired URL is not retried forever. The endpoint shows disabled_at and the last error. Fix the receiver, then turn it back on with PATCH and { "enabled": true }, or Turn on in the app.

Webhook endpoints API

Manage where events go from your own code. The same things are available on the settings page in the app.

List topics

GET/webhook_topics

Needs the webhooks:read scope.

Every topic you can subscribe to, always current.

Request
curl "https://subscriptions.super-simple.co/api/v1/webhook_topics" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_topics", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/webhook_topics",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "topics": [
    "subscription.created",
    "subscription.updated",
    "subscription.paused",
    ...
  ]
}

Get a sample payload

GET/webhook_topics/:topic/sample

Needs the webhooks:read scope.

An example of what a topic delivers, built by the same code as a real delivery from an invented subscription.

Request
curl "https://subscriptions.super-simple.co/api/v1/webhook_topics/subscription.paused/sample" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_topics/subscription.paused/sample", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/webhook_topics/subscription.paused/sample",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "topic": "subscription.paused",
  "payload": {
    "id": "evt_sample",
    "topic": "subscription.paused",
    ...
  }
}

List endpoints

GET/webhook_endpoints

Needs the webhooks:read scope.

Your endpoints, newest first.

Request
curl "https://subscriptions.super-simple.co/api/v1/webhook_endpoints" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{ "webhook_endpoints": [ {
    "id": "66f1c0a2b7d3e4a5c6d7e8f9",
    "url": "https://example.com/hooks/supr",
    "topics": ["subscription.paused", "subscription.cancelled"],
    "description": "Warehouse system",
    "enabled": true,
    "created_at": "2026-10-30T10:00:00.000Z",
    "last_success_at": null,
    "last_error_at": null,
    "last_error": null,
    "consecutive_failures": 0,
    "disabled_at": null
  } ] }

Create an endpoint

POST/webhook_endpoints

Needs the webhooks:write scope.

Registers a URL to receive events. The response includes the signing secret, shown once. Store it somewhere safe, or rotate it later.

Body parameters

  • urlstringrequired

    Where to send events. HTTPS only.

  • topicsarrayrequired

    One or more topics. An unknown topic is refused and the message lists the valid ones.

  • descriptionstring

    A note to yourself.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/webhook_endpoints" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/supr","topics":["subscription.paused","subscription.cancelled"],"description":"Warehouse system"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "https://example.com/hooks/supr",
    "topics": [
      "subscription.paused",
      "subscription.cancelled"
    ],
    "description": "Warehouse system"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
    json={
      "url": "https://example.com/hooks/supr",
      "topics": [
        "subscription.paused",
        "subscription.cancelled"
      ],
      "description": "Warehouse system"
    },
)
data = res.json()
Response200
{
  "webhook_endpoint": {
    "id": "66f1c0a2b7d3e4a5c6d7e8f9",
    "url": "https://example.com/hooks/supr",
    "topics": ["subscription.paused", "subscription.cancelled"],
    "description": "Warehouse system",
    "enabled": true,
    "created_at": "2026-10-30T10:00:00.000Z",
    "last_success_at": null,
    "last_error_at": null,
    "last_error": null,
    "consecutive_failures": 0,
    "disabled_at": null
  },
  "secret": "sss_whsec_..."
}

Get an endpoint

GET/webhook_endpoints/:id

Needs the webhooks:read scope.

One endpoint, with its health: when it last succeeded or failed and how many failures in a row.

Request
curl "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{ "webhook_endpoint": {
    "id": "66f1c0a2b7d3e4a5c6d7e8f9",
    "url": "https://example.com/hooks/supr",
    "topics": ["subscription.paused", "subscription.cancelled"],
    "description": "Warehouse system",
    "enabled": true,
    "created_at": "2026-10-30T10:00:00.000Z",
    "last_success_at": null,
    "last_error_at": null,
    "last_error": null,
    "consecutive_failures": 0,
    "disabled_at": null
  } }

Change an endpoint

PATCH/webhook_endpoints/:id

Needs the webhooks:write scope.

Send any of the fields below. Turning an endpoint back on clears its failure count.

Body parameters

  • urlstring

    A new HTTPS URL.

  • topicsarray

    Replaces the whole list of topics.

  • descriptionstring

    A note to yourself.

  • enabledboolean

    false pauses deliveries. true resumes them.

Request
curl -X PATCH "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"topics":["subscription.cancelled"],"enabled":true}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "topics": [
      "subscription.cancelled"
    ],
    "enabled": true
  }),
});
const data = await res.json();
import os, requests

res = requests.patch(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
    json={
      "topics": [
        "subscription.cancelled"
      ],
      "enabled": True
    },
)
data = res.json()
Response200
{ "webhook_endpoint": {
    "id": "66f1c0a2b7d3e4a5c6d7e8f9",
    "url": "https://example.com/hooks/supr",
    "topics": ["subscription.paused", "subscription.cancelled"],
    "description": "Warehouse system",
    "enabled": true,
    "created_at": "2026-10-30T10:00:00.000Z",
    "last_success_at": null,
    "last_error_at": null,
    "last_error": null,
    "consecutive_failures": 0,
    "disabled_at": null
  } }

Delete an endpoint

DELETE/webhook_endpoints/:id

Needs the webhooks:write scope.

Stops all deliveries to it.

Request
curl -X DELETE "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.delete(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{ "deleted": true, "id": "66f1c0a2b7d3e4a5c6d7e8f9" }

Rotate the signing secret

POST/webhook_endpoints/:id/rotate_secret

Needs the webhooks:write scope.

Issues a new secret for this endpoint only. The old one stops working at once, so update your receiver first or accept a short gap. The new secret is shown once.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/rotate_secret" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/rotate_secret", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/rotate_secret",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "webhook_endpoint": {
    "id": "66f1c0a2b7d3e4a5c6d7e8f9",
    "url": "https://example.com/hooks/supr",
    "topics": ["subscription.paused", "subscription.cancelled"],
    "description": "Warehouse system",
    "enabled": true,
    "created_at": "2026-10-30T10:00:00.000Z",
    "last_success_at": null,
    "last_error_at": null,
    "last_error": null,
    "consecutive_failures": 0,
    "disabled_at": null
  },
  "secret": "sss_whsec_..."
}

Send a test event

POST/webhook_endpoints/:id/test

Needs the webhooks:write scope.

Queues an example through the real delivery path, with the same signing and retries. The payload carries "test": true and an id starting evt_test_. At most five tests can wait on an endpoint, and a switched off endpoint refuses with 409.

Body parameters

  • topicstring

    Any topic. Defaults to the endpoint's first.

Request
curl -X POST "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/test" \
  -H "Authorization: Bearer $SUPR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"topic":"subscription.paused"}'
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "topic": "subscription.paused"
  }),
});
const data = await res.json();
import os, requests

res = requests.post(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/test",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
    json={
      "topic": "subscription.paused"
    },
)
data = res.json()
Response200
{
  "delivery": {
    "id": "66f1c0a2b7d3e4a5c6d7e900",
    "topic": "subscription.paused",
    "status": "pending",
    "test": true
  }
}

List deliveries

GET/webhook_endpoints/:id/deliveries

Needs the webhooks:read scope.

The 50 most recent deliveries to this endpoint, with their status. status is pending, delivering, delivered or dead.

Request
curl "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/deliveries" \
  -H "Authorization: Bearer $SUPR_TOKEN"
const res = await fetch("https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/deliveries", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SUPR_TOKEN}`,
  },
});
const data = await res.json();
import os, requests

res = requests.get(
    "https://subscriptions.super-simple.co/api/v1/webhook_endpoints/1000001/deliveries",
    headers={
        "Authorization": f"Bearer {os.environ['SUPR_TOKEN']}",
    },
)
data = res.json()
Response200
{
  "deliveries": [
    {
      "id": "66f1c0a2b7d3e4a5c6d7e900",
      "topic": "subscription.paused",
      "status": "delivered",
      "attempts": 1,
      "last_status_code": 200,
      "last_error": null,
      "next_attempt_at": null,
      "delivered_at": "2026-10-30T10:01:04.000Z",
      "created_at": "2026-10-30T10:01:02.000Z"
    }
  ]
}

Questions

Do I need the API to move many renewal dates at once?

Not for a one off change. In the app's Subscriptions list, filter, tick the subscriptions and use the Reschedule bulk action. The API is for changes your own systems make regularly.

Can another app read subscription data?

Shopify only lets the app that created a subscription read it, so other apps cannot see ours directly. Connect that system to this API, or have it read the order and customer tags the app can add.

Is there an official client library?

Not yet. The API is plain JSON over HTTPS, so any HTTP client works.

Is there a sandbox?

Use a development store. The API works the same there and never touches your live store.

Can I use Zapier instead?

Yes. Connect it with one click in Settings > Integrations > Zapier. No token needed.

Something is not behaving.

Message us in the app's live chat and quote the X-Request-Id from the response. For a general tour, see the Using the API help article.