Subscriptions¶
A subscription is a billing schedule that references a Protocol. The protocol owns the regimen — the products, the customer, the pricing inputs. The subscription owns the cadence: when the next renewal order is generated (nextBillingDate), how far apart renewals are (daysSupply), and how renewal orders ship. When a renewal fires, the line items come from the protocol and the delivery cadence comes from the subscription.
This split matters for what you can change where:
- Products, quantities, pricing — change the protocol via the amendment flow. The subscription API never touches line items.
- Schedule — pause, resume, skip, and reschedule on the subscription itself.
- Cadence, shipping method,
notes,endDateand ship-to — the five fieldsPUT /v2/subscriptions/{subscriptionId}applies. See Where renewals ship. AdaysSupplychange needs the customer's acceptance first; see Changing cadence.
Each protocol can have at most one active subscription.
All subscription endpoints require a v2 API key (minted in the partner portal — v1 keys cannot call v2). Reads require the order/v2.read scope; every POST/PUT requires both order/v2.read and order/v2.write.
Two billing modes¶
How a subscription gets created — and who charges the customer — depends on your partner billing configuration:
| Juniper-managed billing | Partner-managed billing | |
|---|---|---|
| Created by | Hosted checkout after the customer pays. POST /v2/subscriptions returns 400. |
You, via POST /v2/subscriptions. |
| Renewals charged by | DailyStak, automatically, against the card saved at checkout. | You, off-platform. DailyStak asks you to confirm each renewal. |
| Failed renewal | Dunning: PAST_DUE, automatic retries, cancellation if they all fail. |
Missed confirmations: back to ACTIVE and retried; FAILED after 3 consecutive misses. |
If you are Juniper-managed, checkout creates the subscription for you — you can still pause, skip, reschedule, cancel, and change the cadence, but creation and payment are handled by the platform.
daysSupply: the cadence¶
daysSupply (1–365, default 30) is both the days of product per order and the renewal interval — there is no separate billing frequency, and no proration anywhere on the platform.
Two things to know:
- It reads back, and it can be changed. The response echoes
daysSupply,shippingMethodandcustomerId, so you can verify the cadence you display against ours instead of keeping your own copy. To move a subscriber between cadences without sending them back through checkout, see Changing cadence. - Advancement is anchored. Every renewal, skip, or generated order advances
nextBillingDatebydaysSupplydays from the previously scheduled date, not from today. The schedule never drifts because a renewal ran late — but a long-overdue subscription can become due again immediately after renewing. UsePOST /v2/subscriptions/{subscriptionId}/rescheduleto re-anchor to an exact date.
Creating a subscription (partner-managed billing)¶
The protocol and customer must already exist, the customer must be the protocol's customer, and the customer must have a shipping address on file.
curl -X POST https://api.fulfillment.juniperhealth.com/v2/subscriptions \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"protocolId": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
"customerId": "2vTgd30JFgnwLrgb383PcV9Rken",
"daysSupply": 30,
"shippingMethod": "STANDARD",
"nextBillingDate": "2026-08-24T00:00:00Z",
"notes": "VIP customer"
}'
{
"subscription": {
"id": "2zAbC9xkQ21LmNoPqRsTuVwXyZ0",
"partnerId": "2uPARTNERxxxxxxxxxxxxxxxxxx",
"pricingBreakdown": null,
"protocolId": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
"protocol": null,
"customerId": "2vTgd30JFgnwLrgb383PcV9Rken",
"daysSupply": 30,
"shippingMethod": "STANDARD",
"status": "ACTIVE",
"nextBillingDate": "2026-08-24T00:00:00Z",
"startDate": "2026-07-25T14:05:00.123456Z",
"endDate": null,
"createdAt": "2026-07-25T14:05:00.123456Z",
"updatedAt": "2026-07-25T14:05:00.123456Z",
"lastOrderId": null,
"totalOrdersGenerated": 0,
"notes": "VIP customer",
"scheduleHistory": [],
"isActive": true,
"isDueForBilling": false,
"paymentFailureCount": 0,
"lastPaymentError": null,
"renewalBlockedReason": null,
"renewalBlockedAt": null,
"renewalBlockedMessage": null
},
"orderId": null
}
The subscription is born ACTIVE — there is no pending phase. startDate defaults to now; nextBillingDate defaults to startDate plus daysSupply days. A successful create emits the subscription.created webhook event.
Failure modes: 400 if your billing is Juniper-managed, the customer has no shipping address, or the customer doesn't match the protocol's customer; 404 if the protocol or customer doesn't exist; 409 if the protocol already has an active subscription (paused, failed, or cancelled ones don't block).
The generateInitialOrder gotcha¶
"generateInitialOrder": true runs the same generation path as generate-order, which only fires when the subscription is due (nextBillingDate now or earlier). With the default nextBillingDate (one full cycle in the future) the order generation fails with a 400 — but the subscription has already been created, and in that case subscription.created is not emitted. To create a subscription and its first order in one call, set nextBillingDate explicitly to now or earlier:
{
"protocolId": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
"customerId": "2vTgd30JFgnwLrgb383PcV9Rken",
"daysSupply": 30,
"nextBillingDate": "2026-07-25T00:00:00Z",
"generateInitialOrder": true
}
The 201 response then carries the new order's ID in orderId, and nextBillingDate has advanced one cycle. If you got the 400, the subscription exists — fix the date with reschedule (or just call generate-order once it's due) rather than creating a second one.
Lifecycle¶
Statuses: PENDING, ACTIVE, PAST_DUE, PAUSED, CANCELLED, FAILED, AWAITING_PARTNER.
PENDING— checkout-created subscription awaiting purchase completion.ACTIVE— renewing on schedule.AWAITING_PARTNER— partner-managed renewal is due; DailyStak is waiting for yourconfirm-renewal.PAST_DUE— Juniper-managed renewal charge failed; dunning retries are running.PAUSED— renewals suspended until you resume.FAILED— three consecutive missed partner-managed confirmations. A Juniper-managed subscription that exhausts its payment retries isCANCELLED, neverFAILED.CANCELLED— terminal, and where exhausted Juniper-managed dunning ends. There is no un-cancel; create a new subscription to restart.
Which actions are valid from which status:
| Action | Valid from | Otherwise |
|---|---|---|
PUT /v2/subscriptions/{subscriptionId} |
any status | — |
POST .../pause |
ACTIVE |
400 |
POST .../resume |
PAUSED |
400 |
POST .../cancel |
any status | already cancelled → 200 (idempotent, never an error) |
POST .../skip |
ACTIVE |
400 |
POST .../reschedule |
ACTIVE or PAUSED |
400 |
POST .../generate-order |
Partner-managed billing only (otherwise 409); ACTIVE, end date not reached, and due (nextBillingDate ≤ now) |
400 |
POST .../confirm-renewal |
AWAITING_PARTNER (idempotent replay works from any status) |
400 |
Wrong-state errors are 400s, whose message names the current status — e.g. {"message": "Subscription is not active (status: PAUSED)", "code": "SUBSCRIPTION_NOT_ACTIVE"}. Branch on code: pause and skip answer SUBSCRIPTION_NOT_ACTIVE; resume and reschedule answer INVALID_SUBSCRIPTION_DATA; generate-order answers SUBSCRIPTION_NOT_ACTIVE, BILLING_SCHEDULE_INVALID when not yet due, or CUSTOMER_SHIPPING_ADDRESS_REQUIRED when the customer has no shipping address (add one and retry); confirm-renewal answers RENEWAL_NOT_CONFIRMABLE. The only 409s in this domain are the duplicate-active-subscription conflict on create (SUBSCRIPTION_ALREADY_EXISTS), a concurrent confirm-renewal in progress (RENEWAL_CONFIRMATION_IN_PROGRESS), a confirm-renewal after your billing mode changed (RENEWAL_BILLING_MODE_MISMATCH), and generate-order on a subscription DailyStak bills (SUBSCRIPTION_NOT_PARTNER_MANAGED, or ORDERS_REQUIRE_CHECKOUT when your whole account is DailyStak-billed).
Every schedule-affecting action appends an entry to scheduleHistory (changeType of SKIP, RESCHEDULE, ORDER_GENERATED, PAUSE, RESUME, or CANCEL, with the previous and new dates), so the subscription record is its own audit trail.
Notes on individual actions:
- Pause / resume never move
nextBillingDate. If the billing date passes while paused, the subscription is due the moment you resume and renews on the next scheduler run. If that's not what you want,reschedulefirst — it works while stillPAUSED. - Skip advances
nextBillingDateby one cycle without generating an order. Call it repeatedly to skip multiple cycles — it is not idempotent; each call moves the date again. An optional{"reason": "Customer traveling"}is recorded inscheduleHistory(send{}otherwise). - Reschedule sets
nextBillingDateto an exact instant, which must be strictly in the future — send an RFC 3339 date-time with an explicit offset (e.g.2026-09-01T00:00:00Z). Subsequent cycles advance from the new date. - Cancel works from any state, keeps already-generated orders intact, and is safe to retry.
endDateends the subscription. From that instant on (inclusive, UTC)isActiveisfalse,generate-orderstops, and no renewal is charged or flagged: when the next renewal comes due (or a dunning retry is next attempted), the subscription moves toCANCELLEDwith ascheduleHistoryreason ofEnd date reachedandsubscription.cancelledis sent. APAST_DUEsubscription's unpaid renewal invoice is voided first, so it can't be charged later. Until that renewal is due,statusstill readsACTIVE. A renewal already paid before the end date still produces its order.
The partner-managed renewal loop¶
If your billing is partner-managed, renewals are a three-step handshake: DailyStak tells you a renewal is due, you charge your customer, you confirm.
1. DailyStak flags the renewal. When nextBillingDate arrives, the subscription moves to AWAITING_PARTNER and a subscription.renewal_pending webhook event is delivered with a one-time confirmation token valid for 48 hours, the priced amount, and a line-item summary. The event's data payload:
{
"id": "evt_2zEvT7abcdefghijklmnopqrst",
"type": "subscription.renewal_pending",
"created": 1787529600,
"data": {
"subscriptionId": "2zAbC9xkQ21LmNoPqRsTuVwXyZ0",
"customerId": "2vTgd30JFgnwLrgb383PcV9Rken",
"protocolId": "2vSGym0bH8qVEwCIGlyFoRgJq1A",
"confirmationToken": "2zTOKEN0abcdefghijklmnopqrs",
"expiresAt": "2026-08-26T00:00:00Z",
"amountCents": 8998,
"lineItems": [
{ "productId": "7294312251486", "name": "Magnesium Glycinate", "quantity": 2, "customerPrice": 4499 }
]
}
}
2. You charge your customer through your own billing stack for amount_cents.
3. You confirm with the token (and optionally your payment reference, which is stored on the subscription and threaded onto the order):
curl -X POST https://api.fulfillment.juniperhealth.com/v2/subscriptions/2zAbC9xkQ21LmNoPqRsTuVwXyZ0/confirm-renewal \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"confirmationToken": "2zTOKEN0abcdefghijklmnopqrs",
"externalPaymentReference": "pmt_88123"
}'
{ "orderId": "2zORDERxyzabcdefghijklmnopq", "message": "Renewal confirmed, order generated", "idempotent": false }
On a fresh confirmation (201) the renewal order is generated from the protocol's current line items, the subscription returns to ACTIVE, nextBillingDate advances by one cycle, and a subscription.order_created webhook event fires (subscriptionId, orderId, confirmedBy: "partner", billedNow: false, externalPaymentReference).
Confirmation is idempotent per token. Replaying an already-confirmed token returns 200 with "idempotent": true and the original orderId — no duplicate order is ever created, so retrying on timeouts is safe. Treat orderId as your deduplication key, not the status code. If two confirmations race, one may get a 409 (Renewal confirmation is already being processed) or a 400 asking you to retry — wait a few seconds and resend the same token.
Other confirm-renewal failures are 400s with a specific message: wrong state (not awaiting partner confirmation), Invalid confirmation token, Confirmation token has expired, or the customer no longer having a shipping address.
Missing the window. If the 48-hour token expires unconfirmed, the subscription returns to ACTIVE and the renewal is retried on a later cycle — you'll receive a fresh subscription.renewal_pending with a new token. After 3 consecutive missed confirmations the subscription becomes FAILED — the partner-managed terminal state, where Juniper-managed dunning ends in CANCELLED — and a subscription.payment_failed event fires with "reason": "partner_confirmation_timeout", in the same shape a declined charge sends. A successful confirmation resets the miss counter to zero.
Juniper-managed renewals and dunning¶
With Juniper-managed billing, renewals happen without any API call from you: when nextBillingDate arrives, DailyStak charges the card saved at checkout, generates the renewal order (the quoted amount equals the charge), and advances nextBillingDate by one cycle. You observe this as subscription.order_created (confirmedBy: "platform", billedNow: true) alongside the order's own order.created.
If the charge fails, the subscription enters dunning:
- Status becomes
PAST_DUE;paymentFailureCountandlastPaymentErrorpopulate on the subscription record. - Every failed attempt sends
subscription.payment_failed— the first one included, so you hear about an expiring card on day 0 rather than from a support contact. It carriesreason(card_declinedorauthentication_required),attempt,maxAttemptsandnextRetryAt. - The charge is retried every 3 days. A successful retry returns the subscription to
ACTIVEand sendssubscription.payment_recovered(reason: retry_succeeded). - After the third failed charge (roughly six days after the first failure), the subscription is automatically
CANCELLED— that attempt still sendssubscription.payment_failed, withnextRetryAt: null, immediately followed bysubscription.cancelledwithreason: dunning_exhausted. ThescheduleHistoryentry shows"changedBy": "dunning".
The four renewal outcomes, as events:
| Outcome | Event |
|---|---|
| The renewal could not be attempted at all (no saved card, no shipping address, ...) | subscription.renewal_blocked |
| It was attempted and declined | subscription.payment_failed |
| A later retry settled | subscription.payment_recovered |
| It settled and produced an order | subscription.order_created |
PAST_DUE and the dunning cancellation are observed states — there is no API to trigger or retry a platform charge. You can still cancel a PAST_DUE subscription yourself to end dunning early.
Renewal amount¶
nextRenewalEstimate is what the next renewal is expected to cost. It is a stored snapshot, not a live quote, so reading a subscription never re-prices anything:
"nextRenewalEstimate": {
"subtotal": "27.75",
"shipping": "0.00",
"tax": "2.29",
"total": "30.04",
"currency": "usd",
"daysSupply": 30,
"quotedAt": "2026-07-25T18:04:11.512345Z",
"basis": "quote"
}
It is refreshed at exactly three moments:
- at checkout completion — the amount disclosed on the checkout page, carried onto the subscription (
basis: "quote"); - after each renewal settles — the amount just charged, carried forward for the next cycle (
basis: "last_charge"); - when you change
daysSupply— re-priced for the new cadence (basis: "quote"). If that re-pricing fails the old snapshot is kept, which you can see from itsdaysSupply.
It is not refreshed when catalogue prices change. So render it as an estimate, with two things in hand:
quotedAt— how old the figure is. Show its age, or at least do not present a months-old number as today's price.basis— where it comes from.quotemeans it was priced then and there off the regimen and catalogue.last_chargemeans it is the previous cycle's actual charge carried forward.
daysSupply on the estimate is the cadence it was priced for. If it differs from the subscription's daysSupply, the figure is stale — it refreshes at the next renewal. null means nothing has estimated it yet (no address on file at checkout, or a subscription created before this field existed).
For a live price at a cadence — the number you show a customer before asking them to accept a change — use the renewal estimate preview below rather than this snapshot.
Two things that are always true of a renewal, and which the estimate reflects:
- Renewals follow the shipping threshold.
shippingis"9.99"when the renewal's products come to less than $50, and"0.00"at $50 or more — the same rule a first order follows. A paid upgrade bought for that renewal is charged at its own price instead. - No promotion applies. A first-order promotion discounts the first charge only, which is why the checkout session's
totalAmountcan be well below this figure.
Changing cadence¶
A subscriber moving from 30 to 90 days is increasing their commitment, and nothing about that should send them back through a payment form. PUT /v2/subscriptions/{subscriptionId} applies daysSupply directly. The card on file does not change, and neither does the next charge date.
Because a new cadence means a new recurring amount, we ask you to show the customer that amount and assert they accepted it. Three steps:
1. Price the new cadence¶
curl "https://api.fulfillment.juniperhealth.com/v2/subscriptions/2zAbC9xkQ21LmNoPqRsTuVwXyZ0/renewal-estimate?daysSupply=90" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"subtotal": "83.25",
"shipping": "0.00",
"tax": "6.87",
"total": "90.12",
"currency": "usd",
"daysSupply": 90,
"quotedAt": "2026-07-25T18:04:11.512345Z",
"basis": "quote"
}
Same shape as nextRenewalEstimate, but priced now, at the cadence you ask for, through the same code path that charges the renewal — tax included. Omit daysSupply to price the current cadence. This is the one subscription read that prices live, so treat it as a write-weight call: fetch it when the customer is looking at the choice, not on every page render.
daysSupplyoutside 1–365, or not a whole number →400with{"code": "INVALID_DAYS_SUPPLY"}.- Unknown subscription, or one belonging to another partner →
404, exactly like the detail read. - We could not price it (tax provider unreachable, regimen not priceable) →
503with{"code": "RENEWAL_ESTIMATE_UNAVAILABLE"}. We never answer with a partial number. An amount missing its tax is an amount your customer would accept and then be over-charged against, so you get an error you can retry instead.
It is an estimate, not a quote you can hold: catalogue prices and tax rates move. Show it with its quotedAt.
2. Get the customer's acceptance¶
Show them the new cadence and the total from step 1, and record that they accepted both. That timestamp is what you send us.
3. Apply it¶
curl -X PUT https://api.fulfillment.juniperhealth.com/v2/subscriptions/2zAbC9xkQ21LmNoPqRsTuVwXyZ0 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"daysSupply": 90,
"termsAcceptedAt": "2026-07-25T18:05:44Z"
}'
The updated subscription comes back with the new daysSupply and a re-priced nextRenewalEstimate.
- We record what you assert, and you can read it back. The subscription carries
cadenceTermsAcceptedAt, the timestamp from the last accepted cadence change, so you can verify what we stored against your own record. Null if the cadence has never been changed. termsAcceptedAtis required to changedaysSupplyon a subscription DailyStak bills. Without it:400with{"code": "TERMS_ACCEPTANCE_REQUIRED"}and nothing is changed. With partner-managed billing it is optional — you charge your own customer, so that conversation is yours to run; send it anyway if you have it and we will record it. It must carry a UTC offset (Zor-05:00), be no more than 5 minutes in the future and no more than 24 hours old; otherwise400withTERMS_ACCEPTED_AT_TIMEZONE_REQUIRED,TERMS_ACCEPTED_AT_IN_FUTUREorTERMS_ACCEPTED_AT_TOO_OLD, and nothing is changed.- The paid cycle is never shortened. Changing
daysSupplyfrom 30 to 90 on day 10 does not move the next charge: it still happens on day 30, for a 90-day supply, and every 90 days after that. The cycle you already paid for is never shortened or prorated. - We tell the customer. A cadence change sends them a notice naming the new interval and the unchanged next charge date, gated by the same Subscription renewal emails toggle as the pre-renewal reminder — no separate switch. It carries no amount: you already showed them that.
- You hear about it too. The change emits
subscription.updatedwithreason: "cadence_changed"andchangedFields: ["daysSupply"], like any other field edit.
shippingMethod is applied by the same PUT and needs no acceptance, but it now means how the NEXT renewal ships. Renewals ship STANDARD; sending EXPEDITED or OVERNIGHT buys that class for the next renewal only, charged at the partner's configured price for it, and the subscription returns to STANDARD once that renewal's order is generated. Sending STANDARD gives the upgrade back. A class with no active shipping method configured is refused (400 INVALID_SUBSCRIPTION_DATA).
Where renewals ship¶
shippingAddress on GET /v2/subscriptions/{subscriptionId} is the address this subscription's renewals ship to. shippingAddressSource says where it came from: subscription (the address the subscription was bought with) or customer (the customer's current address, for subscriptions that have none of their own). The list endpoint does not carry these fields.
To change where one subscription ships, send shippingAddress on PUT /v2/subscriptions/{subscriptionId}. It changes that subscription only.
Since 2026-10-21 a subscription keeps the address it was bought with. Updating a customer's address does not move their existing subscriptions; update each subscription's shippingAddress.
Generating an order manually¶
POST /v2/subscriptions/{subscriptionId}/generate-order (no body) generates the next renewal order on demand — but only when the subscription is active and due. It cannot pull an order forward: if nextBillingDate is in the future you get 400 {"message": "Billing schedule error: Subscription <id> is not due for billing until <date>"}. Reschedule to now first if you genuinely need the order early.
Two things to be careful with:
- Partner-managed billing only. No payment is collected: this is an order-generation mechanism for partner-managed billing, where you charge the customer yourself. On a subscription DailyStak bills it returns
409and changes nothing — those renewal orders are created when the renewal payment succeeds. - Not idempotent. Each successful call (201,
{"orderId": ..., "message": "Order generated successfully", "idempotent": false}) creates an order and advances the schedule another cycle. The response carries only theorderId— fetch the full order via the Orders API.
Due scheduled protocol changes are applied to the protocol before the order is built, so the order always reflects the protocol as of the renewal.
Reading subscriptions¶
GET /v2/subscriptions— cursor-paginated list (subscriptions,cursor,hasMore), in creation order, up to 100 per page (default 50). Passcursorfrom the previous response to page forward;cursoris null when exhausted.GET /v2/subscriptions?customerId=cus_123— exact match on the customer id, served from the primary store (not the search index), so it is complete and immediately consistent. It pages exactly like the unfiltered list — a filtered page can come back short whilehasMoreis still true, so keep paging untilcursoris null — and an unknown customer — deleted, or another partner's — returns an empty page rather than a 404. This is the reliable answer to "which subscriptions belong to this customer".GET /v2/subscriptions?search=jane— ranked fuzzy match over customer name, subscription ID, protocol ID, and status. Search is served from a secondary index populated asynchronously, so a just-created subscription may briefly not be searchable — use the plain list or a direct GET for read-after-write. Search and non-search cursors are not interchangeable.customerIdandsearchcannot be combined: sending both returns400with{"message": "customerId and search cannot be combined", "code": "FILTER_CONFLICT"}.GET /v2/subscriptions/{subscriptionId}— one subscription, with the linked protocol embedded underprotocol(id,customerId,status,lineItems, timestamps), and the address its renewals ship to undershippingAddress(see Where renewals ship). This is the only response that populatesprotocol— it isnullin the create response and all list items, which is also where you find the subscription's customer ID.GET /v2/subscriptions/{subscriptionId}/renewal-estimate?daysSupply=90— prices a renewal at that cadence live, tax included. The only subscription read that is not served from stored data; see Changing cadence.
Webhook events¶
Every lifecycle transition emits, whoever caused it — your API call, your team in the portal, the customer in your storefront, or the platform. See Webhooks for delivery, signing, and payload details.
| Event | Fires when |
|---|---|
subscription.created |
A subscription is created — POST /v2/subscriptions, the partner portal, or a completed hosted checkout. Carries protocolId and customerId; with one protocol per cart, protocolId is your cart correlation key. |
subscription.updated |
daysSupply, shippingMethod, endDate or notes changed, or a product was added or removed in the storefront. Carries changedFields[] and reason (cadence_changed / fields_changed). |
subscription.paused |
The subscription moves to PAUSED. |
subscription.resumed |
A PAUSED subscription returns to ACTIVE. |
subscription.cancelled |
The subscription moves to CANCELLED. reason says why: requested, dunning_exhausted, customer_deleted, protocol_deactivated, checkout_compensation, end_date_reached. |
subscription.skipped |
The next order is skipped and the billing date advances one cycle. |
subscription.rescheduled |
nextBillingDate is moved. |
subscription.payment_recovered |
A dunning retry settles and a PAST_DUE subscription returns to ACTIVE (reason: retry_succeeded). |
subscription.renewal_pending |
A partner-managed renewal comes due (carries the 48-hour token and amount). |
subscription.renewal_blocked |
A platform-billed renewal came due but can't be charged (no saved card, no payment profile, no shipping address, ...). Sent once per cause per billing cycle; the renewal is retried and charges once it's fixed. The subscription carries renewalBlockedReason, renewalBlockedAt and renewalBlockedMessage meanwhile. |
subscription.order_created |
A renewal order is generated — after a platform-billed renewal settles (confirmedBy: "platform", billedNow: true), or after your confirm-renewal (confirmedBy: "partner"). |
subscription.payment_failed |
Every failed renewal payment attempt: each Juniper-managed decline (reason: "card_declined" or "authentication_required"), and a third consecutive missed partner confirmation (reason: "partner_confirmation_timeout"). Carries attempt, maxAttempts and nextRetryAt (null on the last attempt). |
order.created |
Any order is created — including every subscription-generated order (initial, confirm-renewal, generate-order, and platform renewals). Delivered asynchronously, usually within seconds. |
Lifecycle events are derived from the subscription record, so they fire for changes your own API calls made too. Every one carries changedBy (the raw actor) and initiatedBy (PARTNER_API | PARTNER_PORTAL | CUSTOMER | PLATFORM); reason is null where the table does not name one. Delivery is at-least-once and stream records are ordered per subscription — dedupe on the event id.
Good to know¶
- Always send timezone-aware date-times. Every date field expects RFC 3339 with an explicit offset (
2026-09-01T00:00:00Z). Timezone-naive values are not supported and can be rejected or mis-compared. PUTapplies five fields.daysSupply,shippingMethod,endDate,notesandshippingAddress— nothing else on the subscription, and nothing at all on the protocol. A body with none of them returns400 No valid fields provided for update; ashippingAddressthat fails validation returns422 VALIDATION_FAILED, and on a subscription billed through Shopify it returns409 SEAL_UNSUPPORTED_CHANGE; adaysSupplychange withouttermsAcceptedAton a subscription DailyStak bills returns400 {"code": "TERMS_ACCEPTANCE_REQUIRED"}(see Changing cadence). Null/omitted means "leave unchanged" — you cannot clear a field back to null. There is no status gate: even a cancelled subscription's notes can be updated. An applied change emitssubscription.updatedwithchangedFields[].- Idempotency cheat sheet:
cancelandconfirm-renewal(per token) are idempotent;skipandgenerate-orderare not — each call advances the schedule again;createconflicts (409) only while an active subscription exists on the protocol. - Deleting a customer cascades. Soft-deleting a customer cancels their active, paused, pending, and awaiting-confirmation subscriptions, each emitting
subscription.cancelledwithreason: customer_deleted— see Customers. - Renewal cadence math never uses "now".
ORDER_GENERATEDandSKIPboth advance from the previously scheduled date. If a subscription was overdue by more than one cycle, expect it to be due again immediately after a renewal — reschedule if you want to re-anchor.