# Customer created
Source: https://juo.dev/api-reference/customer-created
/openapi-admin.json webhook customer.created
Fired when a `customer.created` event occurs. Delivered via Svix.
# Customer payment method created
Source: https://juo.dev/api-reference/customer-payment-method-created
/openapi-admin.json webhook customer-payment-method.created
Fired when a `customer-payment-method.created` event occurs. Delivered via Svix.
# Customer payment method revoked
Source: https://juo.dev/api-reference/customer-payment-method-revoked
/openapi-admin.json webhook customer-payment-method.revoked
Fired when a `customer-payment-method.revoked` event occurs. Delivered via Svix.
# Customer payment method updated
Source: https://juo.dev/api-reference/customer-payment-method-updated
/openapi-admin.json webhook customer-payment-method.updated
Fired when a `customer-payment-method.updated` event occurs. Delivered via Svix.
# Customer updated
Source: https://juo.dev/api-reference/customer-updated
/openapi-admin.json webhook customer.updated
Fired when a `customer.updated` event occurs. Delivered via Svix.
# Order created
Source: https://juo.dev/api-reference/order-created
/openapi-admin.json webhook order.created
Fired when a `order.created` event occurs. Delivered via Svix.
# Order deleted
Source: https://juo.dev/api-reference/order-deleted
/openapi-admin.json webhook order.deleted
Fired when a `order.deleted` event occurs. Delivered via Svix.
# Order updated
Source: https://juo.dev/api-reference/order-updated
/openapi-admin.json webhook order.updated
Fired when a `order.updated` event occurs. Delivered via Svix.
# Product created
Source: https://juo.dev/api-reference/product-created
/openapi-admin.json webhook product.created
Fired when a `product.created` event occurs. Delivered via Svix.
# Product deleted
Source: https://juo.dev/api-reference/product-deleted
/openapi-admin.json webhook product.deleted
Fired when a `product.deleted` event occurs. Delivered via Svix.
# Product updated
Source: https://juo.dev/api-reference/product-updated
/openapi-admin.json webhook product.updated
Fired when a `product.updated` event occurs. Delivered via Svix.
# Product variant created
Source: https://juo.dev/api-reference/product-variant-created
/openapi-admin.json webhook product-variant.created
Fired when a `product-variant.created` event occurs. Delivered via Svix.
# Product variant deleted
Source: https://juo.dev/api-reference/product-variant-deleted
/openapi-admin.json webhook product-variant.deleted
Fired when a `product-variant.deleted` event occurs. Delivered via Svix.
# Product variant updated
Source: https://juo.dev/api-reference/product-variant-updated
/openapi-admin.json webhook product-variant.updated
Fired when a `product-variant.updated` event occurs. Delivered via Svix.
# Schedule adjustment created
Source: https://juo.dev/api-reference/schedule-adjustment-created
/openapi-admin.json webhook schedule-adjustment.created
Fired when a `schedule-adjustment.created` event occurs. Delivered via Svix.
# Schedule adjustment deleted
Source: https://juo.dev/api-reference/schedule-adjustment-deleted
/openapi-admin.json webhook schedule-adjustment.deleted
Fired when a `schedule-adjustment.deleted` event occurs. Delivered via Svix.
# Schedule adjustment updated
Source: https://juo.dev/api-reference/schedule-adjustment-updated
/openapi-admin.json webhook schedule-adjustment.updated
Fired when a `schedule-adjustment.updated` event occurs. Delivered via Svix.
# Subscription created
Source: https://juo.dev/api-reference/subscription-created
/openapi-admin.json webhook subscription.created
Fired when a `subscription.created` event occurs. Delivered via Svix.
# Subscription discount created
Source: https://juo.dev/api-reference/subscription-discount-created
/openapi-admin.json webhook subscription-discount.created
Fired when a `subscription-discount.created` event occurs. Delivered via Svix.
# Subscription discount deleted
Source: https://juo.dev/api-reference/subscription-discount-deleted
/openapi-admin.json webhook subscription-discount.deleted
Fired when a `subscription-discount.deleted` event occurs. Delivered via Svix.
# Subscription discount updated
Source: https://juo.dev/api-reference/subscription-discount-updated
/openapi-admin.json webhook subscription-discount.updated
Fired when a `subscription-discount.updated` event occurs. Delivered via Svix.
# Subscription item created
Source: https://juo.dev/api-reference/subscription-item-created
/openapi-admin.json webhook subscription-item.created
Fired when a `subscription-item.created` event occurs. Delivered via Svix.
# Subscription item deleted
Source: https://juo.dev/api-reference/subscription-item-deleted
/openapi-admin.json webhook subscription-item.deleted
Fired when a `subscription-item.deleted` event occurs. Delivered via Svix.
# Subscription item updated
Source: https://juo.dev/api-reference/subscription-item-updated
/openapi-admin.json webhook subscription-item.updated
Fired when a `subscription-item.updated` event occurs. Delivered via Svix.
# Subscription renewed
Source: https://juo.dev/api-reference/subscription-renewed
/openapi-admin.json webhook subscription.renewed
Fired when a `subscription.renewed` event occurs. Delivered via Svix.
# Subscription updated
Source: https://juo.dev/api-reference/subscription-updated
/openapi-admin.json webhook subscription.updated
Fired when a `subscription.updated` event occurs. Delivered via Svix.
# Add a payment method
Source: https://juo.dev/docs/api-reference/admin/customers/add-payment-method
openapi-admin.json POST /customers/{customerId}/payment-methods
Used to add a payment method to a customer.
# Create a customer
Source: https://juo.dev/docs/api-reference/admin/customers/create
openapi-admin.json POST /customers
Used to create a customer.
# List customers
Source: https://juo.dev/docs/api-reference/admin/customers/list
openapi-admin.json GET /customers
Returns a list of customers.
# Customer resource
Source: https://juo.dev/docs/api-reference/admin/customers/resource
This object represents a customer of your business
The `Customer` object serves multiple purposes, including storing billing and shipping information, creating order schedules, applying discounts, and authenticating customers in Customer Portal.
### Search query fields
| Field | Description | Type |
| ----------- | -------------------------- | ------ |
| id | The customer identifier | string |
| displayName | The customer full name | string |
| email | The customer contact email | string |
| phone | The customer contact phone | string |
### Reference
# Introduction
Source: https://juo.dev/docs/api-reference/admin/introduction
## Welcome
The Juo Admin API provides programmatic access to manage subscription resources. This RESTful API allows administrators to interact with subscriptions, customers, and other Juo entities.
Admin API SDK is available [here](https://www.npmjs.com/package/@juo/admin-api).
## Authentication
All API requests must include an API key in the `X-Juo-Admin-Api-Key` header. Tokens can be generated in the Juo admin interface. Keep your API keys secure and never share them.
```
X-Juo-Admin-Api-Key: your_api_key
```
## Pagination
The API uses cursor-based pagination to handle large result sets. Navigation links are provided in the `Link` response header, containing URLs for the next and previous pages.
```
Link: >; rel="next",
>; rel="prev"
```
The amount of results per page can be set from 1-100 with the `limit` query parameter where applicable:
```
/admin/v1/subscriptions?limit=10
```
## Sorting
Results can be sorted using the `sort` query parameter. Multiple sort criteria can be combined using commas.
Sortable fields are the same as the filterable fields on a resource, there is a special `rank` field that can be always used whenever search query includes a term not bound to a specific field.
```
# Sort by creation date descending
/admin/v1/subscriptions?sort=createdAt:desc
# Sort by status ascending and then by creation date descending
/admin/v1/subscriptions?sort=status:asc,createdAt:desc
```
## Search query language
Resources can be searched / filtered by an expressive query syntax.
The search grammar is expressed in [EBNF](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). It uses the following terminal symbols:
| Symbol | Description |
| ------ | ----------------------------------------------------------- |
| \_ | Optional whitespace |
| \_\_ | Mandatory whitespace |
| field | An identifier (`[a-z][a-zA-Z]*`) |
| value | An identifier, single quoted string or double quoted string |
The syntax can be expressed with the following grammar:
```ebnf theme={null}
Query = Disjunction
Negation = ( "-" | "NOT" ) _ Parenthesis | Parenthesis
Parenthesis = "(" _ Query _ ")" | Term
Disjunction = Conjunction ( __ "OR" __ Conjunction ):+ | Conjunction
Conjunction = Negation ( __ "AND" __ Negation ):+ | Negation
Term = field Operator value | value
Operator = ":" | ":<" | ":>" | ":>=" | ":<="
```
It lets you to form advanced logical queries, some examples below:
```
# Find all active subscriptions which contain a term "Blue"
status:active AND Blue
# The same as above but with quotes
status:'active' AND 'Blue'
# Find failed subscriptions with PayPal instrument along with all cancelled subscriptions after 2024
(status:failed AND instrument:paypal) OR (status:cancelled AND cancelledAt:>='2024-01-01')
# Find all but active subscriptions
- status:active
```
Below you can find the meaning of each available operator:
| Operator | Description |
| -------- | --------------------- |
| : | Equal |
| :> | Greater than |
| :>= | Greater than or equal |
| :\< | Less than |
| :\<= | Less than or equal |
## Rate limiting
The API implements a token bucket rate-limiting strategy to ensure fair usage. Each API token has a bucket that fills at a constant rate. API calls consume tokens from the bucket. When the bucket is empty, requests will be rejected until it refills.
Rate limit information is included in the response headers:
```
X-RateLimit-Limit: 1000 # How many requests the client can make
X-RateLimit-Remaining: 999 # How many requests remain to the client in the time window
X-RateLimit-Reset: 60 # How many seconds must pass before the rate limit resets
```
# Add a variant
Source: https://juo.dev/docs/api-reference/admin/products/add-variant
openapi-admin.json POST /products/{productId}/variants
Used to add a variant to a product.
# Assign product to plan
Source: https://juo.dev/docs/api-reference/admin/products/assign-plan
openapi-admin.json POST /products/{productId}/plans
Used to assign a product to a subscription plan.
# Assign variant to plan
Source: https://juo.dev/docs/api-reference/admin/products/assign-variant-plan
openapi-admin.json POST /products/{productId}/variants/{variantId}/plans
Used to assign a product variant to a subscription plan.
# Create a product
Source: https://juo.dev/docs/api-reference/admin/products/create
openapi-admin.json POST /products
Used to create a product.
# Delete a product
Source: https://juo.dev/docs/api-reference/admin/products/delete
openapi-admin.json DELETE /products/{productId}
Used to delete a product.
# List products
Source: https://juo.dev/docs/api-reference/admin/products/list
openapi-admin.json GET /products
Returns a list of products.
# Remove product from plan
Source: https://juo.dev/docs/api-reference/admin/products/remove-plan
openapi-admin.json DELETE /products/{productId}/plans
Used to remove a product from a subscription plan.
# Remove a variant
Source: https://juo.dev/docs/api-reference/admin/products/remove-variant
openapi-admin.json DELETE /products/{productId}/variants/{variantId}
Used to remove a variant from a product.
# Remove variant from plan
Source: https://juo.dev/docs/api-reference/admin/products/remove-variant-plan
openapi-admin.json DELETE /products/{productId}/variants/{variantId}/plans
Used to remove a product variant from a subscription plan.
# Product resource
Source: https://juo.dev/docs/api-reference/admin/products/resource
This object represents a product in your store
The `Product` object represents a product in your store. Products can be associated with subscription plan groups and collections.
### Search query fields
| Field | Description | Type |
| -------------- | ---------------------------- | ------ |
| id | The product identifier | string |
| title | The product title | string |
| status | The product status | string |
| variants.title | The product's variants title | string |
| variants.sku | The product's variants sku | string |
### Reference
# Update a product
Source: https://juo.dev/docs/api-reference/admin/products/update
openapi-admin.json PATCH /products/{productId}
Used to update a product.
# Actions
Source: https://juo.dev/docs/api-reference/admin/schedule-adjustments/actions
Available actions for schedule adjustments
Actions define what modification to apply when a schedule adjustment matches an order.
## Skip order
Prevents an order from being created. The order will be excluded from the schedule entirely.
**Use cases:**
* Customer going on vacation
* Holiday closures
* Temporary subscription pause
**Input:**
| Field | Type | Required | Description |
| -------- | ------ | -------- | ----------------------------- |
| `reason` | string | Yes | Reason for skipping the order |
```json theme={null}
{
"type": "SKIP_ORDER",
"input": {
"reason": "Customer on vacation"
}
}
```
***
## Change date
Reschedules an order to a different date within the billing period.
**Use cases:**
* Customer requests different delivery day
* Adjusting timing around events
**Input:**
| Field | Type | Required | Description |
| --------- | ------ | -------- | --------------------------------- |
| `newDate` | string | Yes | New date for the order (ISO 8601) |
```json theme={null}
{
"type": "CHANGE_DATE",
"input": {
"newDate": "2025-02-15T00:00:00Z"
}
}
```
Only supports the `CYCLE` matcher. The new date must be within one billing cycle of the original date and cannot be in the past.
***
## Update shipping
Updates the shipping address for a specific order.
**Use cases:**
* Customer temporarily at different address
* One-time delivery to alternate location
**Input:**
| Field | Type | Required | Description |
| ---------------------- | ------ | -------- | --------------------- |
| `address.firstName` | string | No | First name |
| `address.lastName` | string | No | Last name |
| `address.address1` | string | Yes | Street address line 1 |
| `address.address2` | string | No | Street address line 2 |
| `address.city` | string | No | City |
| `address.zip` | string | No | Postal/ZIP code |
| `address.countryCode` | string | Yes | ISO country code |
| `address.provinceCode` | string | No | Province/state code |
| `address.phone` | string | No | Phone number |
| `address.company` | string | No | Company name |
```json theme={null}
{
"type": "UPDATE_SHIPPING",
"input": {
"address": {
"firstName": "John",
"lastName": "Doe",
"address1": "123 Main St",
"city": "New York",
"countryCode": "US",
"zip": "10001"
}
}
}
```
Address must include at least `address1` and `countryCode`.
***
## Update payment method
Updates the payment method for a specific order.
**Use cases:**
* Use different card for specific order
* One-time payment method override
**Input:**
| Field | Type | Required | Description |
| ----------------- | ------ | -------- | ------------------------- |
| `paymentMethodId` | string | Yes | Payment method identifier |
```json theme={null}
{
"type": "UPDATE_PAYMENT_METHOD",
"input": {
"paymentMethodId": "pm_abc123"
}
}
```
***
## Update products
Modifies product lines for a specific order.
**Use cases:**
* One-time quantity change
* Swap variant for single order
* Adjust billing interval for specific cycle
**Input:**
| Field | Type | Required | Description |
| ---------------------------------- | ------- | -------- | ---------------------------------------------- |
| `lines` | array | Yes | Array of line modifications (min 1) |
| `lines[].lineId` | string | Yes | ID of the line to modify |
| `lines[].variantId` | string | No | New variant ID to swap to |
| `lines[].quantity` | integer | No | New quantity (min 1) |
| `lines[].interval` | object | No | Override billing interval |
| `lines[].interval.intervalCount` | integer | No | Number of intervals |
| `lines[].interval.interval` | string | No | Interval unit: DAY, WEEK, MONTH, YEAR |
| `lines[].nextBillingDate` | string | No | Override next billing date (ISO 8601) |
| `lines[].customAttributes` | array | No | Custom attributes to merge onto the order item |
| `lines[].customAttributes[].key` | string | Yes | Attribute key |
| `lines[].customAttributes[].value` | string | Yes | Attribute value |
```json theme={null}
{
"type": "UPDATE_PRODUCTS",
"input": {
"lines": [
{
"lineId": "line_abc123",
"quantity": 2
},
{
"lineId": "line_def456",
"variantId": "variant_xyz789"
},
{
"lineId": "line_ghi789",
"customAttributes": [
{ "key": "gift_message", "value": "Happy birthday!" }
]
}
]
}
}
```
Line IDs must exist in the customer's subscriptions. Attributes are merged onto the item by key — existing keys are overwritten, unspecified keys are preserved.
Reserved/internal attribute keys cannot be set via `customAttributes` and will cause the request to fail with a 400 error.
***
## Apply discount
Applies a discount (Shopify code or manual) to a specific order in the schedule.
**Use cases:**
* One-time promotional discount for specific cycle
* Loyalty reward for long-term subscribers
* Compensation discount for service issues
The action supports two discount sources: Shopify discount codes and manual discounts.
### Shopify code discount
Apply an existing Shopify discount code to the order.
**Input:**
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | ---------------------------------- |
| `discountSource` | string | Yes | Must be `"code"` |
| `discountCode` | string | Yes | The Shopify discount code to apply |
```json theme={null}
{
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "code",
"discountCode": "SAVE20"
}
}
```
Discount code must exist in Shopify, be active, and apply to subscriptions. Only basic discounts and free shipping discounts are supported.
### Manual discount - line items
Create a custom discount targeting line items.
**Input:**
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | ------------------------------------------------------------------ |
| `discountSource` | string | Yes | Must be `"manual"` |
| `discountCode` | string | Yes | Custom discount title/name |
| `discountType` | string | Yes | `"percentage"` or `"fixed_amount"` |
| `value` | number | Yes | Discount value (percentage 0-100 or fixed amount in shop currency) |
| `targetType` | string | Yes | Must be `"line_item"` |
| `targeting` | object | Yes | Specifies what to target |
**Targeting options:**
* `{ "type": "all" }` - Apply to all line items
* `{ "type": "products", "productIds": ["gid://shopify/Product/123", ...] }` - Apply to specific products
**Example: Percentage discount on all items**
```json theme={null}
{
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "manual",
"discountCode": "Loyalty Reward",
"discountType": "percentage",
"value": 15,
"targetType": "line_item",
"targeting": {
"type": "all"
}
}
}
```
**Example: Fixed amount discount on specific products**
```json theme={null}
{
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "manual",
"discountCode": "Product Promo",
"discountType": "fixed_amount",
"value": 10,
"targetType": "line_item",
"targeting": {
"type": "products",
"productIds": ["gid://shopify/Product/123456789"]
}
}
}
```
### Manual discount - shipping
Create a custom discount targeting shipping.
**Input:**
| Field | Type | Required | Description |
| ---------------- | ------ | -------- | ----------------------------- |
| `discountSource` | string | Yes | Must be `"manual"` |
| `discountCode` | string | Yes | Custom discount title/name |
| `discountType` | string | Yes | Must be `"percentage"` |
| `value` | number | Yes | Must be `100` (free shipping) |
| `targetType` | string | Yes | Must be `"shipping"` |
| `targeting` | object | Yes | Must be `{ "type": "all" }` |
**Example: Free shipping discount**
```json theme={null}
{
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "manual",
"discountCode": "Free Shipping",
"discountType": "percentage",
"value": 100,
"targetType": "shipping",
"targeting": {
"type": "all"
}
}
}
```
Only supports the `CYCLE` matcher. Only supports free shipping discounts (100% off) - partial shipping discounts and fixed amount shipping discounts are not supported.
# Create a schedule adjustment
Source: https://juo.dev/docs/api-reference/admin/schedule-adjustments/create
openapi-admin.json POST /schedules/adjustments
Used to create a schedule adjustment.
## Examples
Skip order for the third cycle with a reason.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 3
}
},
"action": {
"type": "SKIP_ORDER",
"input": {
"reason": "Customer on vacation"
}
}
}
```
Reschedule order for the second cycle to a new date.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "CHANGE_DATE",
"input": {
"newDate": "2025-02-15T00:00:00Z"
}
}
}
```
`CHANGE_DATE` only supports the `CYCLE` matcher. The new date must be within one billing cycle of the original date and cannot be in the past.
Skip all orders scheduled for a specific date.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "DATE",
"input": {
"date": "2025-01-01T00:00:00Z"
}
},
"action": {
"type": "SKIP_ORDER",
"input": {
"reason": "Holiday closure"
}
}
}
```
Update shipping address for a specific cycle.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 3
}
},
"action": {
"type": "UPDATE_SHIPPING",
"input": {
"address": {
"firstName": "John",
"lastName": "Doe",
"address1": "123 Main St",
"city": "New York",
"countryCode": "US",
"zip": "10001"
}
}
}
}
```
Address must include at least `address1` and `countryCode`.
Update payment method for a specific cycle.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 4
}
},
"action": {
"type": "UPDATE_PAYMENT_METHOD",
"input": {
"paymentMethodId": "pm_abc123"
}
}
}
```
Modify product lines for a specific cycle.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "UPDATE_PRODUCTS",
"input": {
"lines": [
{
"lineId": "line_abc123",
"quantity": 2
},
{
"lineId": "line_def456",
"variantId": "variant_xyz789"
},
{
"lineId": "line_ghi789",
"customAttributes": [
{ "key": "gift_message", "value": "Happy birthday!" }
]
}
]
}
}
}
```
Each line requires `lineId`. Optional fields: `variantId` (swap variant), `quantity` (min 1), `interval` (override billing interval), `nextBillingDate`, `customAttributes` (merged onto the item by key; reserved/internal keys are rejected with a 400).
Apply a Shopify discount code to a specific cycle.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "code",
"discountCode": "SAVE20"
}
}
}
```
`APPLY_DISCOUNT` only supports the `CYCLE` matcher. Discount must exist in Shopify and be valid for subscriptions.
Apply a manual discount to line items for a specific cycle.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "manual",
"discountCode": "Loyalty Reward",
"discountType": "percentage",
"value": 15,
"targetType": "line_item",
"targeting": {
"type": "all"
}
}
}
}
```
Manual discounts support `targetType` of `"line_item"` or `"shipping"`, with targeting options for `"all"` or specific `"products"`.
Apply a manual discount to shipping for a specific cycle.
```json theme={null}
{
"customerId": "cus_abc123",
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "manual",
"discountCode": "Free Shipping",
"discountType": "percentage",
"value": 100,
"targetType": "shipping",
"targeting": {
"type": "all"
}
}
}
}
```
Only supports free shipping discounts. For shipping discounts, `discountType` must be `"percentage"`, `value` must be `100`, `targetType` must be `"shipping"`, and `targeting` must be `{ "type": "all" }`.
# Delete a schedule adjustment
Source: https://juo.dev/docs/api-reference/admin/schedule-adjustments/delete
openapi-admin.json DELETE /schedules/adjustments/{adjustmentId}
Used to delete a schedule adjustment.
# List schedule adjustments
Source: https://juo.dev/docs/api-reference/admin/schedule-adjustments/list
openapi-admin.json GET /schedules/adjustments
Returns a list of schedule adjustments.
# Matchers
Source: https://juo.dev/docs/api-reference/admin/schedule-adjustments/matchers
Available matchers for schedule adjustments
Matchers define which orders in the schedule should be affected by an adjustment.
## Cycle
Match orders by billing cycle number.
**Use cases:**
* Target a specific future order (e.g., "skip the 3rd order")
* Apply adjustment to a known cycle
**Input:**
| Field | Type | Required | Description |
| ------- | ------- | -------- | ------------------------------- |
| `cycle` | integer | Yes | Target cycle number (0-indexed) |
```json theme={null}
{
"type": "CYCLE",
"input": {
"cycle": 3
}
}
```
Cycle numbers start at 0. Cycle 0 is the first/current cycle.
***
## Date
Match orders by their scheduled date.
**Use cases:**
* Skip all orders on a specific date (e.g., holiday)
* Target orders regardless of cycle number
**Input:**
| Field | Type | Required | Description |
| ------ | ------ | -------- | ---------------------- |
| `date` | string | Yes | Target date (ISO 8601) |
```json theme={null}
{
"type": "DATE",
"input": {
"date": "2025-01-01T00:00:00Z"
}
}
```
***
## Cycle and date
Match orders by both cycle number and scheduled date.
**Use cases:**
* Precise targeting when both cycle and date matter
* Disambiguate when multiple orders could match
**Input:**
| Field | Type | Required | Description |
| ------- | ------- | -------- | ------------------------------- |
| `cycle` | integer | Yes | Target cycle number (0-indexed) |
| `date` | string | Yes | Target date (ISO 8601) |
```json theme={null}
{
"type": "CYCLE_AND_DATE",
"input": {
"cycle": 2,
"date": "2025-03-15T00:00:00Z"
}
}
```
***
## Compatibility
Not all matchers work with all actions:
| Action | CYCLE | DATE | CYCLE\_AND\_DATE |
| ----------------------- | :---: | :--: | :--------------: |
| SKIP\_ORDER | ✓ | ✓ | ✓ |
| CHANGE\_DATE | ✓ | ✗ | ✗ |
| UPDATE\_SHIPPING | ✓ | ✓ | ✓ |
| UPDATE\_PAYMENT\_METHOD | ✓ | ✓ | ✓ |
| UPDATE\_PRODUCTS | ✓ | ✓ | ✓ |
| APPLY\_DISCOUNT | ✓ | ✗ | ✗ |
# Schedule adjustment resource
Source: https://juo.dev/docs/api-reference/admin/schedule-adjustments/resource
This object represents an adjustment that modifies schedule orders during generation.
The `ScheduleAdjustment` object allows you to customize the schedule by modifying orders before they are created. Schedule adjustments can be used to skip orders, change dates, update shipping, and more.
Define what modification to apply (skip, reschedule, update shipping, etc.)
Define which orders to target (by cycle, date, or both)
### Reference
# Get schedule
Source: https://juo.dev/docs/api-reference/admin/schedules/get
openapi-admin.json GET /schedules/
Returns a customer schedule.
# Schedule order resource
Source: https://juo.dev/docs/api-reference/admin/schedules/resource
This object represents an order within a schedule.
The `ScheduleOrder` object is a projected future order generated by the schedule system. Each schedule contains one or more orders that define when and what will be delivered to the customer. Schedule orders serve as a preview of upcoming orders before they are actually created and processed, containing information about the delivery date, items to be delivered, pricing, discounts, and whether the order has been skipped.
### Search query fields
| Field | Description | Type |
| ----- | --------------------------------------------------------------- | ------ |
| id | The subscription identifier (accepts up to 20 subscription IDs) | string |
### Reference
# Create a subscription discount
Source: https://juo.dev/docs/api-reference/admin/subscription-discounts/create
openapi-admin.json POST /subscriptions/{subscriptionId}/discounts
Used to create a subscription discount.
# Delete a subscription discount
Source: https://juo.dev/docs/api-reference/admin/subscription-discounts/delete
openapi-admin.json DELETE /subscriptions/{subscriptionId}/discounts/{discountId}
Used to delete a subscription discount.
# Subscription discount resource
Source: https://juo.dev/docs/api-reference/admin/subscription-discounts/resource
This object represents a discount applied to a subscription item.
The `SubscriptionDiscount` object is used to apply discounts to subscription items, reducing the product price for subscribers.
### Reference
# Update a subscription discount
Source: https://juo.dev/docs/api-reference/admin/subscription-discounts/update
openapi-admin.json PATCH /subscriptions/{subscriptionId}/discounts/{discountId}
Used to update a subscription discount.
# Create a subscription item
Source: https://juo.dev/docs/api-reference/admin/subscription-items/create
openapi-admin.json POST /subscriptions/{subscriptionId}/items
Used to create a subscription item.
# Delete subscription item
Source: https://juo.dev/docs/api-reference/admin/subscription-items/delete
openapi-admin.json DELETE /subscriptions/{subscriptionId}/items/{itemId}
Used to delete a subscription item.
# Subscription item resource
Source: https://juo.dev/docs/api-reference/admin/subscription-items/resource
This object represents a product included in a subscription.
The `SubscriptionItem` object defines which products are included in a subscription and how they are configured. It supports multiple variants and can be configured for specific subscription cycles, allowing customers to customize their subscription with different product options.
### Reference
# Update subscription item
Source: https://juo.dev/docs/api-reference/admin/subscription-items/update
openapi-admin.json PATCH /subscriptions/{subscriptionId}/items/{itemId}
Used to update a subscription item.
# Cancel subscription
Source: https://juo.dev/docs/api-reference/admin/subscriptions/cancel
openapi-admin.json POST /subscriptions/{subscriptionId}/cancel
Used to cancel a subscription.
# Create a subscription
Source: https://juo.dev/docs/api-reference/admin/subscriptions/create
openapi-admin.json POST /subscriptions
Used to create a subscription.
# List subscriptions
Source: https://juo.dev/docs/api-reference/admin/subscriptions/list
openapi-admin.json GET /subscriptions
Returns a list of subscriptions.
# Pause subscription
Source: https://juo.dev/docs/api-reference/admin/subscriptions/pause
openapi-admin.json POST /subscriptions/{subscriptionId}/pause
Used to pause a subscription.
# Reactivate subscription
Source: https://juo.dev/docs/api-reference/admin/subscriptions/reactivate
openapi-admin.json POST /subscriptions/{subscriptionId}/reactivate
Used to reactivate a subscription.
# Subscription resource
Source: https://juo.dev/docs/api-reference/admin/subscriptions/resource
Subscription represents a contract between your business and a customer.
The `Subscription` object stores information and rules defining the contract between your business and a customer. It is created when a customer purchases a subscription-based product.
### Search query fields
| Field | Description | Type |
| --------------- | ------------------------------------------ | ------ |
| id | The subscription identifier | string |
| serial | The incremental subscription number | string |
| status | The subscription status | string |
| createdAt | The date subscription was created | date |
| updatedAt | The last date the subscription was updated | date |
| nextBillingDate | The upcoming next billing date | date |
| canceledAt | The date subscription was cancelled | date |
| customer | The customer identifier | string |
### Reference
# Resume subscription
Source: https://juo.dev/docs/api-reference/admin/subscriptions/resume
openapi-admin.json POST /subscriptions/{subscriptionId}/resume
Used to resume a subscription.
# Update subscription
Source: https://juo.dev/docs/api-reference/admin/subscriptions/update
openapi-admin.json PATCH /subscriptions/{subscriptionId}
Used to update a subscription.
# Overview
Source: https://juo.dev/docs/api-reference/admin/webhooks/overview
How Juo webhooks work and how to subscribe to them
Juo uses [Svix](https://svix.com) to deliver webhooks reliably. When subscription-related events occur in your store, Juo sends an HTTP POST request to your configured endpoint with a JSON payload.
## Setting up
1. Open the Juo Admin Portal and navigate to **Settings → Webhooks**
2. Click **Manage Webhooks** to open the Svix dashboard
3. Add your endpoint URL and select which events to subscribe to
## Delivery guarantees
* **At-least-once delivery** — webhooks are retried on failure
* **Retry schedule** — exponential backoff over 5 days
* **Message history** — view delivery logs in the dashboard
## Payload structure
All webhook payloads are JSON objects. Each event type has a defined schema — see [Topics](/docs/api-reference/admin/webhooks/topics) for the full list.
**Example payload for `subscription.created`:**
```json theme={null}
{
"id": "a1b2c3d4-e5f6-...",
"resource": "subscription",
"status": "active",
"currencyCode": "USD",
"nextBillingDate": "2026-04-01T00:00:00.000Z",
"currentCycle": 1,
"customer": "cust-id-or-object",
"items": [...],
"discounts": [],
"deliveryPrice": 0,
"createdAt": "2026-03-01T00:00:00.000Z",
"updatedAt": "2026-03-01T00:00:00.000Z"
}
```
## Signature verification
All webhooks are signed by Svix. See [Signature Verification](/docs/api-reference/admin/webhooks/signature-verification) to learn how to validate incoming requests.
# Signature verification
Source: https://juo.dev/docs/api-reference/admin/webhooks/signature-verification
Verify that webhook requests come from Juo
Each webhook request includes headers that allow you to verify the signature and prevent replay attacks.
**Signature headers:**
| Header | Description |
| ---------------- | ------------------------------------------- |
| `svix-id` | Unique message ID |
| `svix-timestamp` | Unix timestamp of when the message was sent |
| `svix-signature` | Base64-encoded HMAC-SHA256 signature |
## Verifying with the Svix SDK
The easiest way to verify signatures is with an official Svix library.
```typescript Node.js theme={null}
import { Webhook } from "svix";
const secret = process.env.SVIX_SIGNING_SECRET; // from Svix dashboard
export async function handleWebhook(request: Request): Promise {
const wh = new Webhook(secret);
const payload = await request.text();
const headers = {
"svix-id": request.headers.get("svix-id") ?? "",
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
"svix-signature": request.headers.get("svix-signature") ?? "",
};
try {
const event = wh.verify(payload, headers);
// Process event...
return new Response("OK", { status: 200 });
} catch (err) {
return new Response("Invalid signature", { status: 400 });
}
}
```
```python Python theme={null}
from svix.webhooks import Webhook, WebhookVerificationError
import os
secret = os.environ["SVIX_SIGNING_SECRET"]
def handle_webhook(body: bytes, headers: dict) -> dict:
wh = Webhook(secret)
try:
event = wh.verify(body, headers)
return event
except WebhookVerificationError:
raise ValueError("Invalid signature")
```
```go Go theme={null}
import (
svix "github.com/svix/svix-webhooks/go"
"os"
)
func handleWebhook(body []byte, headers http.Header) (interface{}, error) {
secret := os.Getenv("SVIX_SIGNING_SECRET")
wh, err := svix.NewWebhook(secret)
if err != nil {
return nil, err
}
return wh.Verify(body, headers)
}
```
## Getting your signing secret
1. Open the Juo Admin Portal and navigate to **Settings → Webhooks**
2. Navigate to your endpoint
3. Copy the **Signing Secret** shown under the endpoint settings
Each endpoint has its own unique signing secret.
## Manual verification
If you prefer not to use the Svix SDK, you can verify the signature manually:
1. Construct the signed content: `{svix-id}.{svix-timestamp}.{raw-body}`
2. Compute HMAC-SHA256 using the signing secret (base64-decoded)
3. Compare the result against the `svix-signature` header (strip the `v1,` prefix)
4. Reject messages older than 5 minutes using `svix-timestamp`
See the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how-manual) for details.
# Get Shopify auth token
Source: https://juo.dev/docs/api-reference/customer/auth/shopify-token
openapi-customer.json GET /auth/shopify/token
Returns a Shopify authentication token.
# Introduction
Source: https://juo.dev/docs/api-reference/customer/introduction
## Welcome
Juo’s Customer API provides programmatic access to customer-facing subscription data.\
It's a RESTful API that lets authenticated customers interact with their subscriptions and with other customer-related entities.
Customer API SDK is available [here](https://www.npmjs.com/package/@juo/customer-api).
## Authentication
Juo's Customer API supports several authorization methods depending on the environment in which it’s used.
### Access token
You can obtain an access token by initializing the login flow with Juo. Once the customer completes the login flow, an access token will become available to be used in the `Authorization` header:
```
Authorization: Bearer
```
Contact us at [dev@juo.io](mailto:dev@juo.io) if you need help setting up the login flow in your custom portal.
### Implicit authorization (only in embedded Shopify context)
You can use implicit authorization when using Juo's Customer API in an embedded portal under a Shopify Storefront. To use the implicit authorization replace the server URL with `/apps/juo/api/customer/v1`.
### Delegated Token
You can obtain a Delegated Token through Juo's Admin API. After acquiring a valid Delegated Token, include it in the `X-Delegated-Token` header:
```
X-Delegated-Token:
```
## Pagination
The API uses cursor-based pagination to handle large result sets. Navigation links are provided in the `Link` response header, containing URLs for the next and previous pages.
```
Link: >; rel="next",
>; rel="prev"
```
The amount of results per page can be set from 1-100 with the `limit` query parameter where applicable:
```
/customer/v1/subscriptions?limit=10
```
## Sorting
Results can be sorted using the `sort` query parameter. Multiple sort criteria can be combined using commas.
Sortable fields are the same as filterable fields on a resource. There is a special `rank` field that can be used whenever a search query includes a term not bound to a specific field.
```
# Sort by creation date descending
/customer/v1/subscriptions?sort=createdAt:desc
# Sort by status ascending and then by creation date descending
/customer/v1/subscriptions?sort=status:asc,createdAt:desc
```
## Search query language
Resources can be searched / filtered by an expressive query syntax.
The search grammar is expressed in [EBNF](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). It uses the following terminal symbols:
| Symbol | Description |
| ------ | ----------------------------------------------------------- |
| \_ | Optional whitespace |
| \_\_ | Mandatory whitespace |
| field | An identifier (`[a-z][a-zA-Z]*`) |
| value | An identifier, single quoted string or double quoted string |
The syntax can be expressed with the following grammar:
```ebnf theme={null}
Query = Disjunction
Negation = ( "-" | "NOT" ) _ Parenthesis | Parenthesis
Parenthesis = "(" _ Query _ ")" | Term
Disjunction = Conjunction ( __ "OR" __ Conjunction ):+ | Conjunction
Conjunction = Negation ( __ "AND" __ Negation ):+ | Negation
Term = field Operator value | value
Operator = ":" | ":<" | ":>" | ":>=" | ":<="
```
It lets you form advanced logical queries, like in examples below:
```
# Find all active subscriptions containing a term "Blue"
status:active AND Blue
# The same as above but with quotes
status:'active' AND 'Blue'
# Find failed subscriptions with PayPal instrument along with all cancelled subscriptions after 2024
(status:failed AND instrument:paypal) OR (status:cancelled AND cancelledAt:>='2024-01-01')
# Find all but active subscriptions
- status:active
```
Below you can find the meaning of each available operator:
| Operator | Description |
| -------- | --------------------- |
| : | Equal |
| :> | Greater than |
| :>= | Greater than or equal |
| :\< | Less than |
| :\<= | Less than or equal |
## Rate limiting
The API implements a token bucket rate-limiting strategy to ensure fair usage. Each API token has a bucket that fills at a constant rate. API calls consume tokens from the bucket. When the bucket is empty, requests will be rejected until it refills.
Rate limit information is included in the response headers:
```
X-RateLimit-Limit: 1000 # How many requests the client can make
X-RateLimit-Remaining: 999 # How many requests remain to the client in the time window
X-RateLimit-Reset: 60 # How many seconds must pass before the rate limit resets
```
# List orders
Source: https://juo.dev/docs/api-reference/customer/orders/list
openapi-customer.json GET /orders
Returns a list of orders.
# Order resource
Source: https://juo.dev/docs/api-reference/customer/orders/resource
This object represents an individual transaction within a subscription.
The `Order` object is created each time a subscription is billed or fulfilled, capturing the complete details of that transaction. Orders contain information about the products or services being delivered, pricing, discounts applied, shipping details, and payment status.
### Search query fields
| Field | Description | Type |
| ----- | --------------------------------------------------------------- | ------ |
| id | The subscription identifier (accepts up to 20 subscription IDs) | string |
### Reference
# List products
Source: https://juo.dev/docs/api-reference/customer/products/list
openapi-customer.json GET /products
Returns a list of products.
# Product resource
Source: https://juo.dev/docs/api-reference/customer/products/resource
This object represents a product purchasable by customers
The `Product` object is a purchasable resource, it comes with multiple purchase options based on the subscription offers set up in your account. Customers can subscribe to a product or add them as onetime purchases to upcoming recurring orders.
### Search query fields
| Field | Description | Type |
| ------------------ | --------------------------------- | --------------------------------------------------------------------------------------- |
| id | The product identifier | string |
| title | The product title | string |
| status | The product status | enum (`ACTIVE`, `ARCHIVED`, `DRAFT`) |
| availability | The product availability | enum (`not-for-sale`, `out-of-stock`, `subscription-only`, `onetime-only`, `available`) |
| variants.id | The product's variant id | string |
| variants.title | The product's variant title | string |
| collections.handle | The product's collections' handle | string |
### Reference
# Create a schedule adjustment
Source: https://juo.dev/docs/api-reference/customer/schedule-adjustments/create
openapi-customer.json POST /schedules/adjustments
Used to create a schedule adjustment.
## Examples
Skip order for the third cycle with a reason.
```json theme={null}
{
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 3
}
},
"action": {
"type": "SKIP_ORDER",
"input": {
"reason": "Going on vacation"
}
}
}
```
Reschedule order for the second cycle to a new date.
```json theme={null}
{
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "CHANGE_DATE",
"input": {
"newDate": "2025-02-15T00:00:00Z"
}
}
}
```
`CHANGE_DATE` only supports the `CYCLE` matcher. The new date must be within one billing cycle of the original date and cannot be in the past.
Skip all orders scheduled for a specific date.
```json theme={null}
{
"matcher": {
"type": "DATE",
"input": {
"date": "2025-01-01T00:00:00Z"
}
},
"action": {
"type": "SKIP_ORDER",
"input": {
"reason": "Holiday break"
}
}
}
```
Update shipping address for a specific cycle.
```json theme={null}
{
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 3
}
},
"action": {
"type": "UPDATE_SHIPPING",
"input": {
"address": {
"firstName": "John",
"lastName": "Doe",
"address1": "123 Main St",
"city": "New York",
"countryCode": "US",
"zip": "10001"
}
}
}
}
```
Address must include at least `address1` and `countryCode`.
Update payment method for a specific cycle.
```json theme={null}
{
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 4
}
},
"action": {
"type": "UPDATE_PAYMENT_METHOD",
"input": {
"paymentMethodId": "pm_abc123"
}
}
}
```
Modify product lines for a specific cycle.
```json theme={null}
{
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "UPDATE_PRODUCTS",
"input": {
"lines": [
{
"lineId": "line_abc123",
"quantity": 2
},
{
"lineId": "line_def456",
"variantId": "variant_xyz789"
},
{
"lineId": "line_ghi789",
"customAttributes": [
{ "key": "gift_message", "value": "Happy birthday!" }
]
}
]
}
}
}
```
Each line requires `lineId`. Optional fields: `variantId` (swap variant), `quantity` (min 1), `interval` (override billing interval), `nextBillingDate`, `customAttributes` (merged onto the item by key; reserved/internal keys are rejected with a 400).
Apply a Shopify discount code to a specific cycle.
```json theme={null}
{
"matcher": {
"type": "CYCLE",
"input": {
"cycle": 2
}
},
"action": {
"type": "APPLY_DISCOUNT",
"input": {
"discountSource": "code",
"discountCode": "SAVE20"
}
}
}
```
`APPLY_DISCOUNT` only supports the `CYCLE` matcher. Discount must exist in Shopify and be valid for subscriptions.
# Delete a schedule adjustment
Source: https://juo.dev/docs/api-reference/customer/schedule-adjustments/delete
openapi-customer.json DELETE /schedules/adjustments/{adjustmentId}
Used to delete a schedule adjustment.
# List schedule adjustments
Source: https://juo.dev/docs/api-reference/customer/schedule-adjustments/list
openapi-customer.json GET /schedules/adjustments
Returns a list of schedule adjustments.
# Schedule adjustment resource
Source: https://juo.dev/docs/api-reference/customer/schedule-adjustments/resource
This object represents an adjustment that modifies schedule orders during generation.
The `ScheduleAdjustment` object allows you to customize the schedule by modifying orders before they are created. Schedule adjustments can be used to skip orders, change dates, update shipping, and more.
Define what modification to apply (skip, reschedule, update shipping, etc.)
Define which orders to target (by cycle, date, or both)
### Reference
# Get schedule
Source: https://juo.dev/docs/api-reference/customer/schedules/get
openapi-customer.json GET /schedules/
Returns a customer schedule.
# Schedule order resource
Source: https://juo.dev/docs/api-reference/customer/schedules/resource
This object represents an order within a schedule.
The `ScheduleOrder` object is a projected future order generated by the schedule system. Each schedule contains one or more orders that define when and what will be delivered to the customer. Schedule orders serve as a preview of upcoming orders before they are actually created and processed, containing information about the delivery date, items to be delivered, pricing, discounts, and whether the order has been skipped.
### Search query fields
| Field | Description | Type |
| ----- | --------------------------------------------------------------- | ------ |
| id | The subscription identifier (accepts up to 20 subscription IDs) | string |
### Reference
# Create a subscription discount
Source: https://juo.dev/docs/api-reference/customer/subscription-discounts/create
openapi-customer.json POST /subscriptions/{subscriptionId}/discounts
Used to create a subscription discount.
# Delete a subscription discount
Source: https://juo.dev/docs/api-reference/customer/subscription-discounts/delete
openapi-customer.json DELETE /subscriptions/{subscriptionId}/discounts/{discountId}
Used to delete a subscription discount.
# Subscription discount resource
Source: https://juo.dev/docs/api-reference/customer/subscription-discounts/resource
This object represents a discount applied to a subscription item.
The `SubscriptionDiscount` object is used to apply discounts to subscription items, reducing the product price for subscribers.
### Reference
# Create a subscription item
Source: https://juo.dev/docs/api-reference/customer/subscription-items/create
openapi-customer.json POST /subscriptions/{subscriptionId}/items
Used to create a subscription item.
# Delete subscription item
Source: https://juo.dev/docs/api-reference/customer/subscription-items/delete
openapi-customer.json DELETE /subscriptions/{subscriptionId}/items/{itemId}
Used to delete a subscription item.
# Subscription item resource
Source: https://juo.dev/docs/api-reference/customer/subscription-items/resource
This object represents a product included in a subscription.
The `SubscriptionItem` object defines which products are included in a subscription and how they are configured. It supports multiple variants and can be configured for specific subscription cycles, allowing customers to customize their subscription with different product options.
### Reference
# Update subscription item
Source: https://juo.dev/docs/api-reference/customer/subscription-items/update
openapi-customer.json PATCH /subscriptions/{subscriptionId}/items/{itemId}
Used to update a subscription item.
# Cancel subscription
Source: https://juo.dev/docs/api-reference/customer/subscriptions/cancel
openapi-customer.json POST /subscriptions/{subscriptionId}/cancel
Used to cancel a subscription.
# List subscriptions
Source: https://juo.dev/docs/api-reference/customer/subscriptions/list
openapi-customer.json GET /subscriptions
Returns a list of subscriptions.
# Pause subscription
Source: https://juo.dev/docs/api-reference/customer/subscriptions/pause
openapi-customer.json POST /subscriptions/{subscriptionId}/pause
Used to pause a subscription.
# Reactivate subscription
Source: https://juo.dev/docs/api-reference/customer/subscriptions/reactivate
openapi-customer.json POST /subscriptions/{subscriptionId}/reactivate
Used to reactivate a subscription.
# Subscription resource
Source: https://juo.dev/docs/api-reference/customer/subscriptions/resource
Subscription represents a contract between a business and a customer.
The `Subscription` object stores information and rules defining the contract between a business and a customer. It is created when a customer purchases a subscription-based product.
### Search query fields
| Field | Description | Type |
| --------------- | ------------------------------------------ | ------ |
| id | The subscription identifier | string |
| serial | The incremental subscription number | string |
| status | The subscription status | string |
| createdAt | The date subscription was created | date |
| updatedAt | The last date the subscription was updated | date |
| nextBillingDate | The upcoming next billing date | date |
| canceledAt | The date subscription was cancelled | date |
### Reference
# Resume subscription
Source: https://juo.dev/docs/api-reference/customer/subscriptions/resume
openapi-customer.json POST /subscriptions/{subscriptionId}/resume
Used to resume a subscription.
# Update subscription
Source: https://juo.dev/docs/api-reference/customer/subscriptions/update
openapi-customer.json PATCH /subscriptions/{subscriptionId}
Used to update a subscription.
# Cancel workflow run
Source: https://juo.dev/docs/api-reference/customer/workflows/cancel
openapi-customer.json POST /workflows/{runId}/cancel
Used to cancel an active workflow run.
# Get workflow run
Source: https://juo.dev/docs/api-reference/customer/workflows/get
openapi-customer.json GET /workflows/{runId}
Returns the current state of a workflow run.
# Workflow run resource
Source: https://juo.dev/docs/api-reference/customer/workflows/resource
This object represents the state of a running workflow.
The `WorkflowRunState` object represents the current state of a workflow run started from the Customer API, such as a guided onboarding or cancellation flow. It tracks the run's status, any interaction currently awaiting a customer response, and the outcome once the run completes.
### Reference
# Respond to a workflow run
Source: https://juo.dev/docs/api-reference/customer/workflows/respond
openapi-customer.json POST /workflows/{runId}/respond
Used to respond to a pending interaction in a workflow run.
# Start a workflow
Source: https://juo.dev/docs/api-reference/customer/workflows/start
openapi-customer.json POST /workflows/start
Used to start a new workflow run.
# Blocks
Source: https://juo.dev/docs/apps/blocks
**Apps are currently part of a private beta program and are not publicly
available.**
Access to the Apps platform requires approval from the Juo team.
Blocks are the core building components of frontend applications in Juo. Frontend applications utilize blocks to deliver custom experiences to subscribers, enabling rich, interactive user interfaces.
#### How blocks work
* **Custom Components**: Create custom UI blocks that can be published on the Juo platform
* **Portal Integration**: Blocks can be used in portals to build custom subscriber experiences
* **Reusable Components**: Blocks are reusable across different pages and contexts
* **Framework Agnostic**: Blocks work with various frontend frameworks
Blocks enable developers to:
* Create custom UI components for subscriber portals
* Build specialized subscription management interfaces
* Design branded experiences for merchants
* Extend the default portal functionality
# Overview
Source: https://juo.dev/docs/apps/overview
**Apps are currently part of a private beta program and are not publicly
available.**
Access to the Apps platform requires approval from the Juo team.
Apps are the primary mechanism for extending the Juo platform's capabilities. Developers can create Apps that add custom functionality to the platform, enabling integration with external services, custom business logic, and specialized features.
### App deployment models
Apps in Juo can be deployed in two primary ways:
* **Single-tenant apps**: Installed for and scoped to a single tenant (sometimes referred to as an "account" or "organization"). These apps provide custom functionality tailored to one tenant.
* **Multi-tenant apps**: Designed to be installed by multiple tenants, allowing developers to build reusable, scalable solutions that work across different organizations.
> **Note:** In this documentation, "tenant", "account", and "organization" are used interchangeably to describe an individual business entity or user group operating within the Juo platform. This aligns with common terminology in SaaS and platform ecosystems.
### Architecture
Apps integrate with the Juo platform through well-defined extension points, allowing them to interact with core platform services while maintaining security and isolation.
```mermaid theme={null}
%%{
init: {
'flowchart': {
'subGraphTitleMargin': {
'top': 20,
'bottom': 20
}
}
}
}%%
graph LR
subgraph "Juo Platform"
Core[Core Services]
ExtensionPoints[Extension Points]
end
subgraph "Apps"
App1[Single-Tenant App]
App2[Multi-Tenant App]
App3[Multi-Tenant App]
end
Core --> ExtensionPoints
ExtensionPoints --> App1
ExtensionPoints --> App2
ExtensionPoints --> App3
App1 --> Tenant1[Tenant A]
App2 --> Tenant1
App2 --> Tenant2[Tenant B]
App3 --> Tenant2
App3 --> Tenant3[Tenant C]
```
### Extensibility
Apps can extend the Juo platform in three main areas: Workflows, Blocks, and Portal. Each app can utilize one, some, or all of these extensibility areas depending on its requirements.
# Portals
Source: https://juo.dev/docs/apps/portals
**Apps are currently part of a private beta program and are not publicly
available.**
Access to the Apps platform requires approval from the Juo team.
The Portal extensibility area allows developers to build fully custom subscriber portals or create multiple specialized portals for different groups of subscribers. This provides complete control over the subscriber experience.
#### Portal capabilities
* **Custom portal development**: Build a fully custom subscriber portal from scratch
* **Multiple portals**: Create multiple specialized portals for different subscriber groups or use cases
* **Default portal replacement**: Replace Juo's out-of-the-box portal with your custom implementation
* **Full control**: Complete control over the portal's design, functionality, and user experience
Portals are ideal for:
* Branded subscriber experiences
* Specialized portals for different subscription types
* Custom workflows and user journeys
* Integration with existing customer-facing systems
* White-label solutions
# Workflows
Source: https://juo.dev/docs/apps/workflows
**Apps are currently part of a private beta program and are not publicly
available.**
Access to the Apps platform requires approval from the Juo team.
Workflows enable developers to run custom business logic on the Juo platform securely and efficiently. This extensibility area allows you to adjust or create business logic that executes when specific triggers occur in the platform.
#### How workflows work
* **Custom logic**: Write custom functions that implement your business rules and processes
* **WebAssembly execution**: Functions are compiled to WASI-compatible WebAssembly code for secure, efficient execution
* **Trigger-based**: Workflows execute automatically when subscribed triggers occur in the platform
* **Secure execution**: All workflow code runs in an isolated, sandboxed environment
Workflows are ideal for scenarios such as:
* Custom subscription validation rules
* Business process automation
* Integration with external systems
* Custom data transformations
* Conditional business logic execution
# Blocks
Source: https://juo.dev/docs/blocks/concepts/blocks
Self-contained UI components with typed props and slots, described by a type-safe schema.
A **block** is a self-contained UI component. It has **props** to configure it and **slots** to compose it with other blocks. A type-safe **schema** describes both — driving TypeScript inference in the renderer.
What makes a block different from a regular component is that it can consume **services** from its surrounding [contexts](/docs/blocks/concepts/contexts) to read and manipulate live store data (subscriptions, customers, orders) without prop drilling.
## Anatomy of a block
Every block has four parts:
| Part | Purpose |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Unique identifier (e.g. `"Button"`) used to reference the block from other blocks and from the registry. |
| **Schema** | JSON Schema that describes the block's `props` and `slots`. Drives type inference for the renderer. |
| **Initial value** | Function returning the default `props` / `slots` when the block is first instantiated. |
| **Renderer** | Function that takes a `BlockInstance` and returns an `HTMLElement` to mount. The framework boundary — implementations exist for plain DOM, [Vue](/docs/blocks/frameworks/vue), [React](/docs/blocks/frameworks/react), and [Preact](/docs/blocks/frameworks/preact). |
A block is **defined once** with `defineBlock`, **registered once** with `registerBlock`, and **instantiated many times** through `createBlockInstance`.
```ts theme={null}
import { defineBlock, registerBlock } from "@juo/blocks";
```
## A simple block
A button with one prop — a text label. The renderer creates a real DOM element and uses `effect()` to keep it in sync with `block.props`.
```ts theme={null}
import { defineBlock, registerBlock, effect, untracked } from "@juo/blocks";
import type { FromSchema, JSONSchema } from "json-schema-to-ts";
const schema = {
type: "object",
properties: {
props: {
type: "object",
properties: { label: { type: "string" } },
required: ["label"],
additionalProperties: false,
},
},
required: ["props"],
additionalProperties: false,
} as const satisfies JSONSchema;
export const ButtonBlock = defineBlock>("Button", {
group: "store",
schema,
initialValue: () => ({ props: { label: "Click me" } }),
renderer(block) {
const el = document.createElement("button");
effect(() => {
const { label } = block.props;
untracked(() => {
el.textContent = label;
});
});
return el;
},
});
registerBlock(ButtonBlock);
```
`as const satisfies JSONSchema` plus `FromSchema` keeps the schema and the renderer's prop types in sync — change the schema, the TS types update.
## A more advanced block
A card block with multiple typed props, nested slots, and a preset shown in the Editor's picker.
```ts theme={null}
import {
defineBlock,
registerBlock,
createBlockInstance,
effect,
untracked,
} from "@juo/blocks";
import { html, render } from "lit-html";
import type { FromSchema, JSONSchema } from "json-schema-to-ts";
const schema = {
type: "object",
properties: {
props: {
type: "object",
properties: {
variant: { enum: ["primary", "secondary", "ghost"] },
size: { enum: ["sm", "md", "lg"] },
showThumbnail: { type: "boolean" },
},
required: ["variant", "size", "showThumbnail"],
additionalProperties: false,
},
slots: {
type: "object",
properties: { header: {}, body: {}, actions: {} },
required: ["header", "body", "actions"],
additionalProperties: false,
},
},
required: ["props", "slots"],
additionalProperties: false,
} as const satisfies JSONSchema;
export const SubscriptionCardBlock = defineBlock>(
"SubscriptionCard",
{
group: "juo",
schema,
initialValue: () => ({
props: { variant: "primary", size: "md", showThumbnail: true },
slots: {
header: createBlockInstance("Text", { slots: { default: "Your plan" } }),
body: [],
actions: [
createBlockInstance("Button", { props: { label: "Manage" } }),
],
},
}),
presets: {
compact: {
label: "Compact",
description: "Smaller card with no thumbnail",
group: "Layouts",
state: { props: { variant: "ghost", size: "sm", showThumbnail: false } },
context: {},
},
},
renderer(block) {
const el = document.createElement("div");
effect(() => {
const { variant, size, showThumbnail } = block.props;
untracked(() => {
render(
html`
${showThumbnail ? html`` : null}
`,
el,
);
// mount slot children into the prepared sockets
mountSlot(el.querySelector(".card__header"), block.slots?.header);
mountSlot(el.querySelector(".card__body"), block.slots?.body);
mountSlot(el.querySelector(".card__actions"), block.slots?.actions);
});
});
return el;
},
},
);
registerBlock(SubscriptionCardBlock);
```
Highlights:
* **Typed props.** `variant`, `size`, and `showThumbnail` are fully typed inside the renderer.
* **Slots.** A slot can be a single block, an array of blocks, or a plain string. `createBlockInstance` builds a nested block by name and merges overrides into its default `initialValue`.
* **Presets.** Each preset provides a complete `state` snapshot (props + slot contents) — a named starting point for a block instance.
## Block groups
Every block declares a `group` that identifies its origin:
| Group | Origin |
| ------------- | --------------------------------------------- |
| **1st party** | Blocks provided by Juo. |
| **3rd party** | Blocks provided by partners and integrations. |
| **Store** | Blocks published by the portal owner. |
All three groups are first-class — there's no behavioural difference between them.
## Registering blocks
`registerBlock` adds the definition to the registry so it can be instantiated by name. Call it once at the top of the package entry file:
```ts theme={null}
// src/main.ts
import { registerBlock } from "@juo/blocks";
import { ButtonBlock } from "./Button";
import { SubscriptionCardBlock } from "./SubscriptionCard";
export function registerBlocks() {
registerBlock(ButtonBlock);
registerBlock(SubscriptionCardBlock);
}
```
After registration, the block becomes available wherever the runtime is loaded.
# Contexts
Source: https://juo.dev/docs/blocks/concepts/contexts
Type-safe dependency injection that lets any block read services and shared state without prop drilling.
Contexts let any block in the tree access a shared value — a service, a state signal, the active workflow — without prop drilling and without a global store. Values flow through the DOM via a W3C-style request/response protocol implemented by the `` custom element.
```
← host provides contexts here
└─ ← renders the current route's root block
└─ ← slot fan-out
└─ ← blocks request contexts via injectContext
```
## Providing a context
Call `provideContext()` once at startup, before blocks render:
```ts theme={null}
import {
provideContext,
SubscriptionServiceContext,
createSubscriptionService,
createApiSubscriptionAdapter,
} from "@juo/blocks";
const root = document.querySelector("juo-context-root") as HTMLElement;
provideContext(
root,
SubscriptionServiceContext,
createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);
```
See [Services](/docs/blocks/concepts/services) for the full list of contexts the platform expects.
## Consuming a context
In a vanilla renderer, use `injectContext()` for a one-shot synchronous read against the surrounding DOM:
```ts theme={null}
import { injectContext, SubscriptionServiceContext } from "@juo/blocks";
renderer(block) {
const el = document.createElement("div");
const subscription = injectContext(el, SubscriptionServiceContext);
// ...
return el;
}
```
Framework components use a `useContext` composable / hook that wires signals into native reactivity — see the [Vue](/docs/blocks/frameworks/vue), [React](/docs/blocks/frameworks/react), and [Preact](/docs/blocks/frameworks/preact) guides.
## Reactive updates
If the provided value can change over time, use `subscribeContext`:
```ts theme={null}
import { subscribeContext, ThemeStateContext } from "@juo/blocks";
const unsubscribe = subscribeContext(element, ThemeStateContext, (theme) => {
// Called immediately with the current value, then on each change
applyTheme(el, theme.globalStyles.value);
});
// Call unsubscribe() when the element is removed
```
In Vue/React/Preact components, this isn't needed — the framework `useContext` already bridges signal updates into the host framework's reactivity. See [Reactivity](/docs/blocks/concepts/reactivity).
## Scoping and overrides
Every `` element captures `context-provide` and `context-request` events at its own boundary — providers are stored on the root that catches the event, and requests are answered by the **nearest** root above the requester. That makes contexts naturally scoped.
Any context can be overridden for a sub-tree by nesting a second `` and providing a different value on it. Contexts that the inner root **does not** re-provide fall through to the outer scope.
```html theme={null}
```
```ts theme={null}
const appRoot = document.getElementById("app-root")!;
const previewRoot = document.getElementById("preview-root")!;
// ContextA and ContextB live on the outer root
provideContext(appRoot, ContextA, valueA);
provideContext(appRoot, ContextB, valueB);
// The inner root overrides ContextA only
provideContext(previewRoot, ContextA, valueAOverride);
// Inside the inner root:
// injectContext(el, ContextA) → valueAOverride (stops at the nearest root)
// injectContext(el, ContextB) → valueB (bubbles up to the outer root)
```
Typical uses:
* **Previews and Storybook.** Wrap a block in a nested root that provides mock services.
* **Workflow overlays.** The flow overlay creates its own root so it can swap in a flow-specific `WorkflowService` and `PageContext` without disturbing the rest of the page.
* **Tenant or surface scoping.** Different sections of the page can run against different adapters.
Overrides are per-context — anything not re-provided on the inner root still falls through to the outer scope, because the inner root has no entry for that context and the request bubbles up.
## Defining a custom context
```ts theme={null}
import { createContext, provideContext, injectContext } from "@juo/blocks";
// Define the key once, export it from a shared module
export const CartContext = createContext("cart");
// Provide at root
provideContext(rootElement, CartContext, myCartService);
// Inject anywhere in the tree
const cart = injectContext(element, CartContext);
```
`@juo/blocks` ships context keys for every built-in [service](/docs/blocks/concepts/services) — `SubscriptionServiceContext`, `CustomerServiceContext`, and so on. Define a custom context with `createContext` when sharing something beyond the built-in services.
# Editor
Source: https://juo.dev/docs/blocks/concepts/editor
How the Juo Editor lets non-technical operators manage portals and blocks built with @juo/blocks.
The **Editor** is a separate tool, provided by Juo, that lets store operators manage portals and blocks built with `@juo/blocks`. Operators configure blocks without writing code — they select blocks from a library, place them onto pages, fill in props through generated forms, and translate copy, all against the same definitions a developer writes once with `defineBlock`.
This page covers how a portal integrates with the Editor and what that integration unlocks for the operator. For the step-by-step wiring, see [Building a custom portal](/docs/blocks/guides/custom-portal).
## What the operator gets
A portal that opts into Editor support gives its operators:
* **A block picker** with every block the portal registers, grouped by 1st party, 3rd party, and store-owned (see [Blocks → Block groups](/docs/blocks/concepts/blocks#block-groups)).
* **Auto-generated forms** for each block's props, driven by the block's schema — no separate admin UI to maintain.
* **Slot composition** — nest blocks inside other blocks' slots directly on the canvas.
* **Presets** — curated starting points an author defines on a block, shown as previews in the picker.
* **Theming and translations** managed alongside content, without redeploying the portal.
* **Live preview** — every change the operator makes renders against the real portal with real services.
Because all of the above is driven by the same block definitions, theme, and services that ship in production, what the operator sees in the Editor is what visitors get on the storefront.
## How the integration works
The Editor communicates with the portal through a small protocol. Two pieces in `@juo/blocks` make a portal Editor-aware:
* **The editor build of the web components.** `@juo/blocks/web-components/editor` replaces the standard versions of ``, ``, ``, ``, and `` with variants that connect selection, inline text editing, and drag handles. The runtime build (`@juo/blocks/web-components/runtime`) is the lean storefront version with none of that. See [Web components](/docs/blocks/concepts/web-components) for what each element does.
* **`setupEditorMode`** from `@juo/blocks/editor` — a single call that hands the Editor the portal's block registrations, its routes, and a bridge for theme and translation updates. Call it once from the portal entry and the rest of the integration follows.
A block author rarely cares which build is running. The same `defineBlock` works in both. The differences are at the edges: `` is editable in the Editor and read-only at runtime, blocks render inside a selection wrapper in the Editor, and previewed presets arrive through the bridge.
If a block needs to react to those previews specifically — for instance, to render a hovered preset without committing — it can opt in with `useBridge` from its framework adapter. See [Building a custom portal](/docs/blocks/guides/custom-portal#preset-previews-with-usebridge) for the pattern.
# Localization
Source: https://juo.dev/docs/blocks/concepts/localization
Ship blocks with built-in translations, override them per theme or per block instance.
A block declares the strings it owns, ships catalogs for the locales it supports, and reads translations through `TranslationContext`. The portal switches locales, the theme adds overrides, and a single merchant can rewrite one string on one block instance — all without the block author writing any glue code.
## Where translations live
There are four layers of resolution, applied in order until one wins:
1. **Block instance override** — the merchant rewrote this string on this specific block in the editor.
2. **Block default in override mode** — the block ships its own default.
3. **Theme override** — the theme rewrote this key across all blocks.
4. **Theme default** — the theme's own translation catalog.
If no layer has a value, the resolver returns the key itself.
## Declaring locales on a block
A block opts in by adding a `locales` field to `defineBlock` — a list of supported codes and an async loader:
```ts theme={null}
import { defineBlock } from "@juo/blocks";
export const CheckoutBlock = defineBlock("Checkout", {
group: "theme",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
locales: {
supported: ["en", "de", "nl"],
async load(locale) {
// Each file exports a flat object of { key: "translated string" }.
return (await import(`./locales/${locale}.json`)).default;
},
},
renderer(block) { /* ... */ },
});
```
A locale file is plain JSON:
```json theme={null}
// locales/de.json
{
"checkout.title": "Zur Kasse",
"checkout.cta": "Bestellen",
"overrides": {
"checkout.cta": "Jetzt bestellen"
}
}
```
Top-level keys are the **block defaults**. The optional `overrides` object lets a block ship preferred wording for keys it defines — used by the resolver when the theme has no opinion.
## Reading translations
Resolve a translation through `TranslationContext`. From a vanilla renderer:
```ts theme={null}
import { defineBlock, effect, untracked, injectContext } from "@juo/blocks";
import { TranslationContext } from "@juo/blocks";
defineBlock("Checkout", {
// ...
renderer(block) {
const el = document.createElement("button");
const t = injectContext(el, TranslationContext);
effect(() => {
// Reading t.locale.value re-runs this effect on locale change
const _locale = t.locale.value;
const label = t.resolve(block.type, block.id, "checkout.cta");
untracked(() => {
el.textContent = label;
});
});
return el;
},
});
```
`resolve(blockType, blockId, key)` takes the block's type and instance id so it can apply the per-instance overrides set by the merchant.
### From a framework
Vue, React, and Preact all reach `TranslationContext` through their `useContext` — see the [Vue](/docs/blocks/frameworks/vue), [React](/docs/blocks/frameworks/react), and [Preact](/docs/blocks/frameworks/preact) guides.
## Using `` for inline text
For inline translated strings rendered directly in HTML, `` is a declarative alternative to wiring up `TranslationContext` by hand. It is a custom element that handles resolution and reactivity internally:
```html theme={null}
Place order
```
The element reads the `prop` attribute as the translation key, walks up the DOM to find the nearest block wrapper (`data-block-type` / `data-block-id`), injects `TranslationContext`, and re-renders whenever the locale changes. The text content is used as a fallback: if the key resolves to itself (nothing matched), the element shows the fallback instead.
From a vanilla renderer:
```ts theme={null}
renderer(block) {
const btn = document.createElement("button");
btn.innerHTML = `Place order`;
return btn;
},
```
From Preact or React (using `h` directly):
```tsx theme={null}
h("juo-text", { prop: "checkout.cta" }, props.cta)
```
### `hide-parent-if-empty`
Add the `hide-parent-if-empty` attribute to automatically set `hidden` on the element's parent when the resolved text is empty:
```html theme={null}
Free shipping on all orders
```
The `
` is hidden entirely when the merchant has cleared the subtitle — no empty whitespace or layout gap remains.
## Resolution order in detail
The translation service walks the catalog from most-specific to most-general:
| Step | Source | Wins when |
| ---- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| 1 | `catalogs.blocks.overrides[blockId][key]` | The merchant overrode this string on this block instance. |
| 2 | `catalogs.blocks.defaults[blockType][key]` (mode `"override"`) | The block ships a default and prefers it over the theme. |
| 3 | `catalogs.theme.overrides[key]` | The theme rewrote this key for the whole portal. |
| 4 | `catalogs.theme.defaults[key]` (or `loadDefaultTranslations`) | The theme provides a translation. |
| 5 | `catalogs.blocks.defaults[blockType][key]` (mode `"default"`) | The block has a fallback default. |
| 6 | The key itself | Nothing matched — a developer signal that something's missing. |
A block can flag a key as `"default"` rather than `"override"` to let the theme's wording win when both exist — useful for generic strings like `"add_to_cart"` that the theme is likely to style consistently.
## Setting up `TranslationContext`
The portal creates one translation service per `` and feeds it the theme's locale state:
```ts theme={null}
import {
provideContext,
createTranslationService,
TranslationContext,
ThemeStateContext,
createThemeState,
} from "@juo/blocks";
const themeState = createThemeState({ /* ... */ });
provideContext(root, ThemeStateContext, themeState);
provideContext(
root,
TranslationContext,
createTranslationService(themeState.locales, {
async loadDefaultTranslations(locale) {
// Theme-wide catalog, e.g. shipped with the theme package.
return (await import(`./theme-locales/${locale}.json`)).default;
},
}),
);
```
`themeState.locales` exposes `locale` (a `Signal`), `catalogs` (the merged block + theme catalogs for the active locale), and a `setLocale` setter. Changing the locale reloads catalogs and re-runs every `effect()` that read `t.locale.value`.
## Switching locales
The portal exposes a locale switcher by reading and writing `themeState.locales`:
```ts theme={null}
import { injectContext, ThemeStateContext } from "@juo/blocks";
const theme = injectContext(el, ThemeStateContext);
theme.locales.setLocale("de"); // triggers catalog reload
console.log(theme.locales.locale.value); // "de"
```
Every block that read a translation through `effect()` updates automatically.
## Overriding theme strings from a portal
When a portal wants to ship its own translations for keys other blocks own, pass them through `loadDefaultTranslations` — the result feeds the `theme.defaults` layer used by step 4 of the resolver. For per-key forced overrides (step 3), populate `catalogs.theme.overrides` from the theme state.
## Tips
* **One key, one block.** Namespace keys by block (`checkout.cta`, `cart.empty`). Resolution is global, but lint-style discipline keeps catalogs readable.
* **Ship `en` always.** It's the safe fallback when a locale file fails to load.
* **Test against `untracked()`.** To get a translated string inside a render that already depends on `block.props`, wrap the `resolve()` call so the DOM write doesn't tangle dependencies with the props read.
# Reactivity
Source: https://juo.dev/docs/blocks/concepts/reactivity
Signal-based reactive state that flows through every framework adapter.
`@juo/blocks` is reactive end-to-end. Block props, service state, theme tokens, and workflow status are all reactive signals — when one changes, the renderers that depend on it re-run automatically.
## One reactivity model, many frameworks
`@juo/blocks` exports a set of reactive primitives — `signal`, `computed`, `effect`, `untracked` — that work the same way regardless of which renderer you use. The API follows the [TC39 Signals proposal](https://github.com/tc39/proposal-signals), so the mental model will feel familiar. Each renderer bridges these primitives in the way that feels most natural for that framework:
| Renderer | Pattern |
| -------- | ----------------------------------------------------------------------------------- |
| Vanilla | `signal()`, `effect()`, `computed()` directly |
| Vue | Signals appear as `Ref`s through the `useContext` proxy — use them like any Vue ref |
| React | `useSignal(signal)` returns the current value and re-renders on change |
| Preact | `useSignal(signal)` — same shape as React, Preact's hooks runtime |
A service can update a signal from anywhere — a fetch callback, a websocket, a workflow response — and the change propagates through **every** renderer's output without manual wiring.
## Core primitives
These are re-exported from `@juo/blocks` for use in vanilla renderers, services, and tests.
### `signal(initialValue)`
A writable observable. Reads inside an `effect` or `computed` register a dependency automatically.
```ts theme={null}
import { signal } from "@juo/blocks";
const count = signal(0);
count.value++; // triggers dependents
console.log(count.value); // 1
```
### `computed(fn)`
A read-only derived value, recomputed lazily.
```ts theme={null}
import { signal, computed } from "@juo/blocks";
const price = signal(1000);
const tax = computed(() => price.value * 0.21);
tax.value; // 210
```
### `effect(fn)` + `untracked(fn)`
`effect` runs the callback immediately and re-runs it whenever a signal it read from changes. `untracked` reads a signal value **without** registering it as a dependency — useful when the read is incidental to the work.
```ts theme={null}
import { effect, untracked } from "@juo/blocks";
effect(() => {
const status = subscription.status.value; // tracked
untracked(() => log("status changed", status));
});
```
## Where signals show up
Signals appear in three places:
1. **Service state.** Every service exposes its mutable state as signals — `subscriptionService.current`, `workflowService.canRespond`, `customerService.current`. See [Services](/docs/blocks/concepts/services).
2. **Theme tokens.** The active theme palette is a signal, so blocks re-render when the merchant changes the theme.
3. **Block props.** Inside renderers, `block.props` is reactive — wrap reads in `effect()` to keep the DOM in sync.
## Vanilla example
A renderer that prints the current customer's name and updates whenever the service signal changes:
```ts theme={null}
import { defineBlock, effect, untracked, injectContext } from "@juo/blocks";
import { CustomerServiceContext } from "@juo/blocks";
defineBlock("Greeting", {
group: "theme",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
renderer(block) {
const el = document.createElement("p");
const customer = injectContext(el, CustomerServiceContext);
effect(() => {
const name = customer.current.value?.name ?? "guest";
untracked(() => {
el.textContent = `Welcome back, ${name}`;
});
});
return el;
},
});
```
When the host fetches the customer (`customer.getCurrent()`), the signal updates and the `
` re-renders. No manual subscription, no glue code.
## Framework bridges
Framework adapters handle the bridging. Calling `signal.value` manually inside components is not required:
* **Vue.** `useContext()` returns a proxy that converts each `Signal` field into a `Ref`.
* **React / Preact.** `useSignal(signal)` reads the value and subscribes the component to changes.
See the [Vue](/docs/blocks/frameworks/vue), [React](/docs/blocks/frameworks/react), and [Preact](/docs/blocks/frameworks/preact) guides for the framework-specific shape.
## Creating reactive state in a service
When a service needs to expose reactive values, create signals internally and return them:
```ts theme={null}
import { signal, computed } from "@juo/blocks";
export function createCartService() {
const items = signal([]);
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0),
);
return {
items,
total,
add(item: CartItem) { items.value = [...items.value, item]; },
};
}
```
Consumers in any framework see the same reactive values and remain synchronized without additional wiring.
# Services
Source: https://juo.dev/docs/blocks/concepts/services
Domain APIs that blocks call to read and mutate data.
Services are the data layer for blocks. Each service abstracts a domain — subscriptions, customers, orders, products, schedules, auth, routing, workflows — behind a stable interface so blocks stay decoupled from any particular backend.
## Pattern
Every service follows the same shape:
```ts theme={null}
const service = createService(adapter);
```
The **adapter** is the I/O layer. Real adapters call the application's API; mock adapters return static data for development. The service wraps the adapter, manages reactive state, and exposes a stable interface to blocks.
Provide each service once on the `` element:
```ts theme={null}
import {
provideContext,
createSubscriptionService,
createApiSubscriptionAdapter,
SubscriptionServiceContext,
} from "@juo/blocks";
const root = document.querySelector("juo-context-root") as HTMLElement;
provideContext(
root,
SubscriptionServiceContext,
createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);
```
Blocks then call `injectContext(element, SubscriptionServiceContext)` to retrieve it. See [Contexts](/docs/blocks/concepts/contexts) for how that flows through the DOM.
## Result type
All async service methods return a discriminated union — check `_tag` before reading data:
```ts theme={null}
const result = await subscriptionService.getById(id);
if (result._tag === "Success") {
console.log(result.data);
} else {
console.error(result.error);
}
```
## Reactive state
Where it matters, services expose their state as signals — `subscriptionService.current`, `customerService.current`, `workflowService.isLoading`. Blocks read these signals and re-render automatically. See [Reactivity](/docs/blocks/concepts/reactivity).
## Adapters: real vs mock
Every service ships with a mock adapter for development and Storybook:
```ts theme={null}
import {
createSubscriptionService,
createMockSubscriptionAdapter,
} from "@juo/blocks";
provideContext(
root,
SubscriptionServiceContext,
createSubscriptionService(createMockSubscriptionAdapter()),
);
```
## Writing a custom adapter
If the backend doesn't match the default adapter, implement the adapter interface and pass it to the service factory:
```ts theme={null}
import type { SubscriptionAdapter } from "@juo/blocks";
import {
createSubscriptionService,
SubscriptionServiceContext,
provideContext,
} from "@juo/blocks";
const myAdapter: SubscriptionAdapter = {
async search() { /* ... */ },
async getById(id) { /* ... */ },
async pause(id) { /* ... */ },
// ...
};
provideContext(root, SubscriptionServiceContext, createSubscriptionService(myAdapter));
```
## Available services
Read, mutate, pause, resume, cancel subscriptions; manage items, discounts, upsells.
Current customer profile and reactive customer state.
Order history and details.
Product catalogue lookup and search with variants.
Upcoming orders, skip / reschedule.
Passwordless and social login, token management.
In-page navigation, decoupled from a specific router.
Interactive flow execution — start, respond, observe.
# Web components
Source: https://juo.dev/docs/blocks/concepts/web-components
The custom elements that wire blocks into the DOM — when to reach for each one.
`@juo/blocks` ships a small set of custom elements that connect block definitions to the page. These are the only `@juo/blocks` APIs called from HTML; everything else (services, contexts, signals) lives in JavaScript.
| Element | Role |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| `` | Boundary for context providers and requests. |
| `` | Root page renderer. Resolves a route through `ThemeState` and renders the resulting block. |
| `` | Slot fan-out. Renders the children a merchant placed into a named slot. |
| `` | Declarative ad-hoc instance of a registered block, parameterized by HTML attributes. |
| `` | Inline text node. Read-only at runtime; editable inline in the editor. |
Two builds ship the same tag set:
* `@juo/blocks/web-components/runtime` — lean storefront elements.
* `@juo/blocks/web-components/editor` — same elements plus selection wrappers, drop zones, and inline-text editing.
Import one or the other from the portal entry. See [Editor](/docs/blocks/concepts/editor) for how the editor build layers on top.
## ``
Hosts the context tree. Providers call [`provideContext`](/docs/blocks/concepts/contexts#providing-a-context) against it; blocks below it call `injectContext` and the request is answered by the nearest root above the requester.
```html theme={null}
```
Nest a second root to override services for a sub-tree. See [Scoping and overrides](/docs/blocks/concepts/contexts#scoping-and-overrides) for the pattern and the typical uses (previews, workflow overlays, tenant scoping).
## ``
The root page renderer. Subscribes to `ThemeStateContext`, asks it to resolve a route, and renders the resulting first block reactively. When the theme state's blocks change — because the editor updated a prop, a translation flipped, or the route changed — the page re-renders automatically.
```html theme={null}
```
**Attributes**
* `route` — *optional*. When set, the element calls `themeState.resolve(surface, route)` as soon as a `ThemeState` becomes available, and again whenever the attribute changes. Omit it to drive resolution from JavaScript (e.g. from a custom router).
* `surface` — *optional*. `"page"` (default) or `"overlay"`. Selects which `ThemeState` surface to render.
`` does not take a block name — what it renders is whatever `ThemeState.resolve()` returns for the given path. That keeps routing in one place (`RouterService` / theme state) instead of duplicated across markup.
## ``
Renders the children placed into a named slot of the surrounding page's root block. Page renderers emit one `` per slot they expose; the editor decorates each child with selection chrome via a wrapper.
```ts theme={null}
// Inside a page block's renderer
const container = document.createElement("div");
container.innerHTML = `
`;
return container;
```
**Attributes**
* `name` — *required*. The slot name to render. Must match a key in the page block's `slots` schema.
* `surface` — *optional*. `"page"` (default) or `"overlay"`. Selects which surface to read children from.
`` reads from the same `ThemeState` that `` resolves, so nothing needs to be passed through props — child blocks just appear when the merchant places them in that slot.
## ``
Mounts a fresh ad-hoc instance of a registered block, parameterized by its HTML attributes. Useful for dropping a block into static markup — a one-off mount in a sidebar, a Storybook story, a hand-written page that isn't driven by theme state.
```html theme={null}
```
**Attributes**
* `name` — *required*. The registered block name (the value passed to `defineBlock`/`registerBlock`).
* Any other attribute is forwarded as a prop on the block's instance.
`` is **not** what renders pages from theme state — for that use ``. Use `` only to drop a block by name into markup independently of routing or stored slot composition.
## ``
An inline text node. In the runtime build it's a passive container — it shows its text content. In the editor build it becomes editable inline, with its content flowing through the theme state's translation catalog.
```html theme={null}
Manage subscription
```
Block renderers typically emit `` for any merchant-editable copy. See [Localization](/docs/blocks/concepts/localization) for how the key resolves against the active locale's catalog.
## Composition
A typical page rendered through the platform looks like:
```
← services and ThemeState provided here
← resolves the route, renders the root block
← the root block's renderer output
← slot fan-out
```
The root block, the children in each slot, the prop values, and the translations all come from the same `ThemeState` — the only thing pinned in markup is the route. Editing in the Juo Editor updates the theme state; everything below `` re-renders automatically.
`` doesn't appear in this tree because page rendering goes through theme state, not through ad-hoc instances. Use `` only for a one-off mount that is not part of an editable page.
# Preact
Source: https://juo.dev/docs/blocks/frameworks/preact
Render blocks with Preact. Same hook shape as React, with a smaller runtime.
`@juo/blocks/preact` mirrors the React adapter but uses Preact's renderer and hook runtime. Use it for the React-style component model with a smaller payload — typical for checkout extensions and theme overlays.
```sh theme={null}
pnpm add preact
```
The patterns below extend the [Blocks](/docs/blocks/concepts/blocks), [Reactivity](/docs/blocks/concepts/reactivity), and [Contexts](/docs/blocks/concepts/contexts) concept pages with Preact-specific equivalents.
## Defining a Preact block
```tsx theme={null}
import { defineBlock, registerBlock } from "@juo/blocks";
import { createPreactRenderer } from "@juo/blocks/preact";
import { Toast } from "./Toast";
export const ToastBlock = defineBlock("Toast", {
group: "theme",
schema: /* ... */,
initialValue: () => ({ props: { tone: "info", message: "Saved" } }),
renderer: createPreactRenderer(Toast),
});
registerBlock(ToastBlock);
```
```tsx theme={null}
// Toast.tsx (Preact)
export function Toast({ tone, message }: { tone: string; message: string }) {
return
{message}
;
}
```
## Reading services with `useContext`
Identical surface to the React adapter — `useContext` returns a proxy that exposes signals as `[value, setValue]` tuples:
```tsx theme={null}
import { useContext } from "@juo/blocks/preact";
import { CustomerServiceContext } from "@juo/blocks";
export function CustomerGreeting() {
const customer = useContext(CustomerServiceContext);
const [current] = customer.current;
return
Hello, {current?.name ?? "guest"}
;
}
```
## `useSignal`
```tsx theme={null}
import { useSignal } from "@juo/blocks/preact";
import { signal } from "@juo/blocks";
const counter = signal(0);
export function Counter() {
const value = useSignal(counter);
return ;
}
```
## Providing contexts
```tsx theme={null}
import { useProvideContext } from "@juo/blocks/preact";
import { CartContext, createCart } from "./cart";
export function CartProvider({ children }) {
useProvideContext(CartContext, createCart());
return <>{children}>;
}
```
## React vs Preact
The two adapters expose the same APIs. Choose Preact when:
* A smaller bundle is needed (checkout UI extensions, theme widgets).
* The project is already in a Preact context (some Shopify surfaces).
* React-only ecosystem packages aren't required.
Otherwise, prefer React for broader ecosystem compatibility.
# React
Source: https://juo.dev/docs/blocks/frameworks/react
React 18+ adapter for rendering blocks as components and reading reactive state with useSignal.
`@juo/blocks/react` is the React 18+ adapter. It exposes `createReactRenderer` and a set of hooks for context and signal interop.
```sh theme={null}
pnpm add react react-dom
```
The patterns below extend the [Blocks](/docs/blocks/concepts/blocks), [Reactivity](/docs/blocks/concepts/reactivity), and [Contexts](/docs/blocks/concepts/contexts) concept pages with React-specific equivalents.
## Defining a React block
```tsx theme={null}
// Banner/index.ts
import { defineBlock, registerBlock } from "@juo/blocks";
import { createReactRenderer } from "@juo/blocks/react";
import { Banner } from "./Banner";
export const BannerBlock = defineBlock("Banner", {
group: "theme",
schema: /* ... */,
initialValue: () => ({ props: { tone: "info", message: "Welcome back" } }),
renderer: createReactRenderer(Banner),
});
registerBlock(BannerBlock);
```
```tsx theme={null}
// Banner/Banner.tsx
export function Banner({ tone, message }: { tone: string; message: string }) {
return
{message}
;
}
```
`createReactRenderer` mounts the component inside a `ContextProvider` that resolves context from the surrounding ``. Hooks inside the tree can then call `useContext` and `useSignal`.
## Reading services with `useContext`
`useContext` (the one from `@juo/blocks/react`, not React's own) returns a proxy where every `Signal` becomes a `[value, setValue]` tuple driven by `useSyncExternalStore`. Components re-render when accessed fields change:
```tsx theme={null}
import { useContext } from "@juo/blocks/react";
import { SubscriptionServiceContext } from "@juo/blocks";
export function SubscriptionStatus() {
const subscription = useContext(SubscriptionServiceContext);
// subscription.current is [Subscription | null, setter]
const [current] = subscription.current;
const status = current?.status ?? "unknown";
return (
Status: {status}
);
}
```
The `$` accessor returns the raw service when direct signal access is required:
```tsx theme={null}
const subscription = useContext(SubscriptionServiceContext);
const rawSignal = subscription.$.current; // Signal
```
## `useSignal` — read any signal as React state
For signals that aren't part of a context (a service-internal signal, a `computed`, an externally created one), `useSignal` bridges to React state:
```tsx theme={null}
import { useSignal } from "@juo/blocks/react";
import { signal, computed } from "@juo/blocks";
const counter = signal(0);
const doubled = computed(() => counter.value * 2);
export function Counter() {
const value = useSignal(counter);
const x2 = useSignal(doubled);
return (
);
}
```
## Providing contexts from React
Use `useProvideContext` inside a wrapper component:
```tsx theme={null}
import { useProvideContext } from "@juo/blocks/react";
import { CartContext, createCart } from "./cart";
export function CartProvider({ children }: { children: React.ReactNode }) {
useProvideContext(CartContext, createCart());
return <>{children}>;
}
```
# Vue
Source: https://juo.dev/docs/blocks/frameworks/vue
Render blocks with Vue 3 components. Signals appear as refs through useContext.
`@juo/blocks/vue` is the Vue 3 adapter. It exposes a `createVueRenderer` for the block renderer slot and a `useContext` composable that returns service signals as Vue refs.
```sh theme={null}
pnpm add vue
```
The patterns below extend the [Blocks](/docs/blocks/concepts/blocks), [Reactivity](/docs/blocks/concepts/reactivity), and [Contexts](/docs/blocks/concepts/contexts) concept pages with Vue-specific equivalents.
## Defining a Vue block
```ts theme={null}
// Button/index.ts
import { defineBlock, registerBlock } from "@juo/blocks";
import { createVueRenderer } from "@juo/blocks/vue";
import Button from "./Button.ce.vue";
export const ButtonBlock = defineBlock("Button", {
group: "theme",
schema: /* ... */,
initialValue: () => ({ props: { variant: "primary", label: "Subscribe" } }),
renderer: createVueRenderer(Button),
});
registerBlock(ButtonBlock);
```
```vue theme={null}
```
`createVueRenderer` mounts the component inside a ``-aware wrapper. Children rendered by Vue can call `useContext` and resolve the host's services.
## Reading services with `useContext`
`useContext` returns a proxy where every `Signal` field appears as a `Ref` — drop it into templates, `computed`, and `watch` like any Vue ref.
```vue theme={null}
Status: {{ status }}
```
The `$` accessor returns the underlying signal when direct signal access is required:
```ts theme={null}
const subscription = useContext(SubscriptionServiceContext);
const raw = subscription.$.current; // Signal
```
## Providing contexts from Vue
Use `useProvideContext` inside a wrapping component:
```vue theme={null}
```
## Bridging Vue refs and signals
The `@juo/blocks/vue` entry also exposes `toSignal` and `toRef` for the rare cases where crossing the boundary manually is needed:
```ts theme={null}
import { toSignal, toRef } from "@juo/blocks/vue";
import { ref } from "vue";
const myRef = ref(0);
const myRefAsSignal = toSignal(myRef); // Signal
import { signal } from "@juo/blocks";
const mySignal = signal("hello");
const mySignalAsRef = toRef(mySignal); // Ref
```
These utilities are available for cases where crossing the boundary manually is required. The `useContext` proxy returns refs for most use cases.
# Getting started
Source: https://juo.dev/docs/blocks/getting-started
Scaffold a blocks package with create-juo and start defining store-specific blocks for the Juo Editor.
The recommended starting point for `@juo/blocks` is publishing **store-owned blocks** — the "store" group in the [block picker](/docs/blocks/concepts/blocks#block-groups) — using the [`create-juo`](https://www.npmjs.com/package/create-juo) CLI. It scaffolds a package with `defineBlock`, `registerBlock`, schema/preset/locale conventions, and a build configured for publishing against the merchant's storefront portal hosted by Juo.
Building a host application (a custom page shell, services, and routes) is a separate path — see [Building a custom portal](/docs/blocks/guides/custom-portal).
## Scaffold a package
```sh npm theme={null}
npm create juo@latest
```
```sh pnpm theme={null}
pnpm create juo@latest
```
```sh yarn theme={null}
yarn create juo
```
```sh bun theme={null}
bun create juo
```
Follow the prompts to name the package and choose a framework (plain DOM, Vue, React, or Preact). Then:
```sh theme={null}
cd my-blocks
npm install
npm run dev
```
## Add to an existing project
To add `@juo/blocks` to an existing package:
```sh npm theme={null}
npm install @juo/blocks
```
```sh pnpm theme={null}
pnpm add @juo/blocks
```
```sh yarn theme={null}
yarn add @juo/blocks
```
```sh bun theme={null}
bun add @juo/blocks
```
`@juo/blocks` lists `vue`, `react`, and `preact` as **optional peer dependencies**. Install only the one the host application already runs on — or none, if rendering with plain DOM.
From here, see [Blocks](/docs/blocks/concepts/blocks) to start defining blocks.
# Building a custom portal
Source: https://juo.dev/docs/blocks/guides/custom-portal
Build a host application that renders blocks in production and integrates with the Juo Editor.
A **portal** is the host application that mounts ``, provides services, registers blocks, and decides what to render where. Themes ship one. So can extensions and standalone storefront surfaces. This guide walks through building a portal from scratch, then layering on the editor integration.
Prefer to learn from a working example? The [sample-beauty-portal](https://github.com/juo/sample-beauty-portal) repository is a complete reference portal — registered blocks, services wired up, editor mode enabled — to clone and adapt.
## Install
`@juo/blocks` ships as a single package:
```sh npm theme={null}
npm install @juo/blocks
```
```sh pnpm theme={null}
pnpm add @juo/blocks
```
```sh yarn theme={null}
yarn add @juo/blocks
```
```sh bun theme={null}
bun add @juo/blocks
```
`vue`, `react`, and `preact` are **optional peer dependencies** — install only the one the portal runs on, or none for plain DOM rendering.
## Entry points
`@juo/blocks` exposes a handful of subpath entry points. Import from the ones that match the stack — bundlers tree-shake the rest:
| Entry point | Purpose |
| ------------------------------------ | ------------------------------------------------------------------------ |
| `@juo/blocks` | Core: block system, signals, contexts, services |
| `@juo/blocks/vue` | Vue 3 renderers and composables |
| `@juo/blocks/react` | React renderers and hooks |
| `@juo/blocks/preact` | Preact renderers and hooks |
| `@juo/blocks/editor` | Bridge for integrating with the Juo Editor |
| `@juo/blocks/web-components/editor` | Custom elements with Editor affordances (selection, inline text editing) |
| `@juo/blocks/web-components/runtime` | Lean custom elements for storefront rendering |
## Anatomy
A portal does four things:
1. **Loads the web components.** Pick the runtime or editor build.
2. **Mounts a ``** in the HTML shell.
3. **Provides services** on the root.
4. **Registers blocks** so the editor and runtime can instantiate them by name.
When the portal runs inside the editor iframe, it also calls `setupEditorMode` to advertise its blocks and routes. See [Editor](/docs/blocks/concepts/editor) for what that protocol carries.
## Runtime: the bare minimum
```html theme={null}
```
```ts theme={null}
// src/main.ts — runtime build
import "@juo/blocks/web-components/runtime";
import {
provideContext,
registerBlock,
createThemeState,
ThemeStateContext,
createSubscriptionService,
createApiSubscriptionAdapter,
createCustomerService,
createApiCustomerAdapter,
SubscriptionServiceContext,
CustomerServiceContext,
} from "@juo/blocks";
import { HomePageBlock } from "./blocks/HomePage";
import { ButtonBlock } from "./blocks/Button";
// 1. Register blocks
registerBlock(HomePageBlock);
registerBlock(ButtonBlock);
// 2. Provide services on the root
const root = document.querySelector("juo-context-root") as HTMLElement;
provideContext(
root,
SubscriptionServiceContext,
createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);
provideContext(
root,
CustomerServiceContext,
createCustomerService(createApiCustomerAdapter({ baseUrl: "/api/v1" })),
);
// 3. Provide a ThemeState so knows what to render for each route
provideContext(
root,
ThemeStateContext,
createThemeState({
resolve: async (_surface, path) =>
path === "/" ? { blocks: [{ name: "HomePage", props: {}, slots: {} }] } : null,
}),
);
```
The `` element subscribes to `ThemeStateContext`, resolves the `/` route, and renders the resulting root block. Nested blocks resolve services from the surrounding context tree.
### Setup: providing services and rendering the page
The app shell registers blocks and calls `provideContext` for each service on the ``. `` then injects `ThemeStateContext` to resolve the current route and render the resulting root block.
### Composition: rendering a block from theme state
The resolved root block emits one or more `` slots; the children placed in each slot are themselves blocks. Each block calls `injectContext` to pull services out of the surrounding context tree, and the renderer turns its props plus those services into DOM.
See [Web components](/docs/blocks/concepts/web-components) for the full list of custom elements, [Services](/docs/blocks/concepts/services) for the providers, and [Contexts](/docs/blocks/concepts/contexts) for how the lookup works.
## Adding editor support
When the merchant opens the portal inside the editor, the iframe loads the **editor build** of the web components and the portal needs to advertise its blocks and routes. Detect the editor (e.g. by checking `window.parent !== window` or a URL flag), load the right web component bundle, and call `setupEditorMode`:
```ts theme={null}
// src/main.ts — editor-aware
import {
provideContext,
registerBlock,
createThemeState,
createRouterService,
ThemeStateContext,
RouterServiceContext,
} from "@juo/blocks";
import { setupEditorMode } from "@juo/blocks/editor";
const isEditor = window.parent !== window;
if (isEditor) {
await import("@juo/blocks/web-components/editor");
} else {
await import("@juo/blocks/web-components/runtime");
}
const root = document.querySelector("juo-context-root") as HTMLElement;
// ... register blocks and provide services ...
const themeState = createThemeState({
resolve: (surface, path) => editor.requestThemeState(surface, path),
upsertState: (surface, path, blocks) => editor.upsertThemeState(surface, path, blocks),
});
provideContext(root, ThemeStateContext, themeState);
const routerService = createRouterService();
provideContext(root, RouterServiceContext, routerService);
if (isEditor) {
const cleanup = await setupEditorMode({ routerService, themeState });
window.addEventListener("beforeunload", cleanup);
}
```
`setupEditorMode`:
* Serializes every registered block (name, group, schema, initial value, presets, locales) and sends it to the editor.
* Reports the portal's routes from `routerService.getPages()`.
* Subscribes to incoming editor messages (selection, prop updates, theme state changes, translation overrides) and applies them to the local `ThemeState`.
It returns a cleanup function — call it on teardown.
## Preset previews with `useBridge`
The editor's block picker shows **presets** for each block (see [Blocks → Presets](/docs/blocks/concepts/blocks)). When the merchant hovers a preset, the editor sends a `SET_BLOCK_PRESET` message and the matching block can render a preview without committing.
A block opts into preset previews using `useBridge` from its framework adapter:
```vue theme={null}
```
```tsx theme={null}
// React
import { useState } from "react";
import { useBridge } from "@juo/blocks/react";
function MyBlock() {
const [previewProps, setPreviewProps] = useState(null);
useBridge({
onPresetChange(preset) {
setPreviewProps(preset?.state.props ?? null);
},
});
}
```
```tsx theme={null}
// Preact
import { useState } from "preact/hooks";
import { useBridge } from "@juo/blocks/preact";
function MyBlock() {
const [previewProps, setPreviewProps] = useState(null);
useBridge({
onPresetChange(preset) {
setPreviewProps(preset?.state.props ?? null);
},
});
}
```
`useBridge` is a no-op outside the editor — `isEditorMode` returns `false` and `onPresetChange` never fires. You can ship the same block to both modes without branching.
## Folder layout
A typical portal package looks like:
```
src/
main.ts ← entry: register blocks, provide services, setup editor
blocks/
HomePage/index.ts ← defineBlock + renderer
Button/index.ts
SubscriptionCard/index.ts
services/
router.ts ← the concrete RouterService
index.html
```
Keep `defineBlock` calls in their own folder per block — each block is self-contained, and `registerBlock` is the only thing that needs to touch them all.
# Blocks
Source: https://juo.dev/docs/blocks/overview
Build type-safe, schema-driven subscription UI components — composable in any storefront.
`@juo/blocks` is a library for building **self-contained UI components** — blocks — that present and manipulate subscription data. Each block has props and slots described by a type-safe schema — familiar anatomy from Vue, React, or Preact components.
Blocks consume domain **services** (subscriptions, customers, orders, products…) through type-safe contexts and can be composed into pages. The library is framework-agnostic at its core — renderers are plain functions, and thin adapters add idiomatic ergonomics for Vue, React, and Preact when needed. Wrap any block in a web component to mount it from HTML.
## What the library provides
Self-contained UI components with typed props and slots described by a JSON Schema. Familiar anatomy, framework-agnostic renderer.
Signal-based reactive state that remains synchronized across framework adapters, including Vue refs, React state, and Preact hooks.
Type-safe dependency injection over the DOM. Provide services once at the root; override on any sub-tree; any block can request them.
Domain APIs — subscriptions, customers, orders, products, schedules, auth, routing, workflows.
Connect a portal to the Juo Editor so operators can configure, theme, and translate blocks without code.
Translate copy alongside content, with overrides flowing through the same context tree as services.
## Composition model
A **page** is represented as a root block. Blocks expose **slots** through their schema, and slots contain child blocks. This creates a recursive tree in which each block can define its own structure and dependencies.
Each block is authored in isolation — its props, its slots, and the [services](/docs/blocks/concepts/services) it consumes. Routing, mounting, and composition are handled by the host portal.
## Framework adapters
Concept examples use the plain DOM renderer. Framework adapters provide native integration patterns, including `useContext`, `useSignal`, and framework-native components, while preserving the same registry, context tree, and signal-based reactivity model.
Vue components as renderers, Composition API composables, and signal-as-ref interop.
Components and hooks, `useSignal` for reactive reads.
Same surface as React with Preact's hooks runtime.
Use `create-juo` to scaffold a blocks package. The [custom portal guide](/docs/blocks/guides/custom-portal) covers installation, entry points, and host application mounting.
# Customer
Source: https://juo.dev/docs/blocks/services/customer
Current customer profile as reactive state.
The customer service exposes the signed-in customer. The service exposes one reactive signal and one fetch method.
## Setup
```ts theme={null}
import {
provideContext,
createCustomerService,
createApiCustomerAdapter,
CustomerServiceContext,
} from "@juo/blocks";
provideContext(
root,
CustomerServiceContext,
createCustomerService(createApiCustomerAdapter({ baseUrl: "/api/v1" })),
);
```
## Shape
```ts theme={null}
type Customer = {
id: string;
name: string;
email?: string;
metadata?: Record;
};
type CustomerService = {
current: Signal;
getCurrent(): Promise>;
};
```
## Reactive state
`current` is `null` until a successful call to `getCurrent()` populates it. After that, every block that reads it updates automatically.
## Example: greeting block
```ts theme={null}
import {
defineBlock,
effect,
untracked,
injectContext,
CustomerServiceContext,
} from "@juo/blocks";
defineBlock("Greeting", {
group: "theme",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
renderer(block) {
const el = document.createElement("h1");
const customer = injectContext(el, CustomerServiceContext);
void customer.getCurrent();
effect(() => {
const name = customer.current.value?.name ?? "Guest";
untracked(() => {
el.textContent = `Welcome back, ${name}`;
});
});
return el;
},
});
```
## Custom metadata
The `metadata` field is a free-form record. Adapters can populate it with store-specific fields (loyalty tier, plan flags) without changing the service interface.
# Login
Source: https://juo.dev/docs/blocks/services/login
Passwordless and social login, token storage, redirect handling.
The login service handles authentication for the customer portal — passwordless email codes, social providers (Google, Facebook), and the token lifecycle that follows. It writes tokens to local storage so subsequent requests stay authenticated.
## Setup
```ts theme={null}
import {
provideContext,
createLoginService,
LoginServiceContext,
} from "@juo/blocks";
provideContext(
root,
LoginServiceContext,
createLoginService({
adapter: myLoginAdapter,
formatError: (err) => ({ message: String(err), code: undefined }),
router: routerService,
shop: { domain: "my-shop.myshopify.com", locale: "en" },
onLoginSuccess: async () => {
// hook called after a successful auth
},
}),
);
```
The login service depends on three other dependencies passed at construction time:
* `adapter` — connects to the auth backend (passwordless start/verify, social authenticate, refresh token).
* `router` — a minimal router with `push` / `replace` so the service can redirect post-login.
* `shop` — `{ domain, locale }`; `locale` can be a `Signal` so login adopts the current theme locale.
## Shape
```ts theme={null}
type LoginService = {
passwordless: {
email: {
start(email: string): Promise>;
verify(email: string, code: string): Promise>;
};
};
social: {
start(provider: "Google" | "Facebook"): Promise>;
verify(code: string): Promise>;
};
logout(): Promise>;
getToken(): Promise;
// ...reactive signals for tokens, expires, current user email
};
```
## Example: passwordless start
```ts theme={null}
import { injectContext, LoginServiceContext } from "@juo/blocks";
async function startLogin(el: HTMLElement, email: string) {
const login = injectContext(el, LoginServiceContext);
const result = await login.passwordless.email.start(email);
if (result._tag === "Failure") {
console.error(result.error.message);
return;
}
// show the code input
}
async function verifyCode(el: HTMLElement, email: string, code: string) {
const login = injectContext(el, LoginServiceContext);
const result = await login.passwordless.email.verify(email, code);
if (result._tag === "Success") {
// service redirects via the router automatically
}
}
```
## Token management
`getToken()` returns the current access token, refreshing it transparently if it has expired (using the stored refresh token). Wire it into the fetch layer to ensure all API requests include a valid access token:
```ts theme={null}
async function authedFetch(url: string, init?: RequestInit) {
const token = await login.getToken();
return fetch(url, {
...init,
headers: { ...init?.headers, Authorization: `Bearer ${token}` },
});
}
```
# Order
Source: https://juo.dev/docs/blocks/services/order
Order history and individual order lookups.
The order service exposes past and pending orders for the current customer. Reads are on-demand — there's no reactive list signal, so blocks fetch when they need data.
## Setup
```ts theme={null}
import {
provideContext,
createOrderService,
createApiOrderAdapter,
OrderServiceContext,
} from "@juo/blocks";
provideContext(
root,
OrderServiceContext,
createOrderService(createApiOrderAdapter({ baseUrl: "/api/v1" })),
);
```
## Shape
```ts theme={null}
type Order = {
id: string;
customerId: string;
items: OrderItem[];
deliveryDate: string | null;
status: "pending" | "confirmed" | "delivered" | "skipped";
metadata?: Record;
};
type OrderItem = {
id: string;
productId: string;
variantId: string;
quantity: number;
metadata?: Record;
};
type OrderService = {
getHistory(): Promise>;
getById(id: string): Promise>;
};
```
## Example: order history list
```ts theme={null}
import { defineBlock, injectContext, OrderServiceContext } from "@juo/blocks";
defineBlock("OrderHistory", {
group: "theme",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
renderer(block) {
const el = document.createElement("ul");
const orderService = injectContext(el, OrderServiceContext);
orderService.getHistory().then((result) => {
if (result._tag !== "Success") return;
el.replaceChildren(
...result.data.map((order) => {
const li = document.createElement("li");
li.textContent = `${order.status} — ${order.deliveryDate ?? "no date"}`;
return li;
}),
);
});
return el;
},
});
```
# Product
Source: https://juo.dev/docs/blocks/services/product
Look up products, variants, and purchase options.
The product service is the catalogue lookup used by blocks that show product info — upsell cards, swap pickers, variant selectors. It returns products with their full variant list and purchase options.
## Setup
```ts theme={null}
import {
provideContext,
createProductService,
createApiProductAdapter,
ProductServiceContext,
} from "@juo/blocks";
provideContext(
root,
ProductServiceContext,
createProductService(createApiProductAdapter({ baseUrl: "/api/v1" })),
);
```
## Shape
```ts theme={null}
type Product = {
id: string;
title: string;
featuredImage: Image | null;
hasOnlyDefaultVariant: boolean;
collections: { handle: string }[];
metadata?: Record;
};
type ProductVariant = {
id: string;
title: string;
image: Image | null;
price: PriceWithCurrency;
subscriptionPrice?: PriceWithCurrency;
purchaseOptions?: PurchaseOption[];
metadata?: Record;
};
type WithVariants = T & { variants: PaginatedList };
type ProductService = WithVariants> = {
getById(id: string): Promise;
getByVariantId(variantId: string): Promise;
search(opts?: {
query?: string;
pagination?: { before: string } | { after: string };
}): Promise>;
};
```
The service is generic over `T` so adapters can return extended product types (with custom metadata or computed fields) while keeping the base shape intact.
## Example: variant lookup
```ts theme={null}
import { injectContext, ProductServiceContext } from "@juo/blocks";
async function loadProduct(el: HTMLElement, id: string) {
const productService = injectContext(el, ProductServiceContext);
const product = await productService.getById(id);
if (!product) return;
for (const variant of product.variants.items) {
console.log(variant.title, variant.price);
}
}
```
## Search
```ts theme={null}
const { items, pagination } = await productService.search({
query: "tag:featured",
pagination: { after: lastCursor },
});
```
Search returns a `PaginatedList` — use `pagination` to drive infinite scroll or load-more affordances.
# Router
Source: https://juo.dev/docs/blocks/services/router
In-page navigation without coupling blocks to a specific router.
The router service abstracts in-page navigation from any specific router implementation (Vue Router, React Router, Shopify navigation, plain `location`). Blocks call `push`, subscribe to navigation events, and read the list of registered pages without importing any concrete router.
Unlike the other services, the router has no built-in adapter — the host application is expected to wire one up directly.
## Setup
```ts theme={null}
import { provideContext, RouterServiceContext } from "@juo/blocks";
provideContext(root, RouterServiceContext, {
push(to) {
if (typeof to === "string") {
window.location.href = to;
} else if ("path" in to) {
const qs = to.query ? "?" + new URLSearchParams(to.query) : "";
window.location.href = to.path + qs;
} else {
// resolve named route to a path against the route table
window.location.href = resolveNamed(to.name, to.params);
}
},
subscribe(cb) {
const handler = () => cb({ path: window.location.pathname });
window.addEventListener("popstate", handler);
return () => window.removeEventListener("popstate", handler);
},
getPages() {
return [
{ path: "/", block: "HomePage", title: "Home" },
{ path: "/account", block: "AccountPage", title: "Account" },
];
},
});
```
## Shape
```ts theme={null}
type RouterPushTarget =
| string
| { path: string; query?: Record }
| { name: string; params?: Record; query?: Record };
type RouterService = {
push(to: RouterPushTarget): void;
subscribe(callback: (event: { path: string }) => void): () => void;
getPages(): readonly { path: string; block: string; title: string }[];
};
```
`push` accepts the same three shapes as `vue-router`'s `push` — a plain path, a path+query object, or a named-route object. The adapter decides how to resolve named routes.
## Example: navigation link block
```ts theme={null}
import { defineBlock, injectContext, RouterServiceContext } from "@juo/blocks";
defineBlock("NavLink", {
group: "theme",
schema: {
type: "object",
properties: {
props: {
type: "object",
properties: { to: { type: "string" }, label: { type: "string" } },
required: ["to", "label"],
additionalProperties: false,
},
},
required: ["props"],
additionalProperties: false,
} as const,
initialValue: () => ({ props: { to: "/", label: "Home" } }),
renderer(block) {
const el = document.createElement("a");
const router = injectContext(el, RouterServiceContext);
el.href = block.props.to;
el.textContent = block.props.label;
el.addEventListener("click", (e) => {
e.preventDefault();
router.push(block.props.to);
});
return el;
},
});
```
## Page registrations
`getPages()` returns the static page table — useful for blocks that render a nav menu or sitemap. Each entry maps a path to the **block name** that renders it, plus a display title.
# Schedules
Source: https://juo.dev/docs/blocks/services/schedules
Upcoming orders, skip / reschedule, and billing-sensitive guards.
The schedules service manages the calendar of upcoming orders attached to a subscription. Blocks use it to render the next delivery, skip a cycle, or move a date.
## Setup
```ts theme={null}
import {
provideContext,
createSchedulesService,
createApiSchedulesAdapter,
SchedulesServiceContext,
} from "@juo/blocks";
provideContext(
root,
SchedulesServiceContext,
createSchedulesService(createApiSchedulesAdapter({ baseUrl: "/api/v1" })),
);
```
## Shape
```ts theme={null}
type ScheduleOrder = {
id: string;
subscriptionId: string;
deliveryDate: string;
items: ScheduleOrderItem[];
status: "scheduled" | "processing" | "delivered" | "skipped";
};
type SchedulesService = {
/** Reactive signal — shared upcoming orders for multi-block access. */
upcoming: Signal;
/** Sourced from SubscriptionService — guards billing-sensitive actions. */
hasPendingBilling: Signal;
getUpcomingOrder(): Promise>;
getUpcomingOrders(count: number): Promise>;
getAdjustments(): Promise>;
skipOrder(params: {
subscriptionId: string;
cycle?: number;
date?: string;
}): Promise;
changeDateAdjustment(params: {
subscriptionId: string;
newDate: string;
currentDate?: string;
}): Promise;
removeAdjustment(params: { adjustmentId: string }): Promise;
};
```
## Reactive state
* `upcoming` — list of upcoming orders. Multiple blocks can read it without duplicating fetches.
* `hasPendingBilling` — mirrors the subscription service signal so schedule-modifying blocks can disable controls during billing.
## Example: skip next order
```ts theme={null}
import {
defineBlock,
effect,
untracked,
injectContext,
SchedulesServiceContext,
} from "@juo/blocks";
defineBlock("SkipNext", {
group: "theme",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
renderer(block) {
const el = document.createElement("button");
const schedules = injectContext(el, SchedulesServiceContext);
el.addEventListener("click", async () => {
const next = schedules.upcoming.value[0];
if (!next) return;
await schedules.skipOrder({
subscriptionId: next.subscriptionId,
date: next.deliveryDate,
});
});
effect(() => {
const next = schedules.upcoming.value[0];
const pending = schedules.hasPendingBilling.value;
untracked(() => {
el.textContent = next ? `Skip ${next.deliveryDate}` : "No upcoming order";
el.disabled = pending || !next;
});
});
return el;
},
});
```
## Adjustments
`ScheduleAdjustment` represents pending changes to the calendar — a skipped cycle or a moved date. Read them with `getAdjustments()` to show "your next change" affordances and remove them with `removeAdjustment()`.
# Subscription
Source: https://juo.dev/docs/blocks/services/subscription
Read and mutate subscriptions — items, discounts, schedule, lifecycle.
The subscription service is the core domain API for blocks that show or modify a customer's subscription. It exposes reactive state for the **current** subscription and methods for every mutation a customer-facing block needs.
## Setup
```ts theme={null}
import {
provideContext,
createSubscriptionService,
createApiSubscriptionAdapter,
createMockSubscriptionAdapter,
SubscriptionServiceContext,
} from "@juo/blocks";
// Production
provideContext(
root,
SubscriptionServiceContext,
createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);
// Development / Storybook
provideContext(
root,
SubscriptionServiceContext,
createSubscriptionService(createMockSubscriptionAdapter()),
);
```
## Shape
```ts theme={null}
type SubscriptionService = {
// Reactive state
current: Signal;
hasPendingBilling: Signal;
// Reads
search(options?: SearchOptions): Promise>>;
getById(id: string): Promise>;
getCurrentSubscriptionId(): string | null;
getDefaultSubscriptionId(): Promise>;
getAvailablePlans(subscriptionId: string): Promise>;
getUpsellProducts(params: { subscriptionId: string }): Promise>;
// Items
addItem(subscriptionId: string, item: PostableSubscriptionItem): Promise;
removeItem(subscriptionId: string, itemId: string): Promise;
addUpsellProduct(params: {
subscriptionId: string;
variantId: string;
quantity: number;
oneTime?: boolean;
}): Promise;
// Discounts
addDiscount(subscriptionId: string, code: string): Promise;
removeDiscount(subscriptionId: string, discountId: string): Promise;
// Lifecycle
reschedule(subscriptionId: string, newDate: string): Promise>;
pause(subscriptionId: string): Promise>;
resume(subscriptionId: string): Promise>;
reactivate(subscriptionId: string): Promise>;
cancel(subscriptionId: string, reason?: string): Promise>;
};
```
A `Subscription` carries items, discounts, status (`active | paused | canceled | expired | failed`), `nextDeliveryDate`, `frequency`, and the owning `customerId`.
## Reactive state
| Signal | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| `current` | The currently selected subscription, or `null` if none is loaded. Updates after mutations. |
| `hasPendingBilling` | `true` when a billing attempt is in-flight. Use this to guard mutations that would conflict with billing. |
## Example: pause / resume toggle
```ts theme={null}
import {
defineBlock,
effect,
untracked,
injectContext,
SubscriptionServiceContext,
} from "@juo/blocks";
defineBlock("PauseToggle", {
group: "theme",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
renderer(block) {
const el = document.createElement("button");
const subscription = injectContext(el, SubscriptionServiceContext);
el.addEventListener("click", async () => {
const current = subscription.current.value;
if (!current || subscription.hasPendingBilling.value) return;
const result = current.status === "paused"
? await subscription.resume(current.id)
: await subscription.pause(current.id);
if (result._tag === "Failure") console.error(result.error);
});
effect(() => {
const current = subscription.current.value;
const pending = subscription.hasPendingBilling.value;
untracked(() => {
el.textContent = current?.status === "paused" ? "Resume" : "Pause";
el.disabled = pending || !current;
});
});
return el;
},
});
```
## Example: adding a discount
```ts theme={null}
async function applyCode(id: string, code: string) {
const result = await subscription.addDiscount(id, code);
if (result._tag === "Failure") {
throw new Error("Invalid code");
}
// subscription.current updates automatically
}
```
# Workflow
Source: https://juo.dev/docs/blocks/services/workflow
Run interactive flows — start, respond, observe status.
The workflow service drives interactive customer flows (retention prompts, dunning, onboarding). It speaks to a backend execution engine via a request/response API and exposes the running flow as reactive state — current step, available responses, completion outcome.
## Setup
```ts theme={null}
import {
provideContext,
createWorkflowService,
createWorkflowApiAdapter,
createMockWorkflowApiAdapter,
WorkflowServiceContext,
} from "@juo/blocks";
provideContext(
root,
WorkflowServiceContext,
createWorkflowService({
adapter: createWorkflowApiAdapter(sdk),
// ...other dependencies (translations, theme state)
}),
);
```
For local development, swap the adapter:
```ts theme={null}
createWorkflowService({ adapter: createMockWorkflowApiAdapter() });
```
## Shape (key surface)
```ts theme={null}
type WorkflowService = {
// Reactive state
isActive: ReadonlySignal;
isLoading: ReadonlySignal;
isCompleted: ReadonlySignal;
hasError: ReadonlySignal;
canRespond: ReadonlySignal;
currentStepInfo: ReadonlySignal;
currentInteraction: ReadonlySignal;
// Lifecycle
startFlow(flowType: string, params?: Record): Promise;
respond(responseType: string, data?: Record): Promise;
cancel(): Promise;
};
```
`WorkflowStepInfo` carries the current step's `actionType`, `actionConfig`, and the list of `availableResponses` the customer can pick from.
## Execution model
Workflows are deterministic request/response — no websockets, no in-memory timers. Each customer action becomes a `respond()` call; the backend advances the state machine and returns the next step. Timeouts are orchestrator-driven (the backend schedules them).
## Example: workflow action block
A typical action block reads the current step config and offers a response button per available choice:
```ts theme={null}
import {
defineBlock,
effect,
untracked,
injectContext,
WorkflowServiceContext,
} from "@juo/blocks";
import { html, render } from "lit-html";
defineBlock("DiscountOffer", {
group: "action",
schema: { /* ... */ } as const,
initialValue: () => ({ props: {} }),
renderer(block) {
const el = document.createElement("div");
const workflow = injectContext(el, WorkflowServiceContext);
effect(() => {
const info = workflow.currentStepInfo.value;
const loading = workflow.isLoading.value;
const canRespond = workflow.canRespond.value;
const value = (info?.actionConfig?.discountValue as string) ?? "10";
untracked(() => {
render(
html`
Take ${value}% off your next order?
${(info?.availableResponses ?? []).map(
(r) => html`
`,
)}
`,
el,
);
});
});
return el;
},
});
```
## Starting a flow
The host page typically starts a flow in response to a customer action — such as canceling a subscription or initiating a retention offer.
```ts theme={null}
await workflow.startFlow("cancel_subscription", {
subscriptionId: current.id,
layout: "modal",
});
```
`flowType` is a stable client identifier. The backend selects the published workflow definition that matches the flow type for the trigger source.
# Activity log
Source: https://juo.dev/docs/core-concepts/activity-log
The **Activity log** provides a comprehensive audit trail of all actions and events that occur within the platform. It records changes to subscriptions, orders, subscriber information, payment attempts, and administrative actions. The activity log is essential for troubleshooting, compliance, and understanding the history of any entity in the system. It provides transparency and traceability for all platform operations.
```mermaid theme={null}
graph TB
ActivityLog[Activity log] -.-> Subscriber[Subscriber]
ActivityLog -.-> Subscription[Subscription]
ActivityLog -.-> Order[Order]
ActivityLog -.-> Plan[Subscription plan]
```
# Orders
Source: https://juo.dev/docs/core-concepts/orders
**Orders** represent individual transactions within a subscription. Each time a subscription is billed or fulfilled, an order is created.
Orders contain information about the products or services being delivered, pricing, discounts applied, shipping details, and payment status. Orders provide a detailed record of each transaction and are essential for tracking subscription fulfillment and billing history.
Each order tracks:
* **Order type**: Whether the order is from a checkout (one-time purchase) or recurring (subscription billing)
* **Order status**: Draft (not yet placed) or placed (completed transaction)
* **Pricing**: Subtotal, delivery price, taxes, and total price
* **Payment method**: The payment method used for the order
* **Delivery method**: Shipping, local delivery, or pickup configuration
* **Timestamps**: Created date and placed date
* **External integration**: Links to external e-commerce platform orders
#### Order Items
**Order items** (also referred to as order lines) are the individual products or services included in an order. Each order contains one or more items that represent what was purchased.
Each order item contains:
* **Product information**: Title, subtitle, SKU, product and variant identifiers
* **Quantity**: Number of units purchased
* **Pricing**: Unit price and total line price (after discounts)
* **Discounts**: Item-specific discounts applied to this line
* **Tax information**: Whether the item is taxable
* **Shipping requirements**: Whether the item requires shipping
* **Subscription item**: Link back to the subscription item that generated this order item
* **Subscription plan**: Reference to the subscription plan that was used
**Example**: An order might contain three items: "Premium Coffee Beans" (quantity: 2, \$15.99 each), "Coffee Filters" (quantity: 1, \$4.99), and "Mug Set" (quantity: 1, \$12.99).
#### Order Discounts
**Order discounts** represent promotional offers or pricing adjustments applied to an order. Discounts are applied during order creation and affect the final order total.
Order discounts can:
* **Target shipping**: Apply discounts to delivery/shipping costs
* **Target items**: Apply discounts to specific order items or all items
* **Value types**: Use fixed amount discounts or percentage discounts
* **Source**: Reference the subscription discount that generated this order discount
**Example**: An order might have a "WELCOME10" discount applying 10% off all items, and a "FREESHIP" discount providing free shipping.
#### Payment status
Orders are linked to **billing attempts** which track the payment processing status. The payment status indicates the current state of payment processing:
* **New**: Payment has not been processed yet
* **Pending**: Payment is being processed or waiting for customer action (e.g., confirmation)
* **Completed**: Payment was successfully processed
* **Failed**: Payment processing failed (e.g., insufficient funds, card declined)
Payment statuses are managed through billing attempts, which handle the communication with payment providers and track the payment lifecycle.
#### Refunds
**Refunds** represent the reversal of a payment for an order. Refunds can be initiated by operators or automatically processed in certain scenarios (such as chargebacks).
Refund characteristics:
* **Status tracking**: Refunds have their own status (new, pending, completed, failed)
* **Partial or full**: Refunds can be for the full order amount or specific items
* **Item-level**: Refunds can target specific order items, allowing partial refunds
* **External integration**: Refunds are processed through payment providers and synced with external e-commerce platforms
* **Activity logging**: All refund actions are logged in the activity log
When a refund is processed:
* The refund is created and tracked through the payment provider
* The order is updated to reflect the refund status
* Activity log entries are created for audit purposes
* Subscriptions may be affected depending on the refund type and timing
**Example**: If a subscriber receives damaged products, an operator might issue a partial refund for just those items, while the rest of the order remains fulfilled.
#### Chargebacks
**Chargebacks** occur when a payment is reversed by the cardholder's bank or payment provider, typically due to disputes, fraud claims, or unauthorized transactions. Chargebacks are a critical aspect of payment processing that require immediate attention.
When a chargeback occurs:
* **Automatic refund**: The order is automatically refunded through the payment provider
* **Subscription impact**: Affected subscriptions are marked as failed
* **Payment method revocation**: For non-recoverable chargebacks, the payment method may be automatically revoked to prevent further issues
* **Recovery options**: Depending on chargeback configuration, a new draft order may be created to allow the subscriber to pay again
* **Activity logging**: Comprehensive activity log entries are created for all chargeback-related actions
* **Notifications**: Operators and subscribers are notified about the chargeback
Chargeback characteristics:
* **Reason codes**: Chargebacks include reason codes and messages explaining why the chargeback occurred
* **Recoverable vs non-recoverable**: Some chargeback types can be recovered (disputed), while others result in permanent payment method revocation
* **Order tagging**: Orders affected by chargebacks are tagged for easy identification
* **Fulfillment status**: The system tracks whether orders were fulfilled before the chargeback, affecting refund processing
**Example**: If a subscriber disputes a charge claiming they didn't receive their order, a chargeback is initiated. The system automatically refunds the order, marks the subscription as failed, and may revoke the payment method. If the chargeback is recoverable and the operator can prove delivery, they may dispute it through the payment provider.
```mermaid theme={null}
graph TB
Subscription[Subscription] --> Order[Order]
Order --> OrderItem[Order items]
Order --> OrderDiscount[Order discounts]
Order --> BillingAttempt[Billing attempt]
BillingAttempt --> Refund[Refunds]
BillingAttempt --> Chargeback[Chargebacks]
```
# Overview
Source: https://juo.dev/docs/core-concepts/overview
The core concepts work together to create a complete subscription management system.
Templates that define subscription configurations, billing intervals, and pricing.
End customers with active or past subscriptions and their associated profiles.
Active relationships between subscribers and subscription plans.
Individual transactions, fulfillments, and billing events within a subscription.
System that generates and manages projected future orders and adjustments.
Comprehensive audit trail of all platform actions and subscription changes.
# Schedule
Source: https://juo.dev/docs/core-concepts/schedule
The **Schedule** is the system that generates projected future orders based on subscription configurations. It calculates when orders should be created by analyzing subscription items' billing intervals, delivery frequencies, and associated discounts.
The schedule groups subscription items that share common delivery methods, addresses, and payment methods into cohesive orders, and applies schedule adjustments that can modify orders during generation using workflow conditions and actions.
The schedule also handles dynamic pricing calculations, such as shipping costs, and respects recurring cycle limits for both items and discounts. This allows operators and subscribers to preview upcoming orders and understand the projected billing and fulfillment timeline.
### Schedule adjustments
**Schedule adjustments** allow modification of individual schedule orders. Each adjustment consists of a matcher (to identify which order to modify) and an action (what modification to apply).
Common adjustments include skipping an order (`SKIP_ORDER`) or changing its scheduled date (`CHANGE_DATE`). You can match orders by their billing cycle, a specific date, or both.
For a detailed breakdown of available actions and matchers, see the [Schedule adjustment resource](/docs/api-reference/admin/schedules-adjustments/resource).
### Schedule orders
**Schedule orders** are the projected future orders generated by the schedule system. Each schedule order represents a planned transaction that will occur at a specific date based on the subscription's billing cycle. Schedule orders contain:
* **Items**: The products or services that will be included in the order, with quantities, pricing, and product information
* **Discounts**: Applied discounts that affect the order's pricing, including item-level and shipping discounts
* **Delivery information**: Delivery address, shipping method, and calculated shipping costs
* **Payment information**: Payment method and billing address associated with the order
* **Pricing details**: Calculated subtotal, delivery price, and total amounts after discounts
* **Cycle index**: The billing cycle number this order represents in the subscription's lifecycle
* **Metadata**: Custom attributes, notes, and other order-specific information
Schedule orders can be modified by schedule adjustments during generation, can be marked as skipped, and may represent prepaid orders (where multiple cycles are paid in advance). They serve as a preview of upcoming orders before they are actually created and processed.
### Diagram
```mermaid theme={null}
graph TB
Subscription[Subscription] --> Schedule[Schedule]
Schedule --> ScheduleOrder[Schedule orders]
ScheduleAdjustment[Schedule adjustments] -.-> Schedule
ScheduleOrder --> OrderItem[Order items]
ScheduleOrder --> OrderDiscount[Order discounts]
```
# Subscribers
Source: https://juo.dev/docs/core-concepts/subscribers
**Subscribers** are the end customers who have active or past subscriptions. Each subscriber represents a unique customer in the system and is associated with their subscription history, payment methods, preferences, and account information.
Subscribers can manage their subscriptions through the Subscriber Portal, where they can view their subscription details, update payment information, and perform self-service operations.
```mermaid theme={null}
graph LR
Subscriber[Subscriber] --> Subscription[Subscriptions]
Subscription[Subscriptions] --> Order[Orders]
```
# Subscription plans
Source: https://juo.dev/docs/core-concepts/subscription-plans
**Subscription plans** define the templates for subscriptions that operators create and offer to subscribers. A subscription plan specifies the products or services included, pricing structure, billing frequency, delivery frequency, and other terms.
Subscribers subscribe to these plans, creating new subscriptions or adding new subscription items to existing subscriptions based by the plan's configuration. Plans can be modified over time, but modifications by default does not affect the existing subscriptions.
### Billing policies
**Billing policies** define when and how often subscribers are charged. A billing policy includes:
* **Interval and Interval count**: The frequency of billing (e.g., every 2 weeks, every 3 months). Intervals can be weekly, monthly, or yearly.
* **Anchors**: Specific dates or days when billing should occur (see Anchors section below).
* **Minimum and maximum cycles**: Optional limits on the number of billing cycles (e.g., minimum 3 cycles, maximum 12 cycles).
* **Days before**: How many days before delivery the billing should occur (e.g., bill 7 days before delivery).
* **Cutoff**: Days before the billing date when the cycle should be skipped if the cutoff is passed.
* **Exclusion rules**: Periods during which billing should be excluded (e.g., exclude billing during holiday seasons).
**Example**: A plan with a billing policy of "every 2 weeks, anchored to every Monday, with a 3-day cutoff" would bill subscribers every other Monday, but skip billing if it's less than 3 days away.
### Delivery policies
**Delivery policies** define when and how often products or services are delivered to subscribers. A delivery policy includes:
* **Interval and interval count**: The frequency of delivery (e.g., every week, every 2 months).
* **Anchors**: Specific dates or days when delivery should occur.
* **Cutoff**: Days before the delivery date when the delivery should be skipped if the cutoff is passed.
* **Pre-anchor behavior**: How to handle deliveries that occur before the first anchor date (e.g., deliver immediately or wait for the anchor).
**Example**: A plan with a delivery policy of "every month, anchored to the 15th of each month" would deliver products on the 15th of every month.
### Anchors
**Anchors** are specific dates or days that determine when billing or delivery events occur. Anchors provide flexibility to align subscription events with specific calendar dates rather than just intervals. There are three types of anchors:
* **Year day anchor**: A specific date each year (e.g., March 15th, December 25th). Useful for annual subscriptions or seasonal products.
* **Month day anchor**: A specific day of each month (e.g., the 1st, 15th, or last day of the month). Useful for monthly subscriptions that should always occur on the same date.
* **Week day anchor**: A specific day of the week (e.g., every Monday, every Friday). Useful for weekly subscriptions.
Anchors can also include a **cutoff day**, which determines how many days before the anchor date the event should be skipped if the cutoff has passed.
**Examples**:
* A "coffee subscription" might use a **weekday anchor** (every Monday) for weekly deliveries.
* A "magazine subscription" might use a **month day anchor** (the 1st of each month) for monthly deliveries.
* A "holiday gift subscription" might use a **year day anchor** (December 1st) for annual deliveries.
### Pricing policies
**Pricing policies** define how the price of subscription items changes over time. Pricing policies allow operators to offer introductory pricing, loyalty discounts, or other dynamic pricing strategies. There are two main types:
* **Fixed pricing policy**: Applies a consistent discount or price adjustment from the start. The adjustment can be:
* **Percentage**: A percentage discount (e.g., 10% off)
* **Fixed amount**: A fixed amount discount (e.g., \$5 off)
* **Price override**: A specific price to charge (e.g., always charge \$29.99)
* **Recurring pricing policy**: Applies pricing adjustments after a certain number of cycles. This allows for introductory pricing that changes after a period. The adjustment types are the same as fixed pricing (percentage, fixed amount, or price override), but they only apply after the specified cycle number.
**Examples**:
* A "first month free" subscription uses a **recurring pricing policy** with 100% discount for cycle 1, then full price afterward.
* A "loyalty discount" subscription uses a **recurring pricing policy** with 20% discount starting from cycle 3.
* A "member pricing" subscription uses a **fixed pricing policy** with 15% off all cycles.
```mermaid theme={null}
graph TB
Plan[Subscription Plan] --> Subscription[Subscription]
Plan --> BillingPolicy[Billing policy]
Plan --> DeliveryPolicy[Delivery policy]
Plan --> PricingPolicy[Pricing policy]
BillingPolicy --> Anchor[Anchors]
DeliveryPolicy --> Anchor
```
# Subscriptions
Source: https://juo.dev/docs/core-concepts/subscriptions
**Subscriptions** represent the ongoing relationship between subscribers and subscription plans, defining what products or services a subscriber receives, how often they are billed, and the terms of the subscription agreement.
Subscriptions serve as the central entity linking subscribers, plans, subscription items (the concrete products or services within the subscription), applied discounts, and related orders.
Each subscription tracks:
* **Status**: Current state of the subscription (active, paused, canceled, failed, expired, or merged)
* **Serial number**: Unique sequential identifier within a store
* **Current cycle**: The billing cycle count, starting at 0 (before first billing) and incrementing with each completed billing
* **Next billing date**: When the next billing process will begin
* **Currency code**: The currency used for all pricing
* **Billing and delivery policies**: Inherited from the subscription plan, but can be overridden at the subscription or item level
* **Delivery method**: Shipping, local delivery, or pickup configuration
* **Payment method**: The payment method used for transactions
#### Subscription items
**Subscription items** (also referred to as subscription lines) are the individual products or services included within a subscription. Each subscription may have multiple items, allowing flexibility for subscribers to have personalized bundles or to change items over time.
Each subscription item contains:
* **Product information**: Title, subtitle, product and variant identifiers, SKU
* **Quantity**: Number of units for this item
* **Pricing**: Current price and total price (including discounts and taxes)
* **Subscription plan**: Reference to the specific subscription plan that applies to this item
* **Recurring cycle limit**: Optional limit on how many billing cycles this item will be included (after which it's automatically removed)
* **Billing and delivery policies**: Can override the subscription-level policies for this specific item
* **Pricing policy**: Item-specific pricing rules (e.g., introductory pricing, cycle-based discounts)
**Example**: A subscription might include three items: "Premium Coffee Beans" (monthly, 2 bags), "Coffee Filters" (every 3 months, 1 pack), and "Mug Set" (one-time, 1 set with a recurring cycle limit of 1).
#### Subscription discounts
**Subscription discounts** represent special pricing or promotional offers applied to a subscription. Discounts can be applied at different levels and have various configurations:
* **Target**: Discounts can target:
* **Shipping**: Apply to delivery/shipping costs
* **Subscription items**: Apply to subscription items, either all items or specific selected items
* **Value type**:
* **Fixed amount**: A specific dollar amount discount (e.g., \$5 off)
* **Percentage**: A percentage discount (e.g., 10% off)
* **Recurring cycle limit**: Optional limit on how many billing cycles the discount applies (e.g., first 3 cycles only)
* **Title**: Typically displayed as a discount code to customers
**Example**: A subscription might have a "WELCOME10" discount that applies 10% off all items for the first 3 cycles, and a "FREESHIP" discount that provides free shipping for all orders.
#### Subscription plan relationship
Subscription plans define the templates and default configurations, but the relationship between plans and subscriptions is flexible:
* **Subscription level**: The subscription inherits billing and delivery policies from the plan, which define the default behavior for the entire subscription
* **Item level**: Each subscription item can reference a specific selling plan (via `sellingPlanId`), allowing different items in the same subscription to follow different plan configurations
* **Policy overrides**: Both subscriptions and individual items can override the plan's billing and delivery policies, providing flexibility for custom configurations
**Example**: A subscription might be created from a "Monthly Coffee Plan" but include items from different plans:
* Item 1: "Premium Coffee" from "Monthly Premium Plan" (bills monthly)
* Item 2: "Accessories" from "Quarterly Accessories Plan" (bills every 3 months)
```mermaid theme={null}
graph TB
Subscriber[Subscriber] --> Subscription[Subscription]
Plan[Subscription plan] --> Subscription
Plan --> ItemPlan[Selling plan]
Subscription --> SubscriptionItem[Subscription items]
Subscription --> SubscriptionDiscount[Subscription discounts]
Subscription --> Order[Orders]
ItemPlan -.-> SubscriptionItem
SubscriptionDiscount -.-> SubscriptionItem
```
# Button
Source: https://juo.dev/docs/customer-ui/button
Button with variants, sizes, icons, and async handler support.
```html theme={null}
Button
```
Button with variants, sizes, icons, and async handler support. When a `handler`
is provided, the button renders an inline loading state while the promise settles, then a
success or error glyph.
## Usage
```html theme={null}
Button
```
## Reference
### Parameters
Complex values (`Record`) must be assigned via the `disabled` JS property instead.
Complex values (`Component`) must be assigned via the `icon` JS property instead.
### Events
The element dispatches native [`CustomEvent`](https://developer.mozilla.org/docs/Web/API/CustomEvent)s; the payload is in `event.detail`.
`event.detail`: `[event: MouseEvent]`
### Slots
# Card
Source: https://juo.dev/docs/customer-ui/card
Surface container with border, radius, and nested level support.
```html theme={null}
Level 1
Level 2
Level 3
```
Surface container with border, radius, and nested level support. Levels 1–3 map to progressively
insetter surface tokens. In a normal DOM tree the level is inferred automatically from nesting via
the descendant `.card.card` selectors. As an isolated web component each card lives in its own
shadow root, so those selectors can't match across the boundary — instead we infer the level on
mount by walking the (light-DOM) ancestor `<juo-card>` hosts. An explicit `level` prop always
wins. `padding` and `rounding` map to the `--padding-*` and `--radius-*` scales.
## Usage
```html theme={null}
Card
```
## Reference
### Parameters
Surface level. Omit to infer from DOM nesting; set explicitly for web-component nesting.
Inner padding, from the `--padding-*` scale.
Corner rounding, from the `--radius-*` scale. Defaults to the level's radius.
### Slots
# Checkbox
Source: https://juo.dev/docs/customer-ui/checkbox
Accessible checkbox with checked, unchecked, and indeterminate states, plus an error style.
```html theme={null}
```
Accessible checkbox with checked, unchecked, and indeterminate states, plus an error style. Built
on Reka UI with full keyboard navigation and screen-reader accessibility.
## Usage
```html theme={null}
```
## Reference
### Parameters
The value of the checkbox when it is initially rendered. Use when you do not need to control its value.
The controlled value of the checkbox. Can be binded with.
When `true`, prevents the user from interacting with the checkbox
The value given as data when submitted with a `name`.
Id of the element
Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details.
The element or component this component should render as. Can be overwritten by `asChild`.
The name of the field. Submitted with its owning form as part of a name/value pair.
When `true`, indicates that the user must set the value before the owning form can be submitted.
### Events
The element dispatches native [`CustomEvent`](https://developer.mozilla.org/docs/Web/API/CustomEvent)s; the payload is in `event.detail`.
`event.detail`: `[value: boolean | indeterminate]`
### Slots
# Chip
Source: https://juo.dev/docs/customer-ui/chip
Compact label chip with an optional dismiss button.
```html theme={null}
Chip
```
Compact label chip with an optional dismiss button. Supports a loading spinner (rendered in place of
the close icon while a removal is pending) and a disabled state.
## Usage
```html theme={null}
Chip
```
## Reference
### Parameters
### Events
The element dispatches native [`CustomEvent`](https://developer.mozilla.org/docs/Web/API/CustomEvent)s; the payload is in `event.detail`.
`event.detail`: `[]`
# Design tokens
Source: https://juo.dev/docs/customer-ui/design-tokens
CSS custom property reference for the @juo/customer-ui design system.
All tokens are defined on `:root, :host` so they work in both regular DOM and Shadow DOM.
CSS custom properties inherit through Shadow DOM boundaries, so `--theme-*` overrides set on
`:root` re-theme every `` component automatically.
```html theme={null}
```
Or import in CSS:
```css theme={null}
@import "@juo/customer-ui/design-tokens.css";
```
## Base units
| Theme input | Default | Drives |
| ------------------- | ------- | ------------------------------------ |
| `--theme-spacing` | `8px` | `--space-base` → `--space-*` |
| `--theme-font-size` | `16px` | `--font-size-base` → `--font-size-*` |
| `--theme-radius` | `4px` | `--radius-base` → `--radius-*` |
## Space scale
| Token | Default |
| ------------ | ------- |
| `--space-2` | 2px |
| `--space-4` | 4px |
| `--space-6` | 6px |
| `--space-8` | 8px |
| `--space-12` | 12px |
| `--space-16` | 16px |
| `--space-24` | 24px |
| `--space-32` | 32px |
| `--space-36` | 36px |
| `--space-48` | 48px |
## Font size scale
| Token | Default |
| ---------------- | ------- |
| `--font-size-12` | 12px |
| `--font-size-14` | 14px |
| `--font-size-16` | 16px |
| `--font-size-20` | 20px |
| `--font-size-24` | 24px |
| `--font-size-32` | 32px |
## Radius scale
| Token | Default |
| --------------- | ------- |
| `--radius-1` | 4px |
| `--radius-2` | 8px |
| `--radius-3` | 12px |
| `--radius-4` | 14px |
| `--radius-5` | 18px |
| `--radius-full` | 400px |
## Color palettes
* **tint** — `--tint-{shade}` — shades: 25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900
* **success** — `--success-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900
* **warning** — `--warning-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900
* **error** — `--error-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900
* **info** — `--info-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900
* **callout** — `--callout-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900
* **accent** — `--accent-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, white, black, contrast
* **secondary** — `--secondary-{shade}` — shades: 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, white, black
* **neutral** — `--color-neutral-{shade}` — shades: 0, 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 1000
`--color-neutral-*` is an alias of the tint palette and automatically inherits warm/cold/custom
tint behavior driven by `data-tint`.
## Customization
Override `--theme-*` tokens on `:root` to restyle all `` components globally.
No custom attributes or build steps required.
```css theme={null}
:root {
/* Spacing density */
--theme-spacing: 6px;
/* Border radius */
--theme-radius: 0px;
/* Brand/accent color */
--theme-accent-700: oklch(42% 0.18 265);
--theme-accent-600: oklch(52% 0.20 265);
--theme-accent-500: oklch(60% 0.22 265);
/* Background tint */
--theme-tint-100: oklch(94% 0.005 265);
--theme-tint-200: oklch(89% 0.008 265);
}
```
## Tailwind preset
```css theme={null}
@import "tailwindcss";
@import "@juo/customer-ui/design-tokens.css";
@import "@juo/customer-ui/tailwind-v4.css";
```
```js theme={null}
// tailwind.config.cjs
module.exports = {
presets: [require("@juo/customer-ui/tailwind-v3")],
};
```
* **Spacing** — `p-8`, `gap-16`, `m-24`, etc. — steps: 2, 4, 6, 8, 12, 16, 24, 32, 48
* **Radii** — `rounded-0`, `rounded-1`, `rounded-2`, `rounded-3`, `rounded-4`, `rounded-5`, `rounded-full`, `rounded-card-1`, `rounded-card-2`, `rounded-card-3`, `rounded-thumbnail`, `rounded-buttons`
* **Font sizes** — `text-12`, `text-14`, `text-16`, `text-20`, `text-24`, `text-32`
* **Colors** — all palettes above as `bg/text/border/ring-{palette}-{shade}`
# Getting started
Source: https://juo.dev/docs/customer-ui/getting-started
Install and use @juo/customer-ui web components in any framework or plain HTML.
`@juo/customer-ui` provides framework-agnostic web components built on the Shadow DOM. The components integrate with any HTML page or framework — no build step required for the CDN path.
## Installation
Add a single script tag to your page. All `` elements become available immediately.
```html theme={null}
```
```bash theme={null}
npm install @juo/customer-ui
```
Call `defineCustomElements()` once at your app entry point:
```js theme={null}
import { defineCustomElements } from "@juo/customer-ui/web-components";
defineCustomElements();
```
## Theming
Components read design tokens from CSS custom properties on `:root`. Override any token to customize component appearance:
```css theme={null}
:root {
--theme-accent-600: oklch(52% 0.20 265);
--theme-radius: 6px;
}
```
See [Design Tokens](/docs/customer-ui/design-tokens) for the full token reference.
# Input
Source: https://juo.dev/docs/customer-ui/input
Text input with variants, sizes, an error state with message, and optional leading icon or trailing suffix slots.
```html theme={null}
```
Text input with variants, sizes, an error state with message, and optional leading icon or
trailing suffix slots. Supports text, number, email, and password types.
## Usage
```html theme={null}
Input
```
## Reference
### Parameters
### Events
The element dispatches native [`CustomEvent`](https://developer.mozilla.org/docs/Web/API/CustomEvent)s; the payload is in `event.detail`.
`event.detail`: `[value: string | number]`
### Slots
# Select
Source: https://juo.dev/docs/customer-ui/select
Accessible select dropdown built on Reka UI.
```html theme={null}
```
Accessible select dropdown built on Reka UI. Supports single selection, an optional
empty/default option, sizes, and an inline error message.
## Usage
```html theme={null}
Select
```
## Reference
### Parameters
When `true`, prevents the user from interacting with Select
The controlled value of the Select.
The name of the field. Submitted with its owning form as part of a name/value pair.
When `true`, indicates that the user must set the value before the owning form can be submitted.
The controlled open state of the Select.
The open state of the select when it is initially rendered. Use when you do not need to control its open state.
Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared.
Complex values (`((a: AcceptableValue, b: AcceptableValue) => boolean)`) must be assigned via the `by` JS property instead.
The reading direction of the combobox when applicable. \ If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode.
Whether multiple options can be selected or not.
Native html input `autocomplete` attribute.
The value of the select when initially rendered.
Complex values (`(string | number | null)[]`) must be assigned via the `defaultValue` JS property instead.
### Events
The element dispatches native [`CustomEvent`](https://developer.mozilla.org/docs/Web/API/CustomEvent)s; the payload is in `event.detail`.
`event.detail`: `[value: string | number | null]`
`event.detail`: `[value: boolean]`
# Tag
Source: https://juo.dev/docs/customer-ui/tag
Status/category tag with semantic colour variants (info, success, warning, error, neutral) and two sizes.
```html theme={null}
Tag
```
Status/category tag with semantic colour variants (info, success, warning, error, neutral) and two
sizes. Renders its label via the default slot in a monospace typeface.
## Usage
```html theme={null}
Tag
```
## Reference
### Parameters
### Slots
# Introduction
Source: https://juo.dev/docs/introduction
Juo is a comprehensive subscription management platform that enables operators to build and manage subscription-based businesses. The platform provides a flexible architecture that allows developers to extend and customize functionality through various extensibility mechanisms.
This documentation provides a high-level overview of the Juo platform architecture and its core extensibility concepts to help developers understand the platform's capabilities and structure.
Read on core platform concepts and the platform extensibility areas.
Explore endpoints, parameters, and examples on how to use API.
Learn how to build custom blocks or custom portals from scratch with Blocks
SDK.
# MCP
Source: https://juo.dev/docs/mcp
Connect AI assistants to Juo using the Model Context Protocol.
Juo MCP remote server exposes subscription platform to AI assistants and agents via the [Model Context Protocol](https://modelcontextprotocol.io). Once connected, tools like Claude, Cursor, or any MCP-compatible client can read and manage subscriptions, customers, products, and more — directly from AI workflow.
## Server URL
```
https://mcp.juo.com/sse
```
The server uses **Server-Sent Events (SSE)** transport and requires authentication via the standard **OAuth 2.0** authorization code flow.
## Connecting a client
Add the following to `claude_desktop_config.json`:
```json theme={null}
{
"mcpServers": {
"juo": {
"url": "https://mcp.juo.com/sse"
}
}
}
```
On macOS the config file is located at:
```
~/Library/Application Support/Claude/claude_desktop_config.json
```
Restart Claude Desktop — it will open a browser window to complete OAuth authorization on first use.
Open **Cursor Settings → MCP** and add a new server:
| Field | Value |
| ----- | ------------------------- |
| Name | `juo` |
| Type | `sse` |
| URL | `https://mcp.juo.com/sse` |
Save and reload the window. Cursor will prompt you to authorize via OAuth before the tools become available.
Edit Windsurf MCP config file (`~/.codeium/windsurf/mcp_config.json`):
```json theme={null}
{
"mcpServers": {
"juo": {
"serverUrl": "https://mcp.juo.com/sse"
}
}
}
```
Restart Windsurf and complete the OAuth flow when prompted.
Add to `settings.json` (requires [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) with MCP support enabled):
```json theme={null}
{
"mcp.servers": {
"juo": {
"url": "https://mcp.juo.com/sse"
}
}
}
```
Reload the window and authorize via OAuth when prompted.
Add the following to [Continue](https://continue.dev) config (`~/.continue/config.yaml`):
```yaml theme={null}
mcpServers:
- name: juo
transport:
type: sse
url: https://mcp.juo.com/sse
```
Reload Continue and complete the OAuth authorization in the browser.
Add the Juo MCP server using the CLI:
```bash theme={null}
claude mcp add --transport sse juo https://mcp.juo.com/sse
```
Alternatively, add it manually to `~/.claude/settings.json`:
```json theme={null}
{
"mcpServers": {
"juo": {
"url": "https://mcp.juo.com/sse"
}
}
}
```
On the next Claude Code session start, the OAuth authorization flow will open in the browser.
Add to Zed `settings.json`:
```json theme={null}
{
"context_servers": {
"juo": {
"source": {
"type": "url",
"url": "https://mcp.juo.com/sse"
}
}
}
}
```
Reopen the project — Zed will prompt for OAuth authorization on first connection.
## OAuth authorization flow
The Juo MCP server uses the standard OAuth 2.0 authorization code flow with PKCE. When you connect for the first time:
1. Client initiates a connection to `https://mcp.juo.com/sse`.
2. The server signals that authorization is required.
3. Browser opens the Juo authorization page.
4. You log in with a Juo account and grant the requested permissions.
5. The auth server exchanges the code for an access token and returns it to the client — no manual copying of tokens required.
Tokens are scoped to a tenant and stored securely by an MCP client. Re-authorization is only required when a token expires or is revoked.
```mermaid theme={null}
sequenceDiagram
participant Client as MCP Client
participant Server as mcp.juo.com
participant Auth as Juo Auth
Client->>Server: Connect (SSE)
Server-->>Client: 401 + OAuth metadata
Client->>Auth: Authorization request (PKCE)
Auth-->>Client: Redirect to login
Client->>Auth: Login + grant permissions
Auth-->>Client: Authorization code
Client->>Auth: Exchange code for token
Auth-->>Client: Access token
Client->>Server: Connect with Bearer token
Server-->>Client: Connected — tools ready
```
## Available tools
Once connected, the following tools are available to AI client:
| Tool | Description |
| ------------------ | ----------------------------------------------------------------------- |
| `customers-list` | Paginated list of customers; search by ID, name, email, or phone |
| `customers-create` | Create a customer with contact details (name, email, phone, tags, note) |
| `customers-update` | Partial update of a customer's profile |
| `customers-delete` | Permanently delete a customer |
| Tool | Description |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| `subscriptions-list` | Paginated list; search by ID, serial, status, dates, or customer; optionally expand customer inline |
| `subscriptions-create` | Create a subscription with billing policy, items, delivery address, and payment method |
| `subscriptions-update` | Update payment method, delivery address/method/price, or next billing date |
| `subscriptions-cancel` | Cancel with an optional reason; optionally notify the customer by email |
| `subscriptions-pause` | Pause an active subscription |
| `subscriptions-resume` | Resume a paused subscription |
| `subscriptions-reactivate` | Reactivate a canceled subscription |
| Tool | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------- |
| `subscriptions-items-create` | Add a product variant to a subscription; supports custom price and recurring cycle limit |
| `subscriptions-items-update` | Update item quantity, billing policy, or delivery policy |
| `subscriptions-items-delete` | Remove an item (subscription must retain at least one item) |
| Tool | Description |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `subscriptions-discounts-create` | Apply a discount code or a manual discount (percentage or fixed amount) targeting all items, specific items, or shipping |
| `subscriptions-discounts-update` | Update discount value or recurring cycle limit |
| `subscriptions-discounts-delete` | Remove a discount from a subscription |
| Tool | Description |
| ----------------------------------- | ---------------------------------------------------------------------------------------------- |
| `products-list` | Paginated list; search by ID, title, status, or variant SKU; filter by subscription plan group |
| `products-create` | Create a product with title, description, vendor, type, tags, status, and optional image |
| `products-update` | Partial update of a product (title, description, vendor, type, status, tags) |
| `products-delete` | Permanently delete a product |
| `products-add-variant` | Add a variant (price, SKU, barcode, option values) to a product |
| `products-remove-variant` | Remove a variant from a product |
| `products-assign-product-to-plan` | Associate all variants of a product with a subscription plan |
| `products-remove-product-from-plan` | Disassociate all variants of a product from a subscription plan |
| `products-assign-variant-to-plan` | Associate a specific variant with a subscription plan |
| `products-remove-variant-from-plan` | Remove a specific variant from a subscription plan |
| Tool | Description |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `schedules-list` | View the upcoming billing order projection for a customer (read-only) |
| `schedules-list-adjustments` | List pending schedule adjustments for a customer |
| `schedules-create` | Create a one-off adjustment to an upcoming order: skip, change date, update shipping/payment, swap products, or apply a discount |
| `schedules-delete` | Delete a pending adjustment, reverting the affected order to its original state |
| Tool | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `workflows-list` | Paginated list of workflow definitions; optionally include archived or filter to interactive-only |
| `workflows-get` | Get the full definition of a workflow including steps, transitions, and publication status |
| `workflows-create` | Create a new workflow in draft status |
| `workflows-update` | Full replacement update of a workflow definition |
| `workflows-delete` | Soft-delete a workflow (run history is preserved) |
| `workflows-publish` | Publish a draft workflow, making it active for incoming events |
| `workflows-unpublish` | Move a published workflow back to draft without affecting in-flight runs |
| `workflows-archive` | Archive a workflow without deleting it |
| `workflows-duplicate` | Create a copy of a workflow as a new draft |
| `workflows-list-runs` | Paginated list of execution runs for a workflow |
| `workflows-get-run` | Get the current state of a specific run (active step, context, status) |
| `workflows-get-run-stats` | Aggregate stats: total/successful/failed runs, per-step counts, experiment variant enrollment and conversion rates |
| `workflows-export-runs` | Generate a signed URL to download all runs for a workflow as a CSV file |
# Overview
Source: https://juo.dev/docs/overview
The Juo platform provides a comprehensive subscription management solution that connects operators, subscribers, and external systems through well-defined interfaces and APIs.
### Actors and areas
The platform serves two primary user groups:
* **Operators**: Business owners and administrators who manage subscription businesses. Operators use the **Admin Portal** to manage subscribers, configure subscription plans, monitor performance, and handle administrative tasks. The Admin Portal interacts with the **Admin API**, which provides all administrative operations and business management capabilities.
* **Subscribers**: End customers who have active subscriptions. Subscribers use the **Subscriber Portal** (also referred to as Customer Portal) to manage their subscriptions, update payment methods, view subscription history, and perform self-service operations. The Subscriber Portal interacts with the **Customer API**, which provides subscriber-facing operations and data access.
### External systems
The Juo platform integrates with various external systems that are essential for running a subscription business, for example:
* **E-commerce platforms**: Integrates with platforms like Shopify to synchronize products, customers, and orders
* **Payment providers**: Connects with payment processors to handle subscription billing and payment processing
* **Support platforms**: Integrates with customer support systems to provide seamless customer service
* **Marketing platforms**: Connects with marketing tools for customer communication and campaign management
External systems integrate directly with the Juo platform through APIs and webhooks, enabling a seamless flow of data and operations across the subscription ecosystem.
```mermaid theme={null}
%%{
init: {
'flowchart': {
'subGraphTitleMargin': {
'top': 20,
'bottom': 20
}
}
}
}%%
graph TB
subgraph "Operators"
Operator[Operator]
end
subgraph "Subscribers"
Subscriber[Subscriber]
end
subgraph "Juo Platform"
AdminPortal[Admin Portal]
AdminAPI[Admin API]
SubscriberPortal[Subscriber Portal]
CustomerAPI[Customer API]
end
subgraph "External Systems"
Ecommerce[E-commerce Platform]
PaymentProvider[Payment Provider]
SupportPlatform[Support Platform]
MarketingPlatform[Marketing Platform]
end
Operator --> AdminPortal
AdminPortal --> AdminAPI
Subscriber --> SubscriberPortal
SubscriberPortal --> CustomerAPI
AdminAPI <--> Ecommerce
AdminAPI <--> PaymentProvider
AdminAPI <--> SupportPlatform
AdminAPI <--> MarketingPlatform
CustomerAPI <--> PaymentProvider
```