# 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 purple juo-block frame named 'SubscriptionCard' containing two green-bordered panels. The props panel has 'shape defined by the schema' as a subtitle at the top, followed by variant, size, and showThumbnail values. The slots panel has 'each slot exposed via juo-extension-root' as a subtitle at the top, followed by header, body, and actions slots. Below, a dashed 'renderer output' frame shows a rendered card with a thumbnail, a bold 'Your plan' heading, two lines of muted text ('Renews monthly · Next: Jul 5, 2026' and '$14.99 / month'), and a 'Manage' button on the right. ## 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) ``` Outer green juo-context-root contains two rose provideContext rectangles labelled ContextA and ContextB. Inside, a nested green juo-context-root labelled preview-root contains its own rose provideContext ContextA (overriding the outer one) and a purple juo-block SubscriptionList holding two rose injectContext rectangles, one for ContextA and one for ContextB. Two dashed rose arrows labelled 'resolves A' and 'resolves B' show ContextA resolving to the inner override while ContextB bubbles up past the inner root to the outer provideContext. 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} ``` 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. A green juo-context-root containing a gray app shell. Inside the app shell, two rose 'provideContext' rectangles sit in a row — one for ThemeStateContext and one for SubscriptionServiceContext. Below them, a cyan juo-page route='/' frame contains a single rose 'injectContext ThemeStateContext' rectangle. A dashed rose arrow labelled 'resolves' connects the provideContext ThemeStateContext to the injectContext inside juo-page. ### 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. An amber juo-extension-root name='main' frame containing a purple juo-block SubscriptionList. Inside the block, a rose 'injectContext SubscriptionServiceContext' rectangle sits above a dashed 'renderer output' panel showing a stylized subscription list — a 'Your subscriptions' header followed by three rows, each with a thumbnail, two text lines, a colored 'Active' or 'Paused' status pill, and a 'Manage' button. 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. A cyan juo-page route='/' frame containing two side-by-side amber juo-extension-root frames — 'aside' on the left with a purple juo-block Menu showing five icon-and-label rows, and 'main' on the right stacking a purple juo-block SubscriptionList with placeholder list rows above a purple juo-block Upsell rendering a thumbnail, two pitch lines, and an 'Add' button. 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.