# Create a Contact Source: https://docs.stora.co/2025-09/api-reference/contacts/create-a-contact /2025-09/openapi.json post /2025-09/contacts Create a new contact. Required authorization scope: `public.contact:write` # Delete a Contact Source: https://docs.stora.co/2025-09/api-reference/contacts/delete-a-contact /2025-09/openapi.json delete /2025-09/contacts/{contact_id} Delete a contact by its ID. Required authorization scope: `public.contact:write` # List all Contacts Source: https://docs.stora.co/2025-09/api-reference/contacts/list-all-contacts /2025-09/openapi.json get /2025-09/contacts Retrieve a list of all contacts. Required authorization scope: `public.contact:read` # Show a Contact Source: https://docs.stora.co/2025-09/api-reference/contacts/show-a-contact /2025-09/openapi.json get /2025-09/contacts/{contact_id} Retrieve a specific contact by its ID. Required authorization scope: `public.contact:read` # Update a Contact Source: https://docs.stora.co/2025-09/api-reference/contacts/update-a-contact /2025-09/openapi.json patch /2025-09/contacts/{contact_id} Update an existing contact. Required authorization scope: `public.contact:write` # List all Contract Templates Source: https://docs.stora.co/2025-09/api-reference/contract-templates/list-all-contract-templates /2025-09/openapi.json get /2025-09/contract_templates Retrieve a list of all contract templates. Required authorization scope: `public.contract_template:read` # List all Contracts Source: https://docs.stora.co/2025-09/api-reference/contracts/list-all-contracts /2025-09/openapi.json get /2025-09/contracts Retrieve a list of all contracts. Required authorization scope: `public.contract:read` # Show a Contract Source: https://docs.stora.co/2025-09/api-reference/contracts/show-a-contract /2025-09/openapi.json get /2025-09/contracts/{contract_id} Retrieve a specific contract by its ID. Supports PDF download. Set `application/pdf` in the `Accept` request header. Only signed contracts return a PDF. Required authorization scope: `public.contract:read` # List all Coupons Source: https://docs.stora.co/2025-09/api-reference/coupons/list-all-coupons /2025-09/openapi.json get /2025-09/coupons Retrieve a list of all coupons. Required authorization scope: `public.coupon:read` # Show a Coupon Source: https://docs.stora.co/2025-09/api-reference/coupons/show-a-coupon /2025-09/openapi.json get /2025-09/coupons/{coupon_id} Retrieve a specific coupon by its ID. Required authorization scope: `public.coupon:read` # List all Credit Notes Source: https://docs.stora.co/2025-09/api-reference/credit-notes/list-all-credit-notes /2025-09/openapi.json get /2025-09/credit_notes Retrieve a list of all credit notes. Required authorization scope: `public.credit_note:read` # Show a Credit Note Source: https://docs.stora.co/2025-09/api-reference/credit-notes/show-a-credit-note /2025-09/openapi.json get /2025-09/credit_notes/{credit_note_id} Retrieve a specific credit note by its ID. Supports PDF download. Set `application/pdf` in the `Accept` request header. Required authorization scope: `public.credit_note:read` # List all Stages Source: https://docs.stora.co/2025-09/api-reference/deals-stages/list-all-stages /2025-09/openapi.json get /2025-09/deals/stages Retrieve a list of all stages. Required authorization scope: `public.deal_stage:read` # Show a Deal Stage Source: https://docs.stora.co/2025-09/api-reference/deals-stages/show-a-deal-stage /2025-09/openapi.json get /2025-09/deals/stages/{stage_id} Retrieve a specific deal stage by its ID. Required authorization scope: `public.deal_stage:read` # Create a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/create-a-deal /2025-09/openapi.json post /2025-09/deals Create a new deal. Required authorization scope: `public.deal:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Delete a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/delete-a-deal /2025-09/openapi.json delete /2025-09/deals/{deal_id} Delete a deal by its ID. Required authorization scope: `public.deal:write` # List all Deals Source: https://docs.stora.co/2025-09/api-reference/deals/list-all-deals /2025-09/openapi.json get /2025-09/deals Retrieve a list of all deals. Required authorization scope: `public.deal:read` # Lose a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/lose-a-deal /2025-09/openapi.json post /2025-09/deals/{deal_id}/lose Mark a deal as lost. Required authorization scope: `public.deal:write` # Reopen a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/reopen-a-deal /2025-09/openapi.json post /2025-09/deals/{deal_id}/reopen Reopen a closed deal (won or lost) and place it back on the deals board at the specified stage. Required authorization scope: `public.deal:write` # Show a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/show-a-deal /2025-09/openapi.json get /2025-09/deals/{deal_id} Retrieve a specific deal by its ID. Required authorization scope: `public.deal:read` # Update a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/update-a-deal /2025-09/openapi.json patch /2025-09/deals/{deal_id} Update an existing deal. Required authorization scope: `public.deal:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Win a Deal Source: https://docs.stora.co/2025-09/api-reference/deals/win-a-deal /2025-09/openapi.json post /2025-09/deals/{deal_id}/win Mark a deal as won. Required authorization scope: `public.deal:write` # List all Identity Verifications Source: https://docs.stora.co/2025-09/api-reference/identity-verifications/list-all-identity-verifications /2025-09/openapi.json get /2025-09/identity_verifications Retrieve a list of all identity verifications. Required authorization scope: `public.identity_verification:read` # Show an Identity Verification Source: https://docs.stora.co/2025-09/api-reference/identity-verifications/show-an-identity-verification /2025-09/openapi.json get /2025-09/identity_verifications/{identity_verification_id} Retrieve a specific identity verification by its ID. Required authorization scope: `public.identity_verification:read` # Get an image Source: https://docs.stora.co/2025-09/api-reference/images/get-an-image /2025-09/openapi.json get /2025-09/images/{token}/{filename} Returns a permanent URL suitable for use in `` tags and other embedded contexts. The URL itself never expires, but when accessed it redirects to a temporary pre-signed storage URL that expires after 5 minutes. This endpoint does not require authentication — the verified token acts as authorization. Complete URLs are provided in API responses for resources like sites and unit types. # List all Invoices Source: https://docs.stora.co/2025-09/api-reference/invoices/list-all-invoices /2025-09/openapi.json get /2025-09/invoices Retrieve a list of all invoices. Required authorization scope: `public.invoice:read` # Show an Invoice Source: https://docs.stora.co/2025-09/api-reference/invoices/show-an-invoice /2025-09/openapi.json get /2025-09/invoices/{invoice_id} Retrieve a specific invoice by its ID. Supports PDF download. Set `application/pdf` in the `Accept` request header. Required authorization scope: `public.invoice:read` # Create a Note Source: https://docs.stora.co/2025-09/api-reference/notes/create-a-note /2025-09/openapi.json post /2025-09/notes Create a new note. Required authorization scope: `public.note:write` # Delete a Note Source: https://docs.stora.co/2025-09/api-reference/notes/delete-a-note /2025-09/openapi.json delete /2025-09/notes/{note_id} Delete a note by its ID. Required authorization scope: `public.note:write` # List all Notes Source: https://docs.stora.co/2025-09/api-reference/notes/list-all-notes /2025-09/openapi.json get /2025-09/notes Retrieve a list of all notes. Required authorization scope: `public.note:read` # Show a Note Source: https://docs.stora.co/2025-09/api-reference/notes/show-a-note /2025-09/openapi.json get /2025-09/notes/{note_id} Retrieve a specific note by its ID. Required authorization scope: `public.note:read` # Update a Note Source: https://docs.stora.co/2025-09/api-reference/notes/update-a-note /2025-09/openapi.json patch /2025-09/notes/{note_id} Update an existing note. Required authorization scope: `public.note:write` # Introspect an Access Token Source: https://docs.stora.co/2025-09/api-reference/oauth-2/introspect-an-access-token /2025-09/openapi.json post /oauth2/introspect Introspect an OAuth 2 Access Token using another Access Token. # Retrieve an Access Token Source: https://docs.stora.co/2025-09/api-reference/oauth-2/retrieve-an-access-token /2025-09/openapi.json post /oauth2/token Retrieve an OAuth 2 Access Token. # Retrieve info for the Access Token Source: https://docs.stora.co/2025-09/api-reference/oauth-2/retrieve-info-for-the-access-token /2025-09/openapi.json get /oauth2/token/info Retrieve an OAuth 2 Access Token information. # Revoke an Access Token Source: https://docs.stora.co/2025-09/api-reference/oauth-2/revoke-an-access-token /2025-09/openapi.json post /oauth2/revoke Revoke an OAuth 2 Access Token using your Client Credentials. You may pass either the access token or its refresh token as `token`. # List all Opportunities Source: https://docs.stora.co/2025-09/api-reference/opportunities/list-all-opportunities /2025-09/openapi.json get /2025-09/opportunities Retrieve a list of all opportunities. Required authorization scope: `public.opportunity:read` # Show an Opportunity Source: https://docs.stora.co/2025-09/api-reference/opportunities/show-an-opportunity /2025-09/openapi.json get /2025-09/opportunities/{opportunity_id} Retrieve a specific opportunity by its ID. Required authorization scope: `public.opportunity:read` # Create a Line Item Source: https://docs.stora.co/2025-09/api-reference/orders-line-items/create-a-line-item /2025-09/openapi.json post /2025-09/orders/{order_id}/line_items Create a new line item for the order. Create a new line item can be done only when the order is in draft status. Required authorization scope: `public.order:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Delete a Line Item Source: https://docs.stora.co/2025-09/api-reference/orders-line-items/delete-a-line-item /2025-09/openapi.json delete /2025-09/orders/{order_id}/line_items/{line_item_id} Delete an existing line item. Only line items from orders in draft status can be removed. Required authorization scope: `public.order:write` # List all Line Items Source: https://docs.stora.co/2025-09/api-reference/orders-line-items/list-all-line-items /2025-09/openapi.json get /2025-09/orders/{order_id}/line_items Retrieve a list of all line items for the order. Required authorization scope: `public.order:read` # Update a Line Item Source: https://docs.stora.co/2025-09/api-reference/orders-line-items/update-a-line-item /2025-09/openapi.json patch /2025-09/orders/{order_id}/line_items/{line_item_id} Update an existing line item. Only line items from orders in draft status can be updated. Required authorization scope: `public.order:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Abandon an Order Source: https://docs.stora.co/2025-09/api-reference/orders/abandon-an-order /2025-09/openapi.json post /2025-09/orders/{order_id}/abandon Abandon an order that the customer is no longer expected to complete. Intended for customer-driven abandonment: use it to signal that the customer has not returned to complete an order they began. It is at the operator's discretion when to treat an order as abandoned — whether after hours, days, or weeks. It must not be used for operator-driven cancellations; a dedicated cancellation endpoint will be provided for that purpose. Only orders in the `draft` or `finalized` status can be abandoned. Required authorization scope: `public.order:write` # Create an Order Source: https://docs.stora.co/2025-09/api-reference/orders/create-an-order /2025-09/openapi.json post /2025-09/orders Create a new order. Required authorization scope: `public.order:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Delete an Order Source: https://docs.stora.co/2025-09/api-reference/orders/delete-an-order /2025-09/openapi.json delete /2025-09/orders/{order_id} Delete an order by its ID. Required authorization scope: `public.order:write` # Finalize an Order Source: https://docs.stora.co/2025-09/api-reference/orders/finalize-an-order /2025-09/openapi.json post /2025-09/orders/{order_id}/finalize Finalize an order to process it for payment. Required authorization scope: `public.order:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # List all Orders Source: https://docs.stora.co/2025-09/api-reference/orders/list-all-orders /2025-09/openapi.json get /2025-09/orders Retrieve a list of all orders. Required authorization scope: `public.order:read` # Show an Order Source: https://docs.stora.co/2025-09/api-reference/orders/show-an-order /2025-09/openapi.json get /2025-09/orders/{order_id} Retrieve a specific order by its ID. Required authorization scope: `public.order:read` # Update an Order Source: https://docs.stora.co/2025-09/api-reference/orders/update-an-order /2025-09/openapi.json patch /2025-09/orders/{order_id} Update an existing order. Required authorization scope: `public.order:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Validate an Order Source: https://docs.stora.co/2025-09/api-reference/orders/validate-an-order /2025-09/openapi.json get /2025-09/orders/{order_id}/validate Validate an order to ensure it is ready to be finalized. Required authorization scope: `public.order:read` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Create a Price Adjustment Source: https://docs.stora.co/2025-09/api-reference/price-adjustments/create-a-price-adjustment /2025-09/openapi.json post /2025-09/price_adjustments Create a new price adjustment for a subscription. The adjustment date is resolved from `effective_from` to the subscription's first renewal date on or after it, and returned as `adjust_on`. The price adjustment lands as a `draft` unless `schedule` is `true`, in which case it is scheduled (`pending`) immediately. When the `notification` object is omitted, the operator's default notice period applies. If that default does not fit before the resolved `adjust_on`, `notification.notice_period` reads back as `send_immediately`. An operator whose default is no notice at all resolves to no notification, which requires the `public.price_adjustment:skip_notifications` scope just as an explicit `null` does. Notifications are only sent for scheduled (`pending`) adjustments: a `send_immediately` notice is delivered as soon as the adjustment is scheduled, so a `draft` reads back `notification.sent_at` as `null` until it is scheduled. A `notice_period` of `null` skips customer notification through Stora and requires the `public.price_adjustment:skip_notifications` scope. A subscription can hold only one scheduled price adjustment at a time. Scheduling a second one is a `409`; delete the existing adjustment before scheduling a replacement. The restriction lifts once the existing adjustment has been applied, so the next one can be scheduled while the previous is still awaiting its invoice. A `409` is also returned when the request conflicts with the subscription's current state: the subscription has not been billed through Stripe (`stripe_subscription_id` is null), is not active, is archived, or is scheduled to end; the subscription has no future renewal dates to adjust on; or a scheduled adjustment already exists for that subscription on the resolved `adjust_on`. Neither check applies to drafts, so a `draft` can always be created alongside a scheduled adjustment. Required authorization scope: `public.price_adjustment:write` # Delete a Price Adjustment Source: https://docs.stora.co/2025-09/api-reference/price-adjustments/delete-a-price-adjustment /2025-09/openapi.json delete /2025-09/price_adjustments/{price_adjustment_id} Delete a price adjustment by its ID. Only `draft`, `pending` and `cancelled` price adjustments can be deleted. Deleting a `pending` adjustment calls it off before it fires, and no customer notification is sent about the deletion. Required authorization scope: `public.price_adjustment:write` # List all Price Adjustments Source: https://docs.stora.co/2025-09/api-reference/price-adjustments/list-all-price-adjustments /2025-09/openapi.json get /2025-09/price_adjustments Retrieve a list of all price adjustments. Required authorization scope: `public.price_adjustment:read` # Schedule a Price Adjustment Source: https://docs.stora.co/2025-09/api-reference/price-adjustments/schedule-a-price-adjustment /2025-09/openapi.json post /2025-09/price_adjustments/{price_adjustment_id}/schedule Schedule a `draft` price adjustment, moving it to `pending` so that it is applied on its `adjust_on` date. Only a `draft` can be scheduled. A `409` is returned when the request conflicts with the price adjustment's current state: it has already been scheduled; it can no longer be scheduled, such as a cancelled or failed adjustment; its `adjust_on` date has passed or is today, since an adjustment cannot take effect the same day, in which case it stays a `draft`, to be deleted or replaced; or another price adjustment is already scheduled for the same subscription on that `adjust_on` date. A subscription can hold only one scheduled price adjustment at a time. Promoting a draft while the subscription already has one is a `409`, and the draft is left untouched. Delete the existing adjustment first, or wait until it has been applied. Drafts hold no slot, so any number of them can be staged against the same subscription. A `409` is also returned when the subscription is no longer eligible for price adjustments: it has not been billed through Stripe (`stripe_subscription_id` is null), is not active, is archived, or is scheduled to end. A draft can be created against a subscription that is later cancelled, so this is checked again as the draft is scheduled rather than only at creation. Scheduling sends the customer notification. Where the draft's notice period no longer fits before `adjust_on`, the notice is sent today and `notification.notice_period` reads back as `send_immediately`. A draft whose `notification.notice_period` is `null` skips customer notification through Stora entirely, so scheduling it requires the `public.price_adjustment:skip_notifications` scope. A draft notifies nobody until it is scheduled, which is why the scope is checked here as well as at creation. `estimation` is recalculated from the subscription's current prices as the draft is scheduled, so a draft that has been sitting is scheduled against what the subscription is billed today rather than what it was billed when the draft was created. A subscription with nothing left to adjust is a `422`. Required authorization scope: `public.price_adjustment:write` # Show a Price Adjustment Source: https://docs.stora.co/2025-09/api-reference/price-adjustments/show-a-price-adjustment /2025-09/openapi.json get /2025-09/price_adjustments/{price_adjustment_id} Retrieve a specific price adjustment by its ID. Required authorization scope: `public.price_adjustment:read` # List all Product Categories Source: https://docs.stora.co/2025-09/api-reference/product-categories/list-all-product-categories /2025-09/openapi.json get /2025-09/product_categories Retrieve a list of all product categories. Required authorization scope: `public.product_category:read` # Show a Product Category Source: https://docs.stora.co/2025-09/api-reference/product-categories/show-a-product-category /2025-09/openapi.json get /2025-09/product_categories/{product_category_id} Retrieve a specific product category by its ID. Required authorization scope: `public.product_category:read` # Create Base Price for the Product Source: https://docs.stora.co/2025-09/api-reference/products/create-base-price-for-the-product /2025-09/openapi.json post /2025-09/products/{product_id}/base_price Set base prices for the selected product. Required authorization scope: `public.product:write` # List all Products Source: https://docs.stora.co/2025-09/api-reference/products/list-all-products /2025-09/openapi.json get /2025-09/products Retrieve a list of all products. Required authorization scope: `public.product:read` # Show a Product Source: https://docs.stora.co/2025-09/api-reference/products/show-a-product /2025-09/openapi.json get /2025-09/products/{product_id} Retrieve a specific product by its ID. Required authorization scope: `public.product:read` # Create Base Price for the Protection Level Source: https://docs.stora.co/2025-09/api-reference/protection-levels/create-base-price-for-the-protection-level /2025-09/openapi.json post /2025-09/protection_levels/{protection_level_id}/base_price Set base prices for the selected protection level. Required authorization scope: `public.protection_level:write` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # List all Protection Levels Source: https://docs.stora.co/2025-09/api-reference/protection-levels/list-all-protection-levels /2025-09/openapi.json get /2025-09/protection_levels Retrieve a list of protection levels. Required authorization scope: `public.protection_level:read` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # Show a Protection Level Source: https://docs.stora.co/2025-09/api-reference/protection-levels/show-a-protection-level /2025-09/openapi.json get /2025-09/protection_levels/{protection_level_id} Retrieve a specific protection level by its ID. Required authorization scope: `public.protection_level:read` **This endpoint is changing in a breaking way** as we restructure how Protection Levels work. Contact us at [api\_support@stora.co](mailto:api_support@stora.co) and read [this guide](https://docs.stora.co/2025-09/guides/site-scoped-protection-levels) before starting any integration. # List all Sites Source: https://docs.stora.co/2025-09/api-reference/sites/list-all-sites /2025-09/openapi.json get /2025-09/sites Retrieve a list of all sites. Required authorization scope: `public.site:read` # Show a Site Source: https://docs.stora.co/2025-09/api-reference/sites/show-a-site /2025-09/openapi.json get /2025-09/sites/{site_id} Retrieve a specific site by its ID. Required authorization scope: `public.site:read` # List all Staff Source: https://docs.stora.co/2025-09/api-reference/staff/list-all-staff /2025-09/openapi.json get /2025-09/staff Retrieve a list of all staff. Required authorization scope: `public.staff:read` # Show a Staff Member Source: https://docs.stora.co/2025-09/api-reference/staff/show-a-staff-member /2025-09/openapi.json get /2025-09/staff/{staff_id} Retrieve a specific staff member by its ID. Required authorization scope: `public.staff:read` # List all Subscriptions Source: https://docs.stora.co/2025-09/api-reference/subscriptions/list-all-subscriptions /2025-09/openapi.json get /2025-09/subscriptions Retrieve a list of all subscriptions. Required authorization scope: `public.subscription:read` # Show a Subscription Source: https://docs.stora.co/2025-09/api-reference/subscriptions/show-a-subscription /2025-09/openapi.json get /2025-09/subscriptions/{subscription_id} Retrieve a specific subscription by its ID. Required authorization scope: `public.subscription:read` # Complete a Task Source: https://docs.stora.co/2025-09/api-reference/tasks/complete-a-task /2025-09/openapi.json post /2025-09/tasks/{task_id}/complete Mark a task as complete. Required authorization scope: `public.task:write` # Create a Task Source: https://docs.stora.co/2025-09/api-reference/tasks/create-a-task /2025-09/openapi.json post /2025-09/tasks Create a new task. Required authorization scope: `public.task:write` # Delete a Task Source: https://docs.stora.co/2025-09/api-reference/tasks/delete-a-task /2025-09/openapi.json delete /2025-09/tasks/{task_id} Delete a task by its ID. Required authorization scope: `public.task:write` # List all Tasks Source: https://docs.stora.co/2025-09/api-reference/tasks/list-all-tasks /2025-09/openapi.json get /2025-09/tasks Retrieve a list of all tasks with the correct authorization scope. Required authorization scope: `public.task:read` # Open a Task Source: https://docs.stora.co/2025-09/api-reference/tasks/open-a-task /2025-09/openapi.json post /2025-09/tasks/{task_id}/open Mark a task as open. Required authorization scope: `public.task:write` # Show a Task Source: https://docs.stora.co/2025-09/api-reference/tasks/show-a-task /2025-09/openapi.json get /2025-09/tasks/{task_id} Retrieve a specific task by its ID. Required authorization scope: `public.task:read` # Update a Task Source: https://docs.stora.co/2025-09/api-reference/tasks/update-a-task /2025-09/openapi.json patch /2025-09/tasks/{task_id} Update an existing task. Required authorization scope: `public.task:write` # List all Tenancies Source: https://docs.stora.co/2025-09/api-reference/tenancies/list-all-tenancies /2025-09/openapi.json get /2025-09/tenancies Retrieve a list of all tenancies. Required authorization scope: `public.tenancy:read` # Show a Tenancy Source: https://docs.stora.co/2025-09/api-reference/tenancies/show-a-tenancy /2025-09/openapi.json get /2025-09/tenancies/{tenancy_id} Retrieve a specific tenancy by its ID. Required authorization scope: `public.tenancy:read` # Create an Event Source: https://docs.stora.co/2025-09/api-reference/timeline-events/create-an-event /2025-09/openapi.json post /2025-09/timeline/events Create a new timeline event from an external source. **Check the template you want to use to see which required pre-defined variables it includes. Custom variables are not enforced on creation.** Required authorization scope: `public.timeline_event:write` # Allocations and access Source: https://docs.stora.co/2025-09/guides/allocations-and-access Understand how units are allocated to tenancies, how access is granted and restricted, and how Stora keeps third-party access control systems in sync. When a booking completes, Stora creates a [tenancy and subscription](/2025-09/guides/core-concepts#the-rental-lifecycle-orders-tenancies-and-subscriptions). But how does the customer actually get access to a physical unit? That's what **unit allocations** handle. A unit allocation links a specific unit to a tenancy. It tracks when the unit was reserved, when access was granted, and by whom. Every allocation-related action — reserving, granting access, overlocking, deallocating — changes the unit's status and, if the operator uses a smart entry provider, automatically syncs that change to their access control hardware. ## How the pieces connect A unit allocation sits between a tenancy and a unit. It's the record that says "this specific unit has been assigned to this tenancy." ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4', 'edgeLabelBackground': '#FF7237'}}}%% graph LR Site --> Tenancy Tenancy --> Contact Tenancy --> UA["Unit Allocation"] UA --> Unit Unit --> UT["Unit Type"] ``` * A **site** has **tenancies** — each one a storage agreement with a **contact**. * Each tenancy can have one or more **unit allocations** — one per physical unit assigned. * Each unit allocation points to a **unit**, which belongs to a **unit type** at the same site. The unit allocation also records: * **When** the unit was reserved (`reserved_at`) * **When** access was granted (`granted_access_at`) ## Unit status lifecycle Every unit has a status that reflects where it is in the allocation lifecycle. The status changes as the unit moves through reservation, occupancy, and eventual deallocation. | Status | Meaning | | ------------ | ------------------------------------------------ | | `available` | Ready to be allocated | | `reserved` | Allocated to a tenancy that hasn't started yet | | `occupied` | Tenant has access | | `overlocked` | Access restricted — typically due to non-payment | Units can also be `unavailable` (taken offline for maintenance) or `repossessed` (contents marked for repossession). These are operator-managed states and aren't covered in detail here. ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'flowchart': {'defaultRenderer': 'elk'}, 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4', 'edgeLabelBackground': '#FF7237'}}}%% graph TD available -->|"  reserve  "| reserved available -->|"  grant access  "| occupied reserved -->|"  grant access  "| occupied reserved -->|"  deallocate  "| available occupied -->|"  overlock  "| overlocked occupied -->|"  deallocate  "| available overlocked -->|"  remove overlock  "| occupied overlocked -->|"  deallocate  "| available ``` The happy path is straightforward: `available` → `reserved` → `occupied` → `available`. Overlocking and removing overlocks handle the non-payment exception path. ## How units get allocated There are two ways a unit gets allocated to a tenancy: automatically by Stora when an order completes, or manually via the API. ### Auto-reservation If the operator has auto-reservation enabled, Stora attempts to reserve the requested units when a storefront or Public API order completes. It selects available units of the requested type and creates the allocations. After the attempt, Stora emits one event that reports the outcome for the tenancy: | Event | Outcome | | ---------------------------------------------- | ------------------------------------------------ | | `tenancy.auto_reservation.succeeded` | All requested units were reserved | | `tenancy.auto_reservation.partially_succeeded` | Some, but not all, requested units were reserved | | `tenancy.auto_reservation.failed` | No requested units were reserved | Each successful unit reservation also emits `unit.reserved`. Use the tenancy event to determine the outcome of the complete auto-reservation attempt. For a partial or failed attempt, assign the unreserved units manually. If auto-reservation is disabled, assign all units manually. ### Reserving a unit via the API To allocate a unit to a tenancy that hasn't started yet, use the reserve endpoint: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/units/unit_1e36123098e22cf8/reserve \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tenancy_id": "ten_acaf3269a573af74" }' ``` ```json theme={null} { "unit": { "id": "unit_1e36123098e22cf8", "status": "reserved", "unit_allocation": { "id": "alloc_5f7475758314968d" } } } ``` The unit transitions from `available` to `reserved` and a unit allocation is created. **Constraints:** * The unit must be `available`. * The tenancy's start date must be in the future. If the tenancy has already started, use [grant access](#granting-access-via-the-api) instead. ## Granting access A reserved unit isn't accessible yet — the tenant can't physically enter the storage space. The unit needs to transition to `occupied` for access to be granted. ### Automatic on move-in day Stora automatically transitions reserved units to `occupied` on the tenancy's start date. This runs at approximately 6:00 AM in the operator's local timezone. Once the transition happens, the `unit.occupied` webhook fires and, if the operator uses a smart entry provider, the access control system is synced to grant the tenant physical access. ### Granting access via the API You don't have to wait for move-in day. The grant access endpoint lets you transition a unit to `occupied` immediately: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/units/unit_1e36123098e22cf8/grant_access \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tenancy_id": "ten_acaf3269a573af74" }' ``` ```json theme={null} { "unit": { "id": "unit_1e36123098e22cf8", "status": "occupied", "unit_allocation": { "id": "alloc_5f7475758314968d" } } } ``` Grant access handles two scenarios: | Starting status | What happens | | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `reserved` | The existing reservation transitions to `occupied`. The unit must be reserved for the same tenancy you provide in the request. | | `available` | The unit is allocated *and* access is granted in a single step — no separate reserve call needed. | This is useful when you want to give a tenant early access before their tenancy officially starts, or when you're managing allocations entirely through the API and don't need the intermediate `reserved` state. ## Restricting and restoring access ### Overlocking Overlocking restricts a tenant's access to their units — typically because of failed payments. The overlock endpoint operates on all occupied units for a given contact: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/units/overlock \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contact_id": "con_0ac0514ed0711462" }' ``` ```json theme={null} { "success": { "message": "2 customer units were successfully overlocked." }, "meta": { "unit_ids": [ "unit_1e36123098e22cf8", "unit_2e36123098e22cf8" ] } } ``` All of the contact's occupied units transition to `overlocked`. A `unit.overlocked` webhook fires for each unit. Operators can also configure Stora to auto-overlock units when a payment fails. If this is enabled, Stora handles the transition automatically — you don't need to call the overlock endpoint yourself. The `unit.overlocked` webhook still fires so your integration can react. ### Removing an overlock To restore access, use the remove overlock endpoint with the same contact ID: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/units/remove_overlock \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contact_id": "con_0ac0514ed0711462" }' ``` All of the contact's overlocked units transition back to `occupied`. A `unit.occupied` webhook fires for each unit. ## Deallocating a unit Deallocation removes the unit allocation entirely and returns the unit to `available`: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/units/unit_1e36123098e22cf8/deallocate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```json theme={null} { "unit": { "id": "unit_1e36123098e22cf8", "status": "available", "unit_allocation": null } } ``` A unit can be deallocated from any allocated status: `reserved`, `occupied`, `overlocked`, or `repossessed`. ### Auto-deallocation on move-out If the operator has auto-deallocation enabled, Stora automatically deallocates units when a tenancy ends. The `unit.deallocated` and `unit.available` webhooks fire, and the access control system is synced to revoke access. ## Access control sync If the operator has configured a smart entry provider in Stora (such as Noke, PTI, Paxton, or any of the other supported providers), **every status change described in this guide automatically syncs to the third-party access control system**. You don't need to do anything extra. When you call an allocation endpoint — reserve, grant access, overlock, remove overlock, or deallocate — Stora: 1. Updates the unit's status 2. Publishes the change to the configured access control provider 3. Notifies the provider to grant or restrict physical access accordingly For example, when a unit transitions to `occupied`, Stora tells the access control provider to allow the tenant entry. When it transitions to `overlocked`, the provider restricts access. When it's deallocated, the provider revokes access entirely. This means the API is your single point of control for both the logical state of a unit and the physical access to it. You don't need to integrate with individual access control providers — Stora handles that layer. ## Webhook events Subscribe to these events to react to allocation changes in real time: | Event | When it fires | | ------------------ | ---------------------------------------------- | | `unit.reserved` | A unit has been reserved for a tenancy | | `unit.occupied` | A unit is now occupied — the tenant has access | | `unit.overlocked` | Access has been restricted | | `unit.deallocated` | The unit allocation has been removed | | `unit.available` | The unit is available again | Webhook payloads include the full unit resource, so you can update your local state directly without an additional API call. See [Webhooks](/2025-09/guides/webhooks) for setup and payload structure. # API versioning Source: https://docs.stora.co/2025-09/guides/api-versioning Understand Stora API versions, compatibility guarantees, deprecation timelines, and how to migrate. Stora uses dated API versions to let you plan upgrades without unexpected changes to your integration. A version defines the contract for versioned REST API resource requests, responses, and webhook payloads. The current API version is `2025-09`. Review its [OpenAPI specification](/2025-09/openapi.json) or the [changelog](/2025-09/guides/changelog). ## How API versions work Versioned resource endpoints use the `YYYY-MM` format in the request path: ```text theme={null} https://public-api.stora.co/2025-09/sites ``` For resource endpoints, the path determines the version used for API requests and responses. OAuth 2 token endpoints under `/oauth2/` are not date-versioned. Webhook endpoints also store an API version, which determines the structure of their payloads. See [Webhooks](/2025-09/guides/webhooks) for setup details. ## Compatibility within a version Stora does not introduce breaking changes within a published API version. A breaking change requires a new dated version. We may make backward-compatible changes within a version, including: * Adding an endpoint * Adding an optional request field * Adding a response field * Deprecating a field while keeping it available * Fixing behavior so it matches the documented contract Your integration should ignore response fields it does not recognize so that compatible additions do not disrupt it. Changes that require a new version include: * Removing or renaming an endpoint or field * Changing a field's type * Making an optional request field required * Changing documented behavior in a way that requires client changes ## Version lifecycle Each API version passes through four lifecycle milestones: | Milestone | Access and support | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Current** | The newest version. It receives backward-compatible features, defect fixes, and security updates. | | **Deprecated** | Begins when a successor is released. Standard and Enterprise customers retain access and receive security fixes and fixes for material defects. New features target the current version. | | **Standard retirement** | Standard customers retain access through the last calendar day of the sixth month after deprecation. Access ends at 00:00 UTC the following day. | | **Enterprise retirement** | Enterprise customers retain access through the last calendar day of the twelfth month after deprecation. Access ends at 00:00 UTC the following day. | Enterprise customers continue to receive maintenance support during months seven through twelve. If Stora releases multiple versions within twelve months, their support windows may overlap. For example, if a version is deprecated on May 15, 2026, the last supported day is November 30, 2026 for Standard customers and May 31, 2027 for Enterprise customers. ## Supported versions | Version | Beta release | Public release | Status | Standard last supported day | Enterprise last supported day | Migration guide | | --------- | ------------------ | ----------------- | ------- | --------------------------- | ----------------------------- | --------------- | | `2025-09` | September 30, 2025 | December 16, 2025 | Current | — | — | — | The current version has no scheduled retirement date. When we release a successor, we add separate Standard and Enterprise last supported days and link its dedicated migration guide. ## Deprecation notifications When Stora releases a new API version, we publish the previous version's deprecation date, Standard last supported day, Enterprise last supported day, and migration guide: * On this page * In the [changelog](/2025-09/guides/changelog) * By email to developers registered in the Developer Portal Stora may use a shorter timeline only when necessary to address a serious security, privacy, legal, or regulatory risk. We will communicate the reason and revised timeline as early as possible through the same notification channels. ## Migrate to a new version We publish a dedicated migration guide with every new API version. The guide explains each breaking change and the work required to move from the previous version. We link it from the supported-versions table, [changelog](/2025-09/guides/changelog), and deprecation email. Work through the release-specific migration guide for your current and target versions. Check the [changelog](/2025-09/guides/changelog) and new OpenAPI specification for complete request, response, and webhook schemas. Replace the version in your API request paths and update your integration for the new contract. Change each webhook endpoint to the new API version so its payloads use the new structure. See [Webhooks](/2025-09/guides/webhooks) for endpoint configuration. Test affected API calls and webhook handlers, then deploy before your applicable Standard or Enterprise last supported day. # Authentication Source: https://docs.stora.co/2025-09/guides/authentication Set up authentication with the Stora Public API using access tokens or OAuth 2.0. ## Prerequisites You'll need a [Developer Portal](/2025-09/guides/developer-portal) account. Use the Developer Portal to create a test operator, manage API access for a connected operator account, or create a partner app. You do not need to connect a real operator account just to explore the API. Create a test operator first if you're exploring the API. Test operators let you try credentials, requests, and webhook setup before working with real operator data. ## Choose your connection method Stora supports two ways to authenticate. Both use the same scopes and the same `Authorization: Bearer ` header — they differ in how you get the token. ### Access tokens A static secret key you generate in the Developer Portal. Use it immediately in requests — no token exchange needed. **Good for:** scripts, internal tools, quick automations, exploring the API. | | | | -------- | ------------------------------------------- | | Setup | Generate in the Developer Portal | | Security | The token is long-lived. Store it securely. | | Expiry | Configurable when you create it | | Scopes | Configurable, can be modified later | ### OAuth 2.0 applications Short-lived access tokens issued via the OAuth 2.0 standard. Supports two grant types: * **Client Credentials** — server-to-server, no user interaction required * **Authorization Code** — for partner integrations where an operator authorises your app **Good for:** production integrations, third-party apps, anything you share with others. | | | | -------- | ---------------------------------------------------- | | Setup | Create or manage credentials in the Developer Portal | | Security | Tokens are short-lived (2 hours) | | Expiry | Automatic — request or refresh tokens as needed | | Scopes | Configurable, can be modified later | Start with an access token to explore the API. Move to an OAuth application when you're building for production or distributing to third parties. ## Option A: Access token Go to [`developer.stora.co`](https://developer.stora.co) and choose the flow that matches your work: **Testing / looking around**, **Manage accounts**, or **Create partner app**. Create an access token for your test operator or connected operator account. Start with read-only scopes (e.g. `public.site:read`). Choose when the token should expire. Copy it immediately — you won't be able to see it again. Use it directly in requests: ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/sites" \ -H "accept: application/json" \ -H "authorization: Bearer YOUR_ACCESS_TOKEN" ``` ## Option B: OAuth 2.0 — Client Credentials Use this when your server needs to talk to Stora without user interaction. Go to [`developer.stora.co`](https://developer.stora.co) and choose **Testing / looking around** for test credentials, **Manage accounts** for a connected operator account, or **Create partner app** for a multi-operator integration. Use a test operator for exploration. Use **Manage accounts** when you need credentials for a specific real operator. Create OAuth 2.0 Client Credentials and select the scopes your application needs. Copy your `client_id` and `client_secret`. Exchange your credentials for a short-lived access token: ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/token" \ -H "content-type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "scope": "public.site:read" }' ``` Response: ```json theme={null} { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 7200, "scope": "public.site:read", "created_at": 1710000000 } ``` Use the `access_token` in subsequent requests. It expires after 2 hours (`expires_in: 7200`) — request a new one before it does. ## Option C: OAuth 2.0 — Authorization Code Use this when building a partner integration where an operator's staff member authorises your app to access their data. ### Step 1: Redirect the user to authorise Direct the user's browser to: ``` https://app.stora.co/oauth2/authorize? client_id=YOUR_CLIENT_ID& redirect_uri=https://yourapp.com/callback& response_type=code& scope=public.contact:read public.order:read ``` The user logs in to Stora and approves the requested scopes. Stora redirects back to your `redirect_uri` with an authorisation code: ``` https://yourapp.com/callback?code=AUTHORIZATION_CODE ``` ### Step 2: Exchange the code for tokens ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/token" \ -H "content-type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://yourapp.com/callback" ``` Response: ```json theme={null} { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 7200, "scope": "public.contact:read public.order:read", "created_at": 1710000000, "refresh_token": "REFRESH_TOKEN" } ``` ### Refreshing tokens Access tokens expire after 2 hours. Use the refresh token to get a new one without requiring the user to re-authorise: ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/token" \ -H "content-type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "refresh_token=REFRESH_TOKEN" ``` The response includes a new `access_token` and a new `refresh_token`. The previous refresh token is revoked. ### PKCE (optional) [PKCE](https://oauth.net/2/pkce/) (Proof Key for Code Exchange) protects the Authorization Code flow against code interception. Use it when your OAuth client is a single-user application running on a user-owned device — a mobile app, a desktop app, or a CLI tool. If you're distributing your integration as code that runs outside infrastructure you control (WordPress plugin, browser extension, client-side app extension, packaged on-premise tool, etc.), PKCE alone is not sufficient. Follow [Distributing a public plugin](/2025-09/guides/distributing-public-plugins) instead — the broker pattern it describes is the safe way to keep OAuth credentials and tokens out of distributed code. Generate a `code_verifier` and `code_challenge` before redirecting: ```bash theme={null} CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=/+\n' | head -c 128) CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '/+' '_-') ``` Add the challenge to the authorisation URL: ``` https://app.stora.co/oauth2/authorize? client_id=YOUR_CLIENT_ID& redirect_uri=https://yourapp.com/callback& response_type=code& scope=public.contact:read public.order:read& code_challenge=CODE_CHALLENGE& code_challenge_method=S256 ``` Include the `code_verifier` when exchanging the code: ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/token" \ -H "content-type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://yourapp.com/callback" \ -d "code_verifier=CODE_VERIFIER" ``` # Authorization Source: https://docs.stora.co/2025-09/guides/authorization Authorization scopes for the Stora Public API. Each endpoint, except those related to OAuth 2, requires at least one scope for authorization. Below is the full list of available scopes. | Scope | Name | Description | | -------------------------------------------- | ------------------------------------- | ---------------------------------------- | | `public.access_token:read` | Access Token (Read) | Allow to read all access tokens | | `public.application:read` | Application (Read) | Allow to read all applications | | `public.contact:read` | Contact (Read) | Allow to read all contacts | | `public.contact:write` | Contact (Write) | Allow to manage contacts | | `public.contract:read` | Contract (Read) | Allow to read all contracts | | `public.contract_template:read` | Contract Template (Read) | Allow to read all contract templates | | `public.coupon:read` | Coupon (Read) | Allow to read all coupons | | `public.credit_note:read` | Credit Note (Read) | Allow to read all credit notes | | `public.deal:read` | Deal (Read) | Allow to read all deals | | `public.deal:write` | Deal (Write) | Allow to manage deals | | `public.deal_stage:read` | Deal Stage (Read) | Allow to read all deal stages | | `public.identity_verification:read` | Identity Verification (Read) | Allow to read all identity verifications | | `public.invoice:read` | Invoice (Read) | Allow to read all invoices | | `public.note:read` | Note (Read) | Allow to read all notes | | `public.note:write` | Note (Write) | Allow to manage notes | | `public.opportunity:read` | Opportunity (Read) | Allow to read all opportunities | | `public.order:read` | Order (Read) | Allow to read all orders | | `public.order:write` | Order (Write) | Allow to manage orders | | `public.price_adjustment:read` | Price Adjustment (Read) | Allow to read all price adjustments | | `public.price_adjustment:skip_notifications` | Price Adjustment (Skip notifications) | Price Adjustment (Skip notifications) | | `public.price_adjustment:write` | Price Adjustment (Write) | Allow to manage price adjustments | | `public.product:read` | Product (Read) | Allow to read all products | | `public.product:write` | Product (Write) | Allow to manage products | | `public.product_category:read` | Product Category (Read) | Allow to read all product categories | | `public.protection_level:read` | Protection Level (Read) | Allow to read all protection levels | | `public.protection_level:write` | Protection Level (Write) | Allow to manage protection levels | | `public.site:read` | Site (Read) | Allow to read all sites | | `public.staff:read` | Staff (Read) | Allow to read all staff | | `public.subscription:read` | Subscription (Read) | Allow to read all subscriptions | | `public.task:read` | Task (Read) | Allow to read all tasks | | `public.task:write` | Task (Write) | Allow to manage tasks | | `public.tenancy:read` | Tenancy (Read) | Allow to read all tenancies | | `public.timeline_event:read` | Timeline Event (Read) | Allow to read all timeline events | | `public.timeline_event:write` | Timeline Event (Write) | Allow to manage timeline events | | `public.timeline_source:read` | Timeline Source (Read) | Allow to read all timeline sources | | `public.timeline_template:read` | Timeline Template (Read) | Allow to read all timeline templates | | `public.unit:read` | Unit (Read) | Allow to read all units | | `public.unit:write` | Unit (Write) | Allow to manage units | | `public.unit_allocation:read` | Unit Allocation (Read) | Allow to read all unit allocations | | `public.unit_type:read` | Unit Type (Read) | Allow to read all unit types | | `public.unit_type:write` | Unit Type (Write) | Allow to manage unit types | | `public.webhook_endpoint:read` | Webhook Endpoint (Read) | Allow to read all webhook endpoints | | `public.webhook_endpoint:write` | Webhook Endpoint (Write) | Allow to manage webhook endpoints | # Building a booking flow Source: https://docs.stora.co/2025-09/guides/booking-flow Create a custom booking experience using the Stora API — from browsing availability to completing an order. The Stora API lets you build a custom browsing and checkout experience for a self-storage operator. Your application handles everything from displaying available units to assembling an order. When the customer is ready to pay, you redirect them to a hosted checkout page where their payment details are collected. After payment, Stora handles customer onboarding and creates the tenancy, subscription, and invoices automatically. Your application is notified via [webhooks](/2025-09/guides/webhooks). ## Before you start This guide assumes you have: * An access token or OAuth 2.0 credentials — see [Authentication](/2025-09/guides/authentication) * Familiarity with Stora's domain model — see [Core concepts](/2025-09/guides/core-concepts) * A webhook endpoint configured to receive events — see [Webhooks](/2025-09/guides/webhooks) Your token needs the following [scopes](/2025-09/guides/authorization): | Scope | Used for | | ------------------------------ | ------------------------------------------ | | `public.site:read` | Browsing sites | | `public.unit_type:read` | Browsing unit types and pricing | | `public.unit:read` | Checking unit availability | | `public.contact:write` | Creating customers | | `public.order:write` | Creating and finalizing orders | | `public.order:read` | Reading order status | | `public.coupon:read` | Looking up coupons (if applicable) | | `public.protection_level:read` | Listing protection options (if applicable) | | `public.product:read` | Listing add-on products (if applicable) | ## How it works Your application controls the experience up to payment. After that, Stora takes over. Choose the order creation flow that matches your checkout. ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4'}}}%% sequenceDiagram participant App as Your application participant API as Stora API participant Pay as Hosted checkout participant Portal as Customer portal App->>API: Browse sites and unit types API-->>App: Available storage and pricing App->>API: Create contact API-->>App: Contact App->>API: Create order (draft) API-->>App: Draft order App->>API: Validate order API-->>App: Validation result App->>API: Finalize order API-->>App: Finalized order with payment URL App->>Pay: Redirect customer Pay->>Portal: After payment, customer sets up account API-->>App: Webhooks (order.completed, tenancy.created) ``` ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4'}}}%% sequenceDiagram participant App as Your application participant API as Stora API participant Pay as Hosted checkout participant Portal as Customer portal App->>API: Browse sites and unit types API-->>App: Available storage and pricing App->>API: Create order with contact and finalize API-->>App: Finalized order with payment URL App->>Pay: Redirect customer Pay->>Portal: After payment, customer sets up account API-->>App: Webhooks (order.completed, tenancy.created) ``` After payment, the customer is directed to set up their account and access the operator's customer portal — a white-label experience managed by Stora on the operator's behalf. From the portal, customers can manage payment methods, view allocated units and subscriptions, sign contracts (if required), and complete identity verification (if enabled). This requires no integration work on your part. There are two ways to create an order: build it up incrementally as a draft (useful for multi-step checkouts), or [create and finalize in a single request](#create-and-finalize-in-one-step) (simpler for single-page checkouts). This guide covers the multi-step approach first. ## Step 1: Display available storage Start by fetching the operator's sites, then the unit types and pricing at each site. If you're serving this data to many customers, consider [caching these resources locally](/2025-09/guides/data-synchronisation) rather than fetching them on every page load. ### Fetch sites ```bash theme={null} curl -X GET https://public-api.stora.co/2025-09/sites \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```json theme={null} { "sites": [ { "id": "site_14b419f1096013f1", "name": "Manchester City Centre", "description": "24/7 access, CCTV monitored.", "phone": "0161 123 4567", "address": { "line_1": "42 Deansgate", "city": "Manchester", "postal_code": "M3 2EG" }, "access_hours": { "monday": { "status": "set_hours", "open": "06:00", "close": "22:00" }, "tuesday": { "status": "set_hours", "open": "06:00", "close": "22:00" } } } ] } ``` If the operator has multiple sites, use `address`, `access_hours`, and `description` to build a site selection UI. ### Fetch unit types at a site Unit types represent the categories of storage available — for example "50 sq ft indoor" or "20 ft container." ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/unit_types?site_id=site_14b419f1096013f1" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```json theme={null} { "unit_types": [ { "id": "utype_3b3aed5cca33b11d", "name": "Medium unit", "status": "bookable", "size_description": "10x12 ft", "dimensions": { "width": 10.0, "length": 12.0, "height": 11.0, "measurement_unit": "ft" }, "selling_points": ["Drive-up access", "Ground floor"], "live_prices": [ { "billing_period": "monthly", "price": { "amount": 9000, "currency": "GBP", "formatted": "£90.00" } } ], "prices": [ { "billing_period": "monthly", "price": { "amount": 9500, "currency": "GBP", "formatted": "£95.00" } } ], "require_insurance_coverage": true, "require_security_deposit": true, "security_deposit": { "amount": 5000, "currency": "GBP", "formatted": "£50.00" }, "site": { "id": "site_14b419f1096013f1" } } ] } ``` Key fields: * **`live_prices`** — the current Unit Type prices to show customers and use for `unit_type` order line items. Each entry is scoped to a `billing_period`. Use the matching entry for the customer's selected billing period. * **`prices`** — the base configured prices for the Unit Type. These are useful for reference, but your booking flow should use `live_prices` when displaying or submitting Unit Type pricing. * **`status`** — should be `bookable` for unit types you display. * **`require_insurance_coverage`** / **`require_security_deposit`** — indicate what the operator expects. The API doesn't enforce these, but your checkout should prompt for them when `true`. See [operator expectations](#operator-expectations). * **`selling_points`** — operator-defined features you can display in your UI. Do not calculate or guess Unit Type prices yourself. Use the matching `live_prices` entry for the selected billing period. If no matching `live_prices` entry exists, refresh the Unit Type and ask the customer to choose an available billing period before creating the order. ### Check availability Check whether a unit type has available stock by querying its units filtered by status: ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/units?unit_type_id=utype_3b3aed5cca33b11d&status=available&limit=1" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` If the response contains units, that unit type is available. You don't need to select a specific unit — Stora handles unit allocation when the order completes. Customers book Unit Types, not individual units. Use Unit Types for customer-facing browsing, pricing, and order creation. Every order must include at least one `unit_type` line item. Specific units are allocated or reserved later through Stora's completion and allocation flow. ### Fetch protection levels and products If your checkout lets customers add protection or products, fetch the available options: ```bash theme={null} # Protection levels (goods coverage tiers) curl -X GET https://public-api.stora.co/2025-09/protection_levels \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" # Products (add-on items and services, filtered by site) curl -X GET "https://public-api.stora.co/2025-09/products?site_id=site_14b419f1096013f1" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Protection levels return a `cover_level` (the coverage amount) and `prices` per billing period. Products return a `charge_type` (`one_time` or `recurring`) and `prices`. Both are referenced by ID when adding line items to an order. ## Step 2: Capture customer details Every order needs a contact — the person renting the storage. | Approach | Best for | | --------------------------------------------------------------- | ------------------------------------------------------------- | | **Create upfront** — `POST /contacts` before creating the order | Multi-step checkouts where you want to capture the lead early | | **Create inline** — embed contact fields in the order request | Single-page checkouts where everything is submitted at once | ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/contacts \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "jane.smith@example.com", "full_name": "Jane Smith", "phone_number": "+447700900123", "type": "domestic", "source": "booking", "use_case": "moving_home", "address": { "line_1": "15 Park Road", "city": "Manchester", "postal_code": "M14 5RQ", "country_alpha2": "GB" } }' ``` Only `email` is required. All other fields are optional but recommended — the operator will see this information in their back office. Email addresses must be unique per operator. If a contact with the same email already exists, the request will fail with a validation error. Use `GET /contacts?email=` to check for an existing contact first, and reference it by `id` on the order if found. ## Step 3: Create the order Create an order in `draft` status with at least one `unit_type` line item: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/orders \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: booking-jane-smith-2026-03-30" \ -d '{ "site": { "id": "site_14b419f1096013f1" }, "contact": { "id": "con_0ac0514ed0711462" }, "billing_period": "monthly", "payment_method": "card", "starts_at": "2026-04-15T00:00:00Z", "coupon": { "id": "cpn_9f4a1d7b2c8e3456" }, "email_notifications": { "payment_details": true }, "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9000 }, "item": { "id": "utype_3b3aed5cca33b11d" } } ] }' ``` ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/orders \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: booking-jane-smith-2026-03-30" \ -d '{ "site": { "id": "site_14b419f1096013f1" }, "contact": { "email": "jane.smith@example.com", "full_name": "Jane Smith", "phone_number": "+447700900123" }, "billing_period": "monthly", "payment_method": "card", "starts_at": "2026-04-15T00:00:00Z", "coupon": { "id": "cpn_9f4a1d7b2c8e3456" }, "email_notifications": { "payment_details": true }, "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9000 }, "item": { "id": "utype_3b3aed5cca33b11d" } } ] }' ``` The response includes the order with `status: "draft"` and calculated totals you can use to build an [order summary](#order-summary-fields) for the customer. Key request fields: * **`site.id`** (required) — the site the customer is booking at. * **`contact`** — either an `id` referencing an existing contact, or inline contact fields. * **`billing_period`** — `weekly`, `monthly`, `every_four_weeks`, `every_three_months`, `every_six_months`, or `yearly`. * **`payment_method`** — `card`, `bacs_debit`, or `sepa_debit`. * **`starts_at`** — when the tenancy begins. ISO 8601 date-time or `"now"` for immediate move-in. * **`coupon.id`** — optional coupon to apply to the order. For automation or AI-created orders, use the selected `unit_type.promotion` when present. * **`email_notifications.payment_details`** — set to `true` to email the customer a payment link when the order is finalized. * **`line_items`** — at least one `unit_type` [line item](#line-items) is required. Use `?expand=line_items` to include full line item objects in the response instead of just IDs. You can also expand `contact` and `site`. ### Add more line items While the order is in `draft` status, you can add protection, products, and security deposits: ```bash theme={null} # Add protection curl -X POST https://public-api.stora.co/2025-09/orders/ord_9ef07151f2470754/line_items \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "protection", "quantity": 1, "price": { "amount": 999 }, "item": { "id": "plvl_e8f3ad0a07e47566" } }' ``` ```bash theme={null} # Add a security deposit curl -X POST https://public-api.stora.co/2025-09/orders/ord_9ef07151f2470754/line_items \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "security_deposit", "quantity": 1, "price": { "amount": 5000 }, "item": { "id": "utype_3b3aed5cca33b11d" } }' ``` You can also apply a [coupon](#coupons), configure [email notifications](#email-notifications), and attach [metadata](#metadata). ## Step 4: Finalize the order Finalizing locks the order and generates a hosted checkout page. Optionally validate first to catch any issues: ```bash theme={null} curl -X GET https://public-api.stora.co/2025-09/orders/ord_9ef07151f2470754/validate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Then finalize: ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/orders/ord_9ef07151f2470754/finalize \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "finalize": { "cancel_redirect_url": "https://your-app.com/booking/cancelled" } }' ``` The `finalize.cancel_redirect_url` request field is required and controls where the customer lands if they abandon checkout. Use `"cancel_redirect_url": "default"` if you do not have your own URL to redirect to. The finalized order response includes a `payment_url`. See [Handling payment](#handling-payment) for when to redirect the customer or send the link by email. Once finalized, the order is locked. If the customer needs to make changes, create a new order. ### Handling payment Finalizing an order generates a hosted `payment_url`. In an interactive booking flow, redirect the customer to this URL so they can enter their payment details. If the order is created by automation, AI, or another flow where you cannot redirect the customer immediately, set `email_notifications.payment_details` to `true`. Stora emails the customer a payment link when the order is finalized. Use one payment path per order: redirect the customer to `payment_url`, send the payment details email, or do both only when that is intentional for your customer experience. ### Create and finalize in one step For single-page checkouts, include the `finalize` field in the create request. You can reference an existing contact or create the contact inline in the same request. ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/orders \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: booking-jane-smith-2026-03-30" \ -d '{ "site": { "id": "site_14b419f1096013f1" }, "contact": { "id": "con_0ac0514ed0711462" }, "billing_period": "monthly", "payment_method": "card", "starts_at": "2026-04-15T00:00:00Z", "coupon": { "id": "cpn_9f4a1d7b2c8e3456" }, "email_notifications": { "payment_details": true }, "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9000 }, "item": { "id": "utype_3b3aed5cca33b11d" } }, { "type": "protection", "quantity": 1, "price": { "amount": 999 }, "item": { "id": "plvl_e8f3ad0a07e47566" } } ], "finalize": { "cancel_redirect_url": "https://your-app.com/booking/cancelled" } }' ``` ```bash theme={null} curl -X POST https://public-api.stora.co/2025-09/orders \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: booking-jane-smith-2026-03-30" \ -d '{ "site": { "id": "site_14b419f1096013f1" }, "contact": { "email": "jane.smith@example.com", "full_name": "Jane Smith", "phone_number": "+447700900123", "type": "domestic", "source": "booking", "use_case": "moving_home", "address": { "line_1": "15 Park Road", "city": "Manchester", "postal_code": "M14 5RQ", "country_alpha2": "GB" } }, "billing_period": "monthly", "payment_method": "card", "starts_at": "2026-04-15T00:00:00Z", "coupon": { "id": "cpn_9f4a1d7b2c8e3456" }, "email_notifications": { "payment_details": true }, "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9000 }, "item": { "id": "utype_3b3aed5cca33b11d" } }, { "type": "protection", "quantity": 1, "price": { "amount": 999 }, "item": { "id": "plvl_e8f3ad0a07e47566" } } ], "finalize": { "cancel_redirect_url": "https://your-app.com/booking/cancelled" } }' ``` Using an inline contact here creates the contact as part of order creation. The response is a finalized order with a `payment_url`. If finalization fails, the order is not stored. ## Step 5: Handle completion After payment, Stora emits `order.completed` to confirm that the booking is complete. It then creates the tenancy and subscription and attempts auto-reservation when the operator has enabled it. Everything is communicated via webhooks. ### What Stora creates The storage agreement — linking the contact, site, and unit type with start and end dates. The billing agreement — recurring charges, billing period, and payment method. See [invoicing](#invoicing) for when each charge type is billed. For storefront and Public API orders, Stora attempts to reserve the requested units automatically when the operator has enabled auto-reservation. Use the dedicated `tenancy.auto_reservation.*` events to determine the outcome. For partial or failed attempts, use the [reserve endpoint](/2025-09/api-reference/units/reserve-a-unit) to allocate unreserved units manually. If a contract template was specified on the order, Stora generates a contract for the customer to sign. ### Webhook events | Event | When it fires | | ---------------------------------------------- | -------------------------------------------------------------- | | `order.created` | The order is created | | `order.finalized` | The order is finalized and the payment link is generated | | `order.completed` | Payment is complete and the booking is ready for processing | | `tenancy.created` | A tenancy is created for the completed order | | `tenancy.auto_reservation.succeeded` | All requested units were reserved automatically | | `tenancy.auto_reservation.partially_succeeded` | Some, but not all, requested units were reserved automatically | | `tenancy.auto_reservation.failed` | No requested units were reserved automatically | | `subscription.created` | A subscription is created for the completed order | | `unit.reserved` | An individual unit has been reserved | `order.completed` is the primary signal that the booking is done. Stora triggers it before tenancy creation and auto-reservation processing, but webhook delivery order is not guaranteed. It may arrive after later tenancy or auto-reservation events. Use the `tenancy.auto_reservation.*` events to determine the complete reservation outcome. Each successfully reserved unit also emits `unit.reserved`. For partial or failed attempts, allocate the unreserved units manually. Always use webhooks to detect completion — not redirects. The customer is redirected to Stora's account setup flow after payment, not back to your application. See [Webhooks](/2025-09/guides/webhooks) for setup, payload structure, and signature verification. ## Understanding orders in detail ### Line items There are four line item types: | Type | Description | | ------------------ | ------------------------------------------------------------------------------------ | | `unit_type` | The storage unit rental. References a unit type ID. At least one required per order. | | `protection` | Goods protection / insurance coverage. References a protection level ID. | | `product` | An add-on product or service (e.g. padlock, admin fee). References a product ID. | | `security_deposit` | A one-off refundable deposit. References the unit type ID. | Every line item needs a `type`, `quantity`, `price.amount`, and `item.id`. For `unit_type` line items, set `price.amount` from the matching `unit_type.live_prices` entry for the selected billing period. For `protection` and `product` line items, use the relevant resource's `prices` array. The API requires at least one `unit_type` line item but does not enforce protection or security deposit inclusion — your application controls the checkout experience. ### Operator expectations The unit type's `require_insurance_coverage` and `require_security_deposit` flags indicate what the operator expects. When `true`, your checkout should prompt the customer accordingly. The API won't reject an order missing these. ### Tax Tax is calculated automatically based on the operator's configuration at the site level — you don't set it yourself. Each line item type can have its own tax rate. Security deposits are never taxed. Each line item returns `tax` and `total_excluding_tax` fields, and the order has aggregate tax fields across all line items. ### Promotions Unit types can include a `promotion` when a promotional coupon is associated with them. Expand or inspect the selected Unit Type before creating the order so you can apply that promotion consistently. When an order is created by automation or AI, use `unit_type.promotion` as the order `coupon` when it is present: ```json theme={null} { "coupon": { "id": "cpn_9f4a1d7b2c8e3456" } } ``` This keeps automated bookings aligned with the promotion **the customer would see in a normal booking flow**. If you also let customers enter coupon codes manually, decide whether the entered code or `unit_type.promotion` takes precedence before creating the order. ### Coupons You can apply one coupon per order by including the `coupon` field: ```json theme={null} { "coupon": { "id": "cpn_a26d0d4c582740c1" } } ``` To let customers enter a coupon code, fetch available coupons with `GET /coupons` and match by the `code` field. Coupons fall into two categories based on their `auto_apply_to` configuration: * **Subscription-level** (`auto_apply_to.subscriptions: true`) — apply to the subscription as a whole. * **Line-item level** — apply individually to matching line items. The `auto_apply_to` object controls which types: `unit_types`, `protections`, and/or `products`. Stora applies the coupon to the relevant line items automatically based on this configuration. The order's `total_discount` field reflects the result. * Only one coupon per order. * Fixed-amount coupons can only apply at the subscription level. * One-off products and security deposits are never discounted. ### Invoicing The hosted checkout page collects the customer's payment method — not an immediate payment. After checkout, charges are split across separate invoices: | Line item type | When invoiced | Invoice type | | --------------------- | ---------------------- | ------------------------------------- | | `unit_type` | First billing cycle | Subscription (recurring) | | `protection` | First billing cycle | Subscription (recurring) | | `product` (recurring) | First billing cycle | Subscription (recurring) | | `product` (one-time) | Shortly after checkout | Separate invoice, charged immediately | | `security_deposit` | Shortly after checkout | Separate invoice, charged immediately | ### Order summary fields Every order response includes calculated totals that update as line items change: | Field | What it represents | | ---------------- | -------------------------------------------------------------------- | | `subtotal` | Recurring charges before tax | | `tax` | Total tax across all line items | | `total` | Recurring charges including tax and discounts | | `total_discount` | Total coupon discount applied | | `one_time_total` | One-off charges (security deposits, one-time products) including tax | Each line item also returns `price`, `tax`, `total`, `total_excluding_tax`, and `discount_total` for per-item breakdowns. Use the `formatted` field on any money object for display-ready strings. ### Email notifications Stora sends the customer a booking confirmation and move-in day reminder by default. The content is customised by the operator in the BackOffice. Use `email_notifications` to control these emails per order. Set `payment_details` to `true` when Stora should email the customer a payment link after the order is finalized: ```json theme={null} { "email_notifications": { "booking_confirmation": false, "move_in_day": false, "payment_details": true } } ``` ### Metadata Attach up to 20 key-value pairs for your own references: ```json theme={null} { "metadata": { "external_booking_id": "BK-20260330-001", "channel": "website" } } ``` See [Metadata](/2025-09/guides/metadata) for details. ## Edge cases ### Availability races Stora does not hold units during checkout. If a unit type sells out between browsing and payment, the order still completes but no unit is allocated. The operator handles this from the back office. Refresh availability data at your order summary page to reduce the risk. ### Abandoned orders Orders that are finalized but never completed stay in `finalized` status. Track `order.finalized` events and flag orders that don't receive `order.completed` within a reasonable timeframe. ### Validation errors Common issues: * **"Line items must include at least one unit type line item"** — every order needs at least one storage unit. * **"Billing period must exist"** — ensure the billing period matches one available on the unit type. ### Idempotency Use the `Idempotency-Key` header on `POST` requests to safely retry on network errors. See [Idempotent requests](/2025-09/guides/requests#idempotent-requests). # Changelog Source: https://docs.stora.co/2025-09/guides/changelog Version history for the Stora Public API. **Added** * **Opportunities API**: list and show endpoints **Added** * **Price Adjustments API**: read endpoints; create endpoint; destroy endpoint; schedule endpoint **Added** * **Protection Levels API**: `site` expandable association **Preparation for protection levels moving from the account to each site**; filter by `site_id`; expandable responses enabled for read endpoints **Changed** * **Deals API**: a `protection` line item must reference an account-level protection level, or one belonging to the deal's site **Preparation for protection levels moving from the account to each site** **Added** * **Invoices API**: `due` time-range filters (`due_after`, `due_before`); `due_date` sorting on the invoices list endpoint **Added** * **Invoices API**: `invoice.overdue` webhook event, fired when an invoice becomes overdue for payment; `collection_status` and `due_date` fields on the Invoice resource; `collection_status` filter on the invoices list endpoint **Added** * **Credit Notes API**: `accounting_code` field on line items, also included in the `credit_note.created` and `credit_note.updated` webhook payloads **Added** * **Tenancies API**: `tenancy.auto_reservation.succeeded`, `tenancy.auto_reservation.partially_succeeded`, and `tenancy.auto_reservation.failed` webhook events **Added** * **Contract Templates API**: `updated` and `created` time-range filters * **Contracts API**: `updated` time-range filters * **Coupons API**: `updated` and `created` time-range filters * **Credit Notes API**: `updated` time-range filters * **Deal Stages API**: `updated` and `created` time-range filters * **Invoices API**: `updated` time-range filters * **Notes API**: `updated` time-range filters * **Orders Line Items API**: `updated` and `created` time-range filters * **Products API**: `updated` time-range filters * **Protection Levels API**: `updated` and `created` time-range filters * **Sites API**: `updated` and `created` time-range filters * **Subscriptions API**: `updated` and `created` time-range filters * **Tasks API**: `updated` time-range filters * **Tenancies API**: `updated` and `created` time-range filters * **Timeline Sources API**: `updated` time-range filters * **Timeline Templates API**: `updated` time-range filters * **Unit Allocations API**: `updated` and `created` time-range filters * **Unit Types API**: `updated` and `created` time-range filters * **Units API**: `updated` time-range filters * **Webhook Endpoints API**: `updated` and `created` time-range filters **Changed** * **Orders API**: payment URLs and the new payment details email are now generally available. **Added** * **Orders API**: **beta** support for the new payment details email; abandon endpoint and `order.abandoned` webhook event **Fixed** * **Orders API**: `emails` is marked as deprecated in favor of the `email_notifications` field in the [Create](https://docs.stora.co/2025-09/api-reference/orders/create-an-order) and [Update](https://docs.stora.co/2025-09/api-reference/orders/update-an-order) requests **Added** * **Unit Types API**: `live_prices` field **Added** * **Orders API**: **beta** support for new payments URLs **Added** * **Subscriptions API**: `subscription.resumed` webhook event **Fixed** * **Subscriptions API**: `subscription.cancelled` webhook event emits for every subsequent cancellation after the first **Added** * **Products API**: images **Fixed** * **Tenancies API**: webhook response payloads will be presented with `tenancy` data root **Added** * **Deals API**: `deal.stage_changed` webhook event **Added** * **Contacts API**: add `status` field and filter by `status` **Added** * **Deals API**: create, update, and destroy endpoints **Added** * **Orders API**: filter by `source` and `billing_period`, plus `updated_at` and `starts_at` time-range filters **Added** * **Contacts API**: filter by `use_case`, `type`, `source` `marketing_source` **Added** * **Contacts API**: expose `identity_check` **Added** * **Contracts API**: filter by `contact_id`, `contract_template_id` and `tenancy_id` query params **Added** * **Identity Verifications API**: webhook events **Added** * **Identity Verifications API**: read endpoints * **Unit Types API**: `promotion` expandable association **Added** * **Deals API**: index filters **Added** * **Unit Types API**: filter by `site_id` query parameter **Added** * **Contracts API**: link to sign the contract on Storefront * **Unit Types API**: link to create a new order on Storefront **Added** * **Images API**: expose sites and unit types images **Added** * **Deals API**: add `lose` endpoint; add `reopen` endpoint **Fixed** * **Invoices API**: response payload will show accounting nominal code **Added** * **Invoices API**: add `subscription` and `tenancy` expandable objects and filters to invoice resource **Added** * **Deals API**: add `win` endpoint **Added** * **Contacts API**: add `metadata` to contact resource * **Orders API**: add `metadata` to order resource * **Tasks API**: add `metadata` to task resource **Added** * **Notes API**: add `metadata` to note resource * **Timeline Events API**: create, update, and destroy endpoints * **Timeline Sources API**: read endpoints * **Timeline Templates API**: read endpoints **Added** * **Contracts API**: `contract.created` webhook event; `contract.signed` webhook event * **Deals API**: `deal.won` webhook event; `deal.reopened` webhook event; `deal.lost` webhook event **Added** * **Contracts API**: filter by site ID and status **Fixed** * **Contracts API**: replace `subscription` with `tenancy` in contract response **Added** * **Contracts API**: show endpoint; list endpoint * **Notes API**: filter by resource\_type * **Webhook Endpoints API**: add `metadata` to webhook endpoint resource **Added** * **Credit Notes API**: sorting * **Deals API**: `deal.created` webhook event; `deal.updated` webhook event * **Units API**: search filter on the list endpoint **Added** * **Notes API**: `note.created` webhook event; `note.updated` webhook event; create endpoint * **Sites API**: search filter on the list endpoint * **Subscriptions API**: filter by contact ID **Fixed** * **OpenAPI**: add missing pagination params to Staff API **Added** * **Contacts API**: search filter on the list endpoint * **Notes API**: update endpoint; destroy endpoint; show endpoint * **Units API**: overlock endpoint; remove overlock endpoint **Fixed** * **OpenAPI**: fixes (consistency & references) **Added** * **Deal Stages API**: list and show endpoints * **Deals API**: list and show endpoints * **Notes API**: list endpoint **Added** * **Staff API**: search filter by name or email * **Units API**: add `unit_allocation` expandable object to unit resource **Added** * **Units API**: grant access endpoint; deallocate endpoint **Added** * **Tasks API**: index filters and sorting **Added** * **Units API**: reserve endpoint **Added** * **Tasks API**: `task.reopened` webhook event; `task.updated` webhook event * **Webhook Endpoints API**: endpoints scoped and managed by OAuth apps **Added** * **Webhook Endpoints API**: add `creator` to webhook endpoint resource **Added** * **Tasks API**: `task.completed` webhook event; `task.created` webhook event **Added** * **Staff API**: show endpoint **Added** * **Staff API**: list endpoint **Added** * **Tasks API**: open endpoint **Added** * **Tasks API**: complete endpoint; delete endpoint; update endpoint * **Unit Allocations API**: list endpoint **Added** * **OAuth 2**: Authorization Code flow support * **Unit Allocations API**: show endpoint **Added** * **Contacts API**: filter by email **Added** * **Tasks API**: create endpoint **Added** * **Tasks API**: show endpoint; list endpoint **Added** * **Webhook Endpoints API**: index filters Official release of the `2025-09` version. **Added** * **Contacts API**: initial * **Contract Templates API**: initial * **Coupons API**: initial * **Credit Notes API**: PDF support; filters; initial * **Filters**: Time ranged filters * **Invoices API**: PDF support; filters; initial * **Orders API**: orders summary; support for starts at now; update action; filters; initial version of the create action; add `billing_period` to the order; initial version of index and show actions * **Orders Line Items API**: delete endpoint; update endpoint; create endpoint; list endpoint * **Product Categories API**: initial * **Products API**: filters; initial * **Protection Levels API**: initial * **Sites API**: initial * **Subscriptions API**: initial * **Tenancies API**: initial * **Unit Types API**: Base Price; initial * **Units API**: filters; initial * Add the `Money` component * Expandable responses * Initial resource sorting * Preparation for expandable resources in responses * Webhooks Core # Core concepts Source: https://docs.stora.co/2025-09/guides/core-concepts Understand Stora's domain model, key resources, how they relate, and the lifecycles that drive the system. Before diving into specific endpoints, it helps to understand how Stora's domain model fits together. This guide covers the key resources, how they relate, and the lifecycles that drive the system. ## The big picture Stora models a self-storage business. At the top level, an operator runs one or more **sites** (physical locations). Each site has **unit types** (storage categories) containing individual **units** (bookable spaces). **Contacts** rent units through **orders**, which create **tenancies** (the storage agreement) and **subscriptions** (the billing agreement). ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4', 'edgeLabelBackground': '#FF7237'}}}%% graph TD Site --> UT["Unit Type"] --> Unit Contact -->|"  creates  "| Order Order -->|"  completes  "| Tenancy Order -->|"  completes  "| Subscription Tenancy --> UA["Unit Allocations"] Subscription --> Invoices ``` ## Sites, unit types, and units These three resources represent the physical world of a storage facility. ### Site A single physical self-storage location. It's the container for everything at that location: units, pricing, access control, and reporting. A site has an address, access hours, and contact details. ### Unit type A standardised storage offering at a site — for example "50 sq ft indoor" or "20 ft container." Unit types define the size, features, and pricing that apply to all units of that type. ### Unit A specific, bookable storage space — for example "Unit A-012" of a given unit type. Each unit tracks its own status as it moves through the rental lifecycle. **Unit states:** | State | Meaning | | ------------- | ------------------------------------------------ | | `available` | Ready to be rented | | `reserved` | Allocated to a tenancy that hasn't started yet | | `occupied` | Allocated and the tenancy is active | | `overlocked` | Access restricted, typically due to non-payment | | `repossessed` | Contents repossessed after prolonged non-payment | | `unavailable` | Taken offline by staff (maintenance, etc.) | The typical happy path is: `available` → `reserved` → `occupied` → `available` (when the tenant moves out). Overlocking and repossession are exception paths for non-payment. ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'flowchart': {'defaultRenderer': 'elk'}, 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4'}}}%% graph LR available --> reserved --> occupied --> available occupied --> overlocked occupied --> repossessed unavailable <--> available ``` ## Contacts A contact is the end user of a storage business — an individual or company that inquires, books, and pays for storage. Contacts exist at the operator level (not per-site), so the same person can rent at multiple locations. Contacts support [metadata](/2025-09/guides/metadata) for attaching your own external identifiers. ## The rental lifecycle: orders, tenancies, and subscriptions This is the most important relationship in the API. Three resources represent different views of a rental: ### Order An order captures a contact's intent to rent storage. It includes the selected site, unit type, move-in date, pricing, and optional add-ons (protection, products, services). **Order states:** | State | Meaning | | ----------- | ---------------------------------------------------- | | `draft` | Being assembled — line items can be added or changed | | `finalized` | Locked in — validated and ready for completion | | `completed` | Done — a tenancy and subscription have been created | | `abandoned` | Cancelled before completion | An order contains **line items** — the individual charges that make up the rental (unit rent, protection, products, fees). ### Tenancy A tenancy represents the ongoing storage agreement between a contact and an operator. It answers the questions: **who** has **which unit**, at **which site**, **from when to when**? A tenancy is created when an order completes. It links to: * The **order** that created it * The **subscription** that bills for it * The **unit allocations** — which specific units are assigned * The **site** and **contact** ### Subscription A subscription is the billing side of the same rental. It represents the recurring payment agreement — billing period, prices, discounts, and taxes. Subscriptions generate **invoices** on each billing cycle. ### How they fit together When an order completes, Stora creates both a tenancy and a subscription. They represent the same rental from two different angles: * **Tenancy** = the physical/logistical view ("who is storing what, where") * **Subscription** = the financial view ("what are they paying, and when") ```mermaid actions={false} theme={null} %%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e8ebf4', 'primaryTextColor': '#05195A', 'primaryBorderColor': '#e8ebf4', 'lineColor': '#FF7237', 'secondaryColor': '#e8ebf4', 'tertiaryColor': '#e8ebf4', 'edgeLabelBackground': '#FF7237'}}}%% graph TD Order -->|"  the storage agreement  "| Tenancy Order -->|"  the billing agreement  "| Subscription Tenancy --> UA["Unit Allocations"] Subscription --> Invoices ``` You can navigate between them: an order references its tenancy and subscription. A tenancy references its order and subscription. A subscription references its tenancy and order. Use the `expand` parameter to include related resources inline. ## Billing: invoices and credit notes ### Invoices A subscription generates invoices on each billing cycle. Invoices are also created for one-off charges and security deposits. **Invoice states:** | State | Meaning | | --------------- | ----------------------------------- | | `draft` | Being prepared — not yet issued | | `open` | Issued — awaiting payment | | `paid` | Settled | | `uncollectible` | Payment failed and won't be retried | | `void` | Cancelled | Invoices are available in JSON, CSV, and PDF formats via the `Accept` header. ### Credit notes A credit note reduces or reverses part or all of a previously issued invoice. It references the original invoice and specifies the corrected amounts. ## Deals A deal is a pre-sale CRM record representing a potential order. Where an order captures firm intent with agreed pricing, a deal tracks the early-stage sales process — an inquiry that may or may not convert. **Deal states:** `open` → `won` or `lost` A deal links to a contact, and optionally a site and unit type. When a deal is won, it typically leads to an order being created. ## Supporting resources ### Contracts A contract is a document generated from a **contract template** for a specific contact and tenancy. It tracks the signing lifecycle: `pending` → `signed`, `voided`, `declined`, or `deleted`. ### Notes A text annotation attached to a resource — a contact, unit, subscription, or task. Notes capture observations, reminders, or context added by staff or integrations. ### Tasks A piece of work, optionally linked to a resource (a unit, subscription, or contact). Tasks are either `open` or `completed`, and can be assigned to staff. ### Products and protection levels **Products** are sellable items or services (e.g. padlocks, insurance, admin fees). **Protection levels** are optional add-ons that protect a contact's stored goods up to a chosen coverage amount. Both can appear as line items on an order. ### Coupons Reusable promotions that reduce the price a contact pays, by a percentage or fixed amount. Coupons can be scoped to specific charge types and limited by duration or number of uses. ## Unit allocations A unit allocation joins a specific unit to a tenancy. It tracks when the unit was reserved, when access was granted, and by whom. This is what drives the unit's status — allocating a unit moves it from `available` to `reserved` or `occupied`. For a full walkthrough of the allocation lifecycle, auto-reservation, access control sync, and the API endpoints for managing unit access, see [Allocations and access](/2025-09/guides/allocations-and-access). ## Resource IDs All resources use prefixed string IDs. The prefix tells you the resource type at a glance: | Prefix | Resource | | ------- | ---------------- | | `site_` | Site | | `ut_` | Unit Type | | `unit_` | Unit | | `con_` | Contact | | `ord_` | Order | | `ten_` | Tenancy | | `sub_` | Subscription | | `inv_` | Invoice | | `cn_` | Credit Note | | `deal_` | Deal | | `task_` | Task | | `we_` | Webhook Endpoint | ## Scopes Every API endpoint (except OAuth 2) requires at least one scope. Scopes follow the format: ``` public.: ``` For example: `public.contact:read`, `public.order:write`, `public.site:read`. When expanding related resources in a response, your token needs the read scope for each expanded resource. For example, expanding `site` on a subscription requires `public.site:read` in addition to `public.subscription:read`. # Data synchronisation Source: https://docs.stora.co/2025-09/guides/data-synchronisation Reduce API calls by fetching data once and using webhooks to keep your local copy current. If your integration serves data from Stora to end users — unit types on a booking page, prices on a comparison tool, coupon codes in a checkout — you'll want to avoid fetching everything from the API on every request. The API has [rate limits](/2025-09/guides/requests#rate-limiting) (10 requests per second, 60 per minute), and round trips add latency. The key idea is simple: **fetch once, then let webhooks tell you when something changes.** ## How often does data actually change? Not all resources change at the same rate. Understanding this helps you decide what's safe to cache and for how long. ### Rarely These typically only change when an operator reconfigures their offering. * [Sites](/2025-09/api-reference/sites/list-all-sites) * [Unit types](/2025-09/api-reference/unit-types/list-all-unit-types) * [Products](/2025-09/api-reference/products/list-all-products) * [Product categories](/2025-09/api-reference/product-categories/list-all-product-categories) * [Protection levels](/2025-09/api-reference/protection-levels/list-all-protection-levels) * [Contract templates](/2025-09/api-reference/contract-templates/list-all-contract-templates) * [Staff](/2025-09/api-reference/staff/list-all-staff) These are safe to cache for long periods — hours or even a full day — and refresh on a schedule or when you receive a relevant webhook. ### On business events These change when an operator takes a deliberate action, like updating pricing or creating a promotion. * [Coupons](/2025-09/api-reference/coupons/list-all-coupons) * Unit Type live prices on [unit types](/2025-09/api-reference/unit-types/list-all-unit-types), plus base prices on [products](/2025-09/api-reference/products/list-all-products) and [protection levels](/2025-09/api-reference/protection-levels/list-all-protection-levels) Price changes on unit types and protection levels trigger their respective `.updated` webhooks. Use these to invalidate cached pricing, including Unit Type `live_prices` used by booking flows. ### Frequently These change with customer and operational activity — bookings, payments, unit status changes. * [Units](/2025-09/api-reference/units/list-all-units) * [Contacts](/2025-09/api-reference/contacts/list-all-contacts) * [Orders](/2025-09/api-reference/orders/list-all-orders) * [Deals](/2025-09/api-reference/deals/list-all-deals) * [Tenancies](/2025-09/api-reference/tenancies/list-all-tenancies) * [Subscriptions](/2025-09/api-reference/subscriptions/list-all-subscriptions) * [Invoices](/2025-09/api-reference/invoices/list-all-invoices) * [Credit notes](/2025-09/api-reference/credit-notes/list-all-credit-notes) * [Contracts](/2025-09/api-reference/contracts/list-all-contracts) * [Tasks](/2025-09/api-reference/tasks/list-all-tasks) * [Notes](/2025-09/api-reference/notes/list-all-notes) * [Unit allocations](/2025-09/api-reference/unit-allocations/list-all-unit-allocations) Cache selectively based on your use case. If you're building a dashboard that shows unit occupancy across sites, caching unit status and keeping it fresh via webhooks makes sense. If you only read invoices when processing a payment, fetch them at that point instead. ## Using webhooks to stay in sync [Webhooks](/2025-09/guides/webhooks) are the primary tool for knowing when data has changed. Rather than polling the API on a timer, subscribe to the events that matter to your integration and react when they fire. The pattern: 1. **Fetch the data you need** on startup or first use 2. **[Subscribe to relevant webhook events](/2025-09/guides/webhooks#getting-started)** for those resources 3. **When an event arrives**, update your local copy using the data in the event payload — or re-fetch from the API if you need expanded relationships Webhook payloads include the full resource, so in most cases you can update your local data directly without an additional API call: ```json theme={null} { "event": { "type": "unit_type.updated", "data": { "unit_type": { "id": "ut_abc123", "name": "50 sq ft Indoor", "status": "bookable" } } } } ``` Most resources have webhook events, but not all — sites, products, product categories, contract templates, and staff don't have webhooks yet. For these, fetch on a schedule. Since they all change rarely, even polling once a day may be enough depending on your use case. For the full list of available events, see the [webhooks guide](/2025-09/guides/webhooks#available-events). # Developer Portal Source: https://docs.stora.co/2025-09/guides/developer-portal Create test operators, manage connected operator accounts, and build partner apps for the Stora API. Use the Developer Portal at [`developer.stora.co`](https://developer.stora.co) to set up and manage development access for the Stora API. The portal guides you through three workflows: * **Testing / looking around** — create a test operator and explore the API safely. * **Manage accounts** — connect a real operator account when you need to manage API access for that operator. * **Create partner app** — create an OAuth application for partner integrations and marketplace distribution. Choose the workflow that matches what you are building. You do not need to connect a real operator account just to explore the API. ## Testing / looking around Start here when you want to explore the API. Create a Developer Portal account, then create a test operator. A test operator is a sandbox-style operator account created for your developer company. It is separate from any real operator connection. Use a test operator to try requests, create credentials, configure webhook endpoints, and test integration behavior before working with real operator data. Create credentials and learn how access tokens and OAuth 2.0 work. Use your test operator to make your first API call. ## Manage accounts Use **Manage accounts** when you need to manage API access for a specific real operator. Connecting an operator account is optional. Use this flow when you are building a single-operator integration or when a developer manages credentials for an operator. The connection flow works like this: The developer shares an invitation link from the Developer Portal. The operator approves the connection by opening the link. After approval, the developer can manage OAuth 2.0 Client Credentials, access tokens, and webhook endpoints for that operator. Either the operator or developer can terminate the connection at any time. Terminating a connection can revoke access and disable credentials that depend on that operator relationship. ## Create partner app Use **Create partner app** when you are building an integration for multiple operators or preparing an app for marketplace distribution. In the Developer Portal, you can: * Create and maintain partner applications. * Develop against your test operators. * Invite selected operators to test your application. * Review the [partner approval checklist](/2025-09/guides/partner-approval-checklist) before you submit. * Submit your application for review when it is ready. * Manage the partner application listing on [`marketplace.stora.co`](https://marketplace.stora.co). Test applications are intentionally limited. They can be used with your developer company's test operators, or with real operators that accept an application invitation. For deeper guidance, see: * [Building a partner integration](/2025-09/guides/partner-integrations) * [Distributing a public plugin](/2025-09/guides/distributing-public-plugins) * [Third-party plugin naming rules](/2025-09/guides/third-party-plugin-naming) # Developer tools Source: https://docs.stora.co/2025-09/guides/developer-tools Use AI tools, MCP, and machine-readable specs to work with the Stora API more efficiently. This documentation site includes built-in integrations that help you work with the Stora API using AI assistants, code editors, and automation tools. ## Contextual menu Every page in these docs has a contextual menu (look for the icons in the page header) that lets you: * **Copy as markdown** — grab the page content for use in prompts or notes * **Open in ChatGPT, Claude, or Perplexity** — send the current page directly to an AI assistant with one click * **Install MCP server** — connect your docs-aware AI tools to the Stora API docs ## Documentation MCP server The Stora docs include a hosted [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server. When connected, your AI tools can search the content of these docs directly instead of relying on training data or web search. This is a documentation search tool — it doesn't connect to the Stora API or your account data. The MCP server URL is: ``` https://docs.stora.co/mcp ``` ```bash theme={null} claude mcp add stora-docs --transport sse https://docs.stora.co/mcp ``` Add the following to your `.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "stora-docs": { "url": "https://docs.stora.co/mcp" } } } ``` Add the following to your `.vscode/mcp.json`: ```json theme={null} { "servers": { "stora-docs": { "url": "https://docs.stora.co/mcp" } } } ``` Once connected, your AI assistant can answer questions about the Stora API using the latest documentation rather than potentially outdated training data. ## OpenAPI specification The full OpenAPI 3.1 specification is available in JSON format at: ``` https://docs.stora.co/2025-09/openapi.json ``` Most AI coding assistants and agent frameworks can consume this spec directly. Point your agent to the URL above and provide it with an access token. ## llms.txt These docs automatically generate and maintain [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt) files at the documentation root. These are an [industry standard](https://llmstxt.org/) that helps AI systems efficiently index and understand documentation — similar to how a sitemap helps search engines. ## Tips for AI integration These tips apply whether you're using the MCP server, feeding the OpenAPI spec to an agent, or working with an AI assistant that has access to these docs. * **Start with read-only scopes.** When experimenting, limit the access token to `read` scopes to prevent unintended modifications. * **Use idempotency keys for writes.** When the agent creates or updates resources, include an `Idempotency-Key` header to avoid duplicate operations on retries. * **Respect rate limits.** AI agents can generate bursts of requests. Implement backoff when receiving `429` responses. # Distributing a public plugin Source: https://docs.stora.co/2025-09/guides/distributing-public-plugins Connect operators to Stora from distributed code you don't host (WordPress plugins, browser extensions, on-premise packages). If you're distributing an integration to many operators through code that runs outside infrastructure you control — for example a WordPress plugin, browser extension, client-side app extension, or packaged on-premise tool — you cannot embed OAuth 2.0 client credentials in that distributed code. This guide compares two patterns: the recommended **broker** pattern, and a lighter **authorization proxy** alternative for cases where you accept that Stora tokens will live in each plugin installation. ## Choose an option **Use the broker pattern when:** * Your integration is installed by each operator on infrastructure you don't control (their WordPress site, browser, server, or laptop). * You need access to operator data beyond simple read-only, public information. * You want a single place to rotate credentials, revoke individual installations, and monitor usage. **Use the authorization proxy alternative only when:** * You need to keep the Stora `client_secret` out of distributed plugin code, but you cannot operate a full broker that stores tokens and proxies every API call. * Your plugin can protect Stora access and refresh tokens on each operator's infrastructure. * You accept weaker revocation, observability, and compromise isolation than the broker pattern provides. **You don't need this pattern** if your integration runs on infrastructure you control and operators authorise it directly — that's a standard [Authorization Code flow](/2025-09/guides/authentication#option-c-oauth-2-0-%E2%80%94-authorization-code). | Pattern | Stores Stora `client_secret` | Stores Stora access and refresh tokens | Responsible for token security | | ------------------- | ---------------------------- | -------------------------------------- | --------------------------------------------- | | Broker | Your broker | Your broker | Your backend infrastructure | | Authorization proxy | Your proxy | Each plugin installation | Your plugin and the operator's infrastructure | A hosted Shopify app usually uses the standard Authorization Code flow because the app backend is operated by the app developer and can act as the confidential OAuth client. Do not put Stora credentials or tokens in Shopify theme code, app extensions, or other client-side/distributed code. Route those calls through a backend you host. ## Option 1: Broker pattern (recommended) The broker pattern is the preferred option for public plugins. It keeps Stora tokens and OAuth credentials on infrastructure you control, and gives you one place to revoke installations, monitor usage, and handle refresh-token rotation. ### Architecture The broker pattern splits responsibility across three actors. Your distributed integration code runs inside an environment you don't control — it holds no Stora credentials. Your broker is a backend service you host — it holds your Stora `client_id` and `client_secret`, stores each operator's access and refresh tokens, and is the only thing Stora sees on the other end of OAuth. Stora issues exactly one confidential OAuth application to you, regardless of how many installations of your integration exist. ### Connection flow at a glance ```mermaid theme={null} sequenceDiagram participant User as Operator
(browser) participant Plugin as Plugin
(operator's site) participant Broker as Your broker
(backend you host) participant Stora as Stora OAuth User->>Plugin: Clicks "Connect to Stora" Plugin-->>User: 302 → broker /connect User->>Broker: GET /connect?install_id=… Broker->>Broker: Mint state, store {state → install_id} Broker-->>User: 302 → Stora /oauth2/authorize User->>Stora: Log in, approve scopes Stora-->>User: 302 → broker /callback?code=…&state=… User->>Broker: GET /callback?code=…&state=… Broker->>Stora: POST /oauth2/token (with client_secret) Stora-->>Broker: access_token + refresh_token Broker->>Broker: Store tokens keyed by install_id Broker-->>User: 302 → plugin success URL with broker_token User->>Plugin: GET …?broker_token=… ``` ### API call flow at a glance ```mermaid theme={null} sequenceDiagram participant Plugin as Plugin participant Broker as Your broker participant Stora as Stora API Plugin->>Broker: GET /api/orders
Authorization: Bearer <broker_token> Broker->>Broker: Look up Stora tokens for install
Refresh if expired Broker->>Stora: GET /2025-09/orders
Authorization: Bearer <stora_token> Stora-->>Broker: 200 OK (JSON) Broker-->>Plugin: 200 OK (JSON) ``` Stora sees one client (your broker), one redirect URI, one pair of credentials. Operators see your plugin. This is what makes the pattern safe to distribute publicly — the credentials that prove identity to Stora never leave your servers. ### Why the broker doesn't hand Stora tokens to the plugin It's tempting to skip the broker's own token layer and just forward Stora's `access_token` and `refresh_token` to the plugin. Doing so re-creates the problems the broker exists to avoid: * **Refresh-token theft persists.** Stora's refresh tokens are long-lived bearer credentials. Once exfiltrated from a plugin's database or backup, they work until revoked. * **You lose the kill switch.** To cut off a single installation you'd have to revoke Stora tokens — which affects the operator's other integrations and requires reconnection. * **You lose scope narrowing.** Broker-issued tokens can be narrower than the upstream OAuth grant. Raw Stora tokens cannot. * **You lose observability.** Direct plugin → Stora calls bypass your broker's logs, metrics, and rate limiting. * **Refresh rotation gets messy.** Stora rotates refresh tokens on every use; with plugin-held tokens, every rotation has to be pushed back to each installation. * **The `client_secret` is irrelevant after exfiltration.** Attackers use stolen tokens directly — they don't need to mint new ones. If you decide to hand Stora tokens to the plugin anyway, use the [authorization proxy alternative](#option-2-authorization-proxy-lighter-alternative) below rather than putting your `client_secret` in the plugin. Encrypt tokens at rest and be clear-eyed about what encryption buys you. See [Alternatives considered](#alternatives-considered) for the full list of mitigations and their limits — the short version is that encryption at rest only meaningfully protects against DB dumps, not against RCE or a malicious sibling plugin. ### Connecting an operator Before you can make API calls on behalf of an operator, the operator's installation needs to go through the Authorization Code flow once. All three of `client_id`, `client_secret`, and the `redirect_uri` belong to your broker — the plugin never sees them. The flow below shows what your broker implements. This guide assumes Stora has already issued you a confidential OAuth 2.0 application with a single `redirect_uri` pointing at your broker (e.g. `https://broker.yourcompany.com/stora/callback`). Partner credentials are not self-serve today — we provision them for you during onboarding. See [Building a partner integration](/2025-09/guides/partner-integrations) for how to get set up. The operator clicks "Connect to Stora" in your plugin's UI. The plugin redirects the operator's browser to your broker's `/connect` endpoint, passing whatever you use to identify this installation (site URL, install ID, tenant slug). ``` GET https://broker.yourcompany.com/stora/connect ?install_id=shop.example.com &plugin_nonce= ``` The `plugin_nonce` is yours to design — it lets the broker trust that this redirect actually came from a real installation of your plugin, not a random browser. See [What your broker is responsible for](#what-your-broker-is-responsible-for) for plugin-to-broker authentication notes. The broker mints a fresh `state` value, stores `{state → install_id}` in short-lived storage (Redis, or a DB row with a TTL — a few minutes is enough), and redirects the operator's browser to Stora: ``` HTTP/1.1 302 Found Location: https://app.stora.co/oauth2/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=https://broker.yourcompany.com/stora/callback &response_type=code &scope=public.contact:read public.order:read &state= ``` The `state` parameter is not optional for a broker: it prevents CSRF on the callback and lets you correlate the returning code with the right installation. The operator logs in to their Stora BackOffice (if not already) and approves the requested scopes. Stora redirects back to your broker's callback with a one-time code: ``` GET https://broker.yourcompany.com/stora/callback ?code=AUTHORIZATION_CODE &state= ``` Verify the `state` matches one you issued recently and recover the `install_id`. Then exchange the code at Stora's token endpoint, authenticating with your `client_secret`: ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/token" \ -H "content-type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "code=AUTHORIZATION_CODE" \ -d "redirect_uri=https://broker.yourcompany.com/stora/callback" ``` Response: ```json theme={null} { "access_token": "ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 7200, "scope": "public.contact:read public.order:read", "created_at": 1710000000, "refresh_token": "REFRESH_TOKEN" } ``` Store the `access_token`, `refresh_token`, and `expires_at` in your broker's database, keyed by `install_id`. Issue a **broker-scoped token** (an opaque identifier you mint yourself) back to the plugin and redirect to the plugin's success URL: ``` HTTP/1.1 302 Found Location: https://shop.example.com/wp-admin/admin.php?page=your-plugin&broker_token= ``` From this point on, the plugin holds only the broker token. Stora's `access_token` and `refresh_token` live on your broker and never leave it. The `redirect_uri` Stora sees on every connection is always `https://broker.yourcompany.com/stora/callback` — one URL, regardless of how many operator installations exist. That's the property that makes this pattern safe to distribute publicly. ### Making API calls Once connected, the plugin makes requests to your broker, the broker translates them into Stora API calls using the stored `access_token`, and returns the response to the plugin. The broker is responsible for refreshing expired tokens silently and for surfacing a "reconnect required" signal when refresh fails. #### The happy path The plugin calls your broker. Authenticate the plugin with the broker-scoped token you issued during the connect flow: ```bash theme={null} curl -X GET "https://broker.yourcompany.com/stora/orders" \ -H "authorization: Bearer BROKER_TOKEN" ``` The broker looks up the installation, checks the stored `expires_at`, refreshes if needed (see below), and proxies the request to Stora: ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/orders" \ -H "authorization: Bearer STORA_ACCESS_TOKEN" ``` Return the response to the plugin as-is, or reshape it to match your plugin's data model — your choice. #### Refreshing tokens Stora access tokens expire after 2 hours. When the stored `expires_at` is within a small buffer of now (e.g. 5 minutes), refresh before making the call: ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/token" \ -H "content-type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "refresh_token=STORA_REFRESH_TOKEN" ``` The response contains a new `access_token` **and** a new `refresh_token`. The previous refresh token is revoked immediately. Your broker must atomically update both values, or the next refresh will fail. Refresh-token rotation is not optional on Stora's side — every successful refresh invalidates the previous refresh token. If your broker loses track of the latest `refresh_token` (crash between HTTP response and DB write, race between two parallel refresh attempts for the same installation), the installation is bricked until the operator reconnects. Serialise refresh attempts per installation and write the new token before returning the new access token to callers. #### When refresh fails A `400` from the Stora token endpoint with `"error": "invalid_grant"` means the grant is no longer valid — usually because the operator disconnected your integration in Stora BackOffice. You cannot recover this without the operator going through the connect flow again. The broker should: mark the installation's tokens as revoked locally, return a well-defined error to the plugin (e.g. `401 Unauthorized` with a body like `{"error":"stora_reconnect_required"}`), and let the plugin prompt the operator to reconnect. #### Rate limits Stora applies rate limits per operator (10 req/s, 60 req/min — see [rate limiting](/2025-09/guides/requests#rate-limiting)). Your broker adds no rate-limit protection by default; a `429` from Stora propagates back to the plugin. Consider short request coalescing (same plugin, same endpoint, same operator, within the same second) if your plugin is chatty. ### Disconnecting and reconnecting Operators need to offboard cleanly, re-grant access after revocation, and occasionally switch which Stora account they've connected — without uninstalling the plugin. Your plugin's settings UI must expose Disconnect and Reconnect actions, and the broker must back them with the logic below. #### Disconnect The plugin's settings UI exposes a Disconnect button. When the operator clicks it, the plugin calls a disconnect endpoint on your broker — authenticated with the broker token — and your broker: 1. Calls Stora's `POST /oauth2/revoke` with the stored access token and your OAuth client credentials. 2. Deletes the stored Stora tokens and the broker token for this installation. 3. Returns `204 No Content` to the plugin. ```bash theme={null} curl -X POST "https://public-api.stora.co/oauth2/revoke" \ -H "content-type: application/json" \ -d '{ "token": "STORA_ACCESS_TOKEN", "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" }' ``` Stora stores the access token and refresh token on the same OAuth record, so revoking either string revokes the pair — one call is enough. Broker routes such as `/stora/connect`, `/stora/callback`, and `/stora/disconnect` are illustrative throughout this guide. Your broker can expose whatever URLs you prefer; only the upstream Stora calls (`/oauth2/authorize`, `/oauth2/token`, `/oauth2/revoke`) are fixed. #### Reconnect The plugin's settings UI also exposes a Reconnect button, which redirects the operator back through your existing connect entry point. The broker overwrites the installation's stored tokens on the next successful callback — no new endpoint required. Expose Reconnect even while the current tokens are still valid. Operators use it to re-grant expanded scopes when your integration adds a new feature, or to switch to a different Stora account without a reinstall. #### On plugin uninstall The integration should fire the same disconnect flow automatically when the operator removes it from their host, using whatever uninstall or deprovisioning hook the platform exposes — for example WordPress's `register_uninstall_hook`, or Shopify's `app/uninstalled` webhook for hosted Shopify apps. Treat uninstall hooks as a hygiene layer, not a replacement for the explicit Disconnect action. Some platforms don't fire them reliably: a user who deletes a WordPress site wholesale never triggers plugin uninstall hooks, and webhook delivery can fail on any hosted platform. The explicit Disconnect action plus Stora-side revocation remains the source of truth. ### What your broker is responsible for The broker is a piece of infrastructure you own and operate. At minimum, plan for the following. #### Plugin-to-broker authentication Stora authenticates your broker via `client_secret`. Your broker needs its own way to authenticate plugin installations. A common pattern: the plugin generates a long random value at activation time, registers it with the broker (HTTPS call keyed by install URL + admin email), and uses it as a bearer token on subsequent calls. Bind each token to one installation so a token stolen from one site can't be used to impersonate another. HMAC-signing requests with a per-install secret is a stronger alternative. #### Token storage and rotation Store `access_token`, `refresh_token`, and `expires_at` per installation. Refresh tokens rotate on every use — the previous one is revoked the instant Stora returns a new one. Write the new `refresh_token` to your database *before* you return the response to whoever triggered the refresh, and serialise refresh attempts per installation so two parallel workers can't race. Encrypt tokens at rest; the column on your broker's database is as sensitive as your Stora `client_secret`. #### Per-installation kill switch Because the broker mints its own plugin-facing tokens, you can cut off a single installation without touching Stora: invalidate the broker token locally and the plugin starts receiving your reconnect error on every call. This is the fastest response to a compromised installation — no Stora support ticket, no scope re-negotiation, no impact on other installations. #### Monitoring and abuse detection Log every call that passes through the broker with `install_id`, endpoint, and status code. Basic alerts to set up: sustained `429` rates per installation (indicates a runaway plugin), sustained `4xx` rates (indicates a broken installation or probing), sudden growth in call volume from a single installation, and anything that looks like credential-stuffing against your own `/connect` endpoint. Ship these to whatever you already use — you don't need anything Stora-specific. #### Disconnect and reconnect endpoints Expose routes the plugin can call to trigger [Disconnect](#disconnect) (broker revokes tokens at Stora and wipes local state) and [Reconnect](#reconnect) (broker redirects back through the connect flow). Wire the same revoke-and-discard path into the plugin's platform-specific uninstall hook as best-effort hygiene. Operators never see your broker. To them, your plugin is the integration and Stora is the data source. Everything about the broker — its URL, its storage, its uptime — is your private implementation detail. ## Option 2: Authorization proxy (lighter alternative) The authorization proxy is a lighter alternative to the broker pattern. It keeps your Stora `client_secret` on infrastructure you control, but it does **not** keep Stora tokens out of the plugin. The plugin installation — for example a WordPress site — stores the operator's Stora `access_token` and `refresh_token`, and calls the proxy whenever it needs to exchange an authorization code or refresh a token. Prefer the broker pattern for production integrations that handle customer PII, payments, contracts, access control, or other sensitive data. The authorization proxy reduces deployment complexity, but every plugin installation becomes part of your token trust boundary. You are responsible for protecting those tokens on the operator's infrastructure. WordPress environments vary widely, and tokens can be exposed through database dumps, backups, compromised admin accounts, vulnerable plugins, or server-level access. The authorization proxy protects your Stora `client_secret`, but it does not protect operator tokens after they are issued. If a refresh token is leaked, an attacker can mint access tokens until the grant is revoked, or until refresh-token rotation invalidates the stolen token before the attacker uses it. Choose the broker pattern if you do not want plugin installations to hold Stora tokens. ### How the authorization proxy works The plugin starts the OAuth connection by sending the operator to the authorization proxy. The proxy validates that the plugin callback URL is HTTPS and allowed for the installation, creates a signed `state` value containing the callback context, and redirects the operator to Stora. After approval, Stora redirects back to the proxy with the authorization code; the proxy verifies the signed `state`, creates a signed short-lived `code_context` bound to that code and installation, and redirects the browser back to the plugin with the code. The plugin then calls the proxy server-to-server to exchange the code for Stora access and refresh tokens. The server-to-server call must authenticate the plugin installation, for example with an `Authorization` header or HMAC signature. The plugin stores the returned tokens locally, but never calls Stora's token endpoint directly. When the access token expires, the plugin sends the stored refresh token to the proxy; the proxy authenticates to Stora with the partner `client_secret`, performs the refresh, and returns the rotated tokens for the plugin to replace locally. The `code_context` is not a replacement for the Stora authorization code. It is a short-lived signed value created by the proxy so the proxy can stay stateless. It binds the authorization code to the plugin installation and callback that started the flow. When the plugin later calls `/token`, the proxy verifies the `code_context` before using its `client_secret` to exchange the code with Stora. You can implement `code_context` as a signed JWT/JWS or any equivalent authenticated token format. The payload should include the authorization code hash, installation identifier, callback URL, and short expiry. Sign it with a proxy-only secret. Do not put the Stora `client_secret` or access/refresh tokens in `code_context`. ```json theme={null} { "code_hash": "sha256:BASE64URL_SHA256_OF_AUTHORIZATION_CODE", "install_id": "demo-self-storage.example.com", "callback_url": "https://demo-self-storage.example.com/wp-admin/admin.php?page=your-plugin", "expires_at": 1710000300 } ``` When `/token` is called, verify the signature, expiry, `install_id`, callback URL, and that `sha256(code)` matches `code_hash` before exchanging the code with Stora. The diagram below uses WordPress as the example plugin host. ```mermaid theme={null} sequenceDiagram autonumber actor Operator as Operator participant Browser as Operator browser participant WP as WordPress site
public plugin participant Proxy as Authorization proxy participant StoraAuth as Stora OAuth authorize participant StoraToken as Stora OAuth token Operator->>WP: Click "Connect to Stora" WP->>WP: Create wp_state and remember install_id WP-->>Browser: Redirect to proxy /connect
install_id, wp_callback, wp_state Browser->>Proxy: GET /connect Proxy->>Proxy: Validate wp_callback is HTTPS
and allowed for install_id
Create signed stora_state with install_id, wp_callback, wp_state Proxy-->>Browser: 302 to Stora /oauth2/authorize
client_id, redirect_uri=proxy callback,
response_type=code, scope, state=stora_state Browser->>StoraAuth: Open authorization page Operator->>StoraAuth: Log in and approve scopes StoraAuth-->>Browser: 302 to proxy callback
code=AUTHORIZATION_CODE, state=stora_state Browser->>Proxy: GET /stora/callback?code&state Proxy->>Proxy: Verify signed stora_state
Create signed code_context
bound to code_hash, install_id, expiry Proxy-->>Browser: 302 to WordPress callback
code=AUTHORIZATION_CODE,
code_context=SIGNED_CONTEXT,
state=wp_state Browser->>WP: GET /wp-admin/...your-plugin?code&code_context&state WP->>WP: Verify wp_state WP->>Proxy: POST /token
code, code_context, install_id
Authorization: Bearer plugin_secret
or HMAC signature Proxy->>Proxy: Validate plugin authentication
Verify code_context matches code and install_id Proxy->>StoraToken: POST /oauth2/token
grant_type=authorization_code,
client_id, client_secret,
code=AUTHORIZATION_CODE,
redirect_uri=proxy callback StoraToken-->>Proxy: access_token, refresh_token, expires_in Proxy-->>WP: access_token, refresh_token, expires_in WP->>WP: Store tokens securely for this site WP-->>Operator: Show connected state Note over WP,Proxy: The client_secret stays only in the proxy.
WordPress receives the Stora authorization code, but never receives the client_secret. Note over WP: WordPress stores Stora access and refresh tokens for this site. Note over Proxy: The proxy can stay stateless by signing state and code_context instead of storing callback data. Note over WP,StoraToken: WordPress never calls Stora's token endpoint directly.
Code exchanges and refreshes go through the proxy because Stora requires the partner client_secret. rect rgb(245, 248, 255) WP->>Proxy: Later: POST /refresh
refresh_token, install_id
Authorization: Bearer plugin_secret
or HMAC signature Proxy->>Proxy: Validate plugin authentication
Read client_secret from proxy secrets Proxy->>StoraToken: POST /oauth2/token
grant_type=refresh_token,
client_id, client_secret, refresh_token StoraToken-->>Proxy: new access_token, new refresh_token Proxy-->>WP: new access_token, new refresh_token, expires_in WP->>WP: Replace stored tokens with rotated tokens end ``` ### Authorization proxy trade-offs Compared with the broker pattern, the authorization proxy removes the need to store per-installation Stora tokens on your backend. That makes the proxy easier to operate, and it can be close to stateless if you use signed, expiring `state` and `code_context` values. The trade-off is that Stora tokens now live in the plugin installation. A compromised WordPress database, backup, admin account, server, or sibling plugin can expose the operator's Stora tokens. You also lose the broker's per-installation kill switch, API-level observability, rate limiting, and scope narrowing. Use this pattern only when that risk is acceptable, and document the token-storage responsibility clearly for operators. ## Alternatives considered You don't need to read this section to build the integration. It exists for partners who want to understand why we recommend the broker pattern over the alternatives the OAuth 2.0 spec technically allows. PKCE protects authorisation codes from interception during the redirect. It does not protect OAuth tokens after they're issued. For a plugin distributed to many operators, the consequences are: * **Tokens live on the operator's infrastructure.** `wp_options` is plaintext; other sensitive values in the same table get exfiltrated together in DB dumps, backups copied to staging, and compromised-plugin incidents. * **No client authentication.** A public `client_id` can be copied into any application. PKCE binds a code to a device but proves nothing about *who* the client is. A phishing app reusing your `client_id` presents the real Stora consent screen with your branding. * **Refresh tokens are long-lived bearer credentials.** Once out of the operator's database they work until revoked. Rotation shrinks the window; it doesn't close it. * **Loose redirect URIs.** Thousands of installations mean either wildcard redirect URIs (vulnerable to subdomain takeover) or Dynamic Client Registration (see below). * **No abuse isolation.** Revoking the public `client_id` breaks every installation simultaneously. * **No consent phishing protection.** An attacker can complete the flow with your `client_id` since there's no secret to prove identity. Public clients plus PKCE are designed for **single-user applications on a user's own device** (RFC 8252 — native mobile and desktop apps). They are not designed for server-resident multi-tenant integrations where every installation stores its own long-lived tokens on infrastructure you don't control. Hosted marketplace apps with developer-operated backends are different: the backend can keep credentials and tokens server-side, which is the same security boundary the broker pattern creates. DCR would let each plugin installation register its own `client_id` (still public, still PKCE) at install time. Each site then has its own OAuth client record on Stora's side, which solves the redirect-URI and abuse-isolation problems. Stora does not currently support Dynamic Client Registration. If you have a use case that specifically requires it, [get in touch](https://stora.co/contact) — we prioritise based on partner demand. DCR alone doesn't solve the token-storage problem (tokens still live in `wp_options`); it mainly simplifies the consent and redirect-URI side of operating many public-client installations. We understand the broker pattern adds operational overhead. If you decide to let the plugin store Stora access and refresh tokens, use the [authorization proxy](#option-2-authorization-proxy-lighter-alternative) shape above. Do **not** put the Stora `client_secret` in the plugin, and do **not** have the plugin call Stora's token endpoint directly. The plugin should receive the authorization code in its callback, then call your proxy to exchange the code or refresh token. Your proxy authenticates to Stora with the `client_secret` and returns the resulting tokens to the plugin. At minimum, do the following: * **Keep the `client_secret` proxy-side.** The proxy exists to protect the confidential OAuth client credentials. The plugin should only ever see the authorization code, `code_context`, access token, refresh token, expiry, and the plugin-to-proxy credential you design. * **Use signed, expiring `state` and `code_context`.** If you want the proxy to stay stateless, encode the callback URL, installation identifier, and nonce inside a signed `state` value. Validate the callback URL is HTTPS and allowlisted for the installation before redirecting. After Stora redirects back, issue a signed `code_context` bound to the authorization code hash, installation identifier, and short expiry. Reject expired, tampered, or mismatched values. * **Authenticate plugin-to-proxy calls.** The `/token` and `/refresh` endpoints still need plugin authentication, such as an `Authorization` header with an install-time shared secret or HMAC-signed requests. Do not pass that secret through browser redirects or query strings. Otherwise anyone with a code or refresh token can ask your proxy to use your `client_secret` for them. * **Encrypt tokens at rest.** WordPress core stores `wp_options` values as plaintext. Google Site Kit's [`Data_Encryption`](https://github.com/google/site-kit-wp/blob/main/includes/Core/Storage/Data_Encryption.php) is the de-facto reference implementation — AES-256-CTR with a key derived from `LOGGED_IN_KEY` in `wp-config.php`. Most major plugins (Jetpack, WooCommerce Stripe, Mailchimp for WooCommerce) don't encrypt at all; doing so puts you ahead of the ecosystem default. * **Understand what encryption buys you.** It only mitigates stolen SQL backups, misconfigured phpMyAdmin, read-only DB leaks, and compromised noisy neighbours on shared MySQL. It does **not** mitigate a compromised WordPress admin, an RCE on the host, a malicious sibling plugin, or a `wp-config.php` leak — in all those cases the key is on the same box. * **Narrow your scopes aggressively.** Request only what the plugin genuinely needs. Every scope you request is a scope an attacker inherits. * **Rotate refresh tokens through the proxy and persist the replacement.** Stora rotates refresh tokens on every successful refresh. The plugin must send the current refresh token to the proxy, the proxy must refresh with the `client_secret`, and the plugin must atomically replace both the access token and refresh token it has stored. * **Accept that you cannot revoke one installation without revoking the operator's entire OAuth grant.** Without a broker-issued token layer, there is no local kill switch granularity — you either revoke the Stora grant or you don't. Even with all of the above, the integration's security posture is bounded by the weakest plugin installation running it. The authorization proxy protects your `client_secret`; it does not remove the plugin host from the Stora token trust boundary. The broker pattern does, which is why we recommend it for anything touching customer PII, payments, contracts, access control, or other sensitive data. ## Next steps You now have the OAuth side built. Before going live, also review: Review OAuth, scopes, Timeline Events, webhooks, disconnect behavior, and public plugin requirements before you submit. Per-operator limits apply. Your broker or plugin must handle 429s, depending on the pattern. React to Stora events instead of polling. Your broker or proxy receives them on a single URL. Full details on OAuth flows, token exchange, and token refresh. Questions about any of the above, or an integration pattern that doesn't fit these options? [Contact us](https://stora.co/contact) before you start building — it's cheaper than rebuilding. # Errors Source: https://docs.stora.co/2025-09/guides/errors Error codes, response format, and troubleshooting for the Stora Public API. ## Error codes | Code | HTTP status | Type | Default message | | ----------------------- | ----------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `invalid_request` | 400 | `invalid_request_error` | The request body is malformed or not valid. | | `api_error` | 400 | `api_error` | Internal or third-party API error. | | `invalid_token` | 401 | `invalid_request_error` | Invalid token. | | `forbidden` | 403 | `invalid_request_error` | You do not have permission to access this resource. Please contact our support team if you believe you should have access. | | `resource_not_found` | 404 | `invalid_request_error` | The requested resource was not found. | | `invalid_endpoint` | 404 | `invalid_request_error` | The requested endpoint does not exist. | | `invalid_format` | 406 | `invalid_request_error` | The request ACCEPT header is not valid or supported. | | `conflict` | 409 | `invalid_request_error` | The resource cannot be deleted or modified due to existing dependencies or business constraints. | | `invalid_url_param` | 422 | `invalid_request_error` | The request URL param is invalid. | | `invalid_content` | 422 | `invalid_request_error` | The request body content is not valid. | | `too_many_requests` | 429 | `invalid_request_error` | Rate Limit Exceeded. You have reached the maximum number of requests. Please try again later. | | `internal_server_error` | 500 | `api_error` | Unexpected internal error. | ## Example response ```json theme={null} { "error": { "code": "invalid_content", "details": [ { "message": "value at `/email` does not match format: email", "pointer": "/email" } ], "links": [ { "kind": "open_api", "name": "OpenAPI specification", "url": "https://public-api.stora.co/2025-09/openapi.json" }, { "kind": "documentation", "name": "Errors", "url": "https://docs.stora.co/public-api#overview--errors" } ], "message": "The request body content is not valid.", "request_id": "01563646-58c1-4607-8fe0-cae3e92c4477", "type": "invalid_request_error" } } ``` ## Troubleshooting * **401 Unauthorized** — Check that your access token is valid and hasn't expired. Regenerate it if needed. * **403 Forbidden** — Your token doesn't have the required scope for this endpoint. Check the [authorization scopes](/2025-09/guides/authorization). * **422 Invalid content** — The request body doesn't match the expected schema. Check the `details` array in the error response for specific field-level errors. * **429 Too Many Requests** — You've hit the rate limit. See [rate limiting](/2025-09/guides/requests#rate-limiting) for how to handle this. # Your first API call Source: https://docs.stora.co/2025-09/guides/first-request Make your first request to the Stora Public API and explore the response. With your token in hand, let's make some requests and understand how the API responds. Create a test operator in the [Developer Portal](/2025-09/guides/developer-portal) before exploring real operator data. Test operators are the recommended way to try requests safely. ## List your sites ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/sites" \ -H "accept: application/json" \ -H "authorization: Bearer YOUR_ACCESS_TOKEN" ``` Response: ```json theme={null} { "sites": [ { "id": "site_14b419f1096013f1", "name": "Downtown Storage", "description": "Central location with 24/7 access", "phone": "+44 20 7946 0958", "opened_at": "2024-03-15T00:00:00Z", "created_at": "2024-01-10T09:30:00Z", "updated_at": "2025-06-01T14:22:00Z", "directions": { "google_maps_url": "https://maps.google.com/?q=..." }, "access_hours": { "monday": { "status": "set_hours", "open": "06:00", "close": "22:00" }, "tuesday": { "status": "set_hours", "open": "06:00", "close": "22:00" }, "saturday": { "status": "twenty_four_hours" }, "sunday": { "status": "closed" } }, "address": { "line_1": "42 Storage Lane", "line_2": null, "city": "London", "postal_code": "EC1A 1BB" } } ], "meta": { "pagination": { "count": 1, "page": 1, "pages": 1, "limit": 50, "next": null, "prev": null, "last": 1 } } } ``` A few things to notice: * **`sites` array** — resources are wrapped in a key matching the resource name * **`meta.pagination`** — every list response includes pagination info. Use `page` and `limit` query parameters to navigate. * **IDs are prefixed strings** — e.g. `site_14b419f1096013f1`. Use these when referencing resources in other endpoints. ## Fetch a single resource Fetch a single site by its ID: ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/sites/site_14b419f1096013f1" \ -H "accept: application/json" \ -H "authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```json theme={null} { "site": { "id": "site_14b419f1096013f1", "name": "Downtown Storage", "..." }, "meta": {} } ``` ## Expand related resources Some endpoints support the `expand` query parameter to include related resources inline. Without `expand`, related resources appear as IDs. With it, they're included as full objects. ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/sites/site_14b419f1096013f1?expand=unit_types" \ -H "accept: application/json" \ -H "authorization: Bearer YOUR_ACCESS_TOKEN" ``` You can expand multiple relations with a comma-separated list, and nest with dot notation: ``` ?expand=contact,line_items,line_items.item ``` Each expanded resource requires the appropriate read scope on your token. ## Common patterns These apply across the entire API. Each is covered in more detail in the dedicated guides. ### Pagination List endpoints return 50 items by default, up to a maximum of 100. Use `page` and `limit` to navigate: ``` GET /2025-09/contacts?page=2&limit=25 ``` Check `meta.pagination.pages` for the total number of pages and `meta.pagination.next` for the next page number (`null` on the last page). See [Responses](/2025-09/guides/responses#pagination) for full details. ### Error handling Errors return a consistent structure with a `code` for programmatic handling and `details` for field-level validation messages. See [Errors](/2025-09/guides/errors) for the full list of error codes. ```json theme={null} { "error": { "code": "invalid_content", "type": "invalid_request_error", "message": "The request body content is not valid.", "details": [ { "message": "value at `/email` does not match format: email", "pointer": "/email" } ], "request_id": "01563646-58c1-4607-8fe0-cae3e92c4477" } } ``` ### Idempotency For any `POST` request, include an `Idempotency-Key` header to safely retry without creating duplicates. The API stores the response for 24 hours. See [Requests](/2025-09/guides/requests#idempotent-requests) for details. ```bash theme={null} curl -X POST "https://public-api.stora.co/2025-09/contacts" \ -H "content-type: application/json" \ -H "authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "idempotency-key: a1b2c3d4-unique-key" \ -d '{"full_name": "Jane Smith", "email": "jane@example.com"}' ``` ### Rate limits The API allows 10 requests per second and 60 requests per minute. If you exceed this, you'll receive a `429 Too Many Requests` response. Check the `RateLimit-Reset` header for when to retry. See [Requests](/2025-09/guides/requests#rate-limiting) for details. # Introduction Source: https://docs.stora.co/2025-09/guides/introduction Build integrations, automate workflows, and sync data with the Stora Public API. The Stora Public API gives you programmatic access to your self-storage business. Use it to build custom integrations, automate operational workflows, and sync data with the tools you already use. **Not a developer?** Connect Stora to thousands of apps without writing code using our [Zapier integration](https://intercom.help/stora/en/articles/13162033-automate-tasks-in-zapier). Understand Stora's domain model — sites, units, contacts, orders, tenancies, and more. Create test operators, manage connected accounts, and submit partner apps. Get set up with access tokens or OAuth 2.0 and start making requests. Make your first request, explore responses, and learn common patterns. Receive real-time notifications when events happen in Stora. ## What you can do * **Manage contacts and leads** — create, update, and search your customer database * **Automate bookings** — build custom booking flows with orders, tenancies, and subscriptions * **Sync inventory** — track unit availability, allocations, and status changes in real time * **Process billing** — access invoices, credit notes, and subscription data * **React to events** — receive webhooks when things happen in Stora * **Explore safely** — create test operators in the [Developer Portal](/2025-09/guides/developer-portal) before working with real operator data * **Build partner integrations** — create partner apps, develop with test operators, and submit marketplace listings from the [Developer Portal](/2025-09/guides/developer-portal) # Resource metadata Source: https://docs.stora.co/2025-09/guides/metadata Store arbitrary key-value pairs on resources using the metadata field. Some resources support a `metadata` field that allows you to store arbitrary key-value pairs. This is useful for attaching your own identifiers, references, or any other information that is meaningful to your integration. ## Format Metadata is a flat JSON object where both keys and values must be strings. ```json theme={null} { "metadata": { "external_id": "abc_123", "source": "zapier", "correlation.id": "req-001" } } ``` ## Constraints | Constraint | Limit | | ---------------------- | ------------------------------------------------------------------------------------------------- | | Maximum number of keys | 20 | | Key format | Lowercase alphanumeric characters, dots (`.`), underscores (`_`), colons (`:`), and hyphens (`-`) | | Maximum key length | 40 characters | | Maximum value length | 500 characters | | Value type | String only | ## Update semantics Metadata updates use **merge-patch** semantics ([RFC 7386](https://datatracker.ietf.org/doc/html/rfc7386)). When updating metadata, only the keys you include in the request are affected. Existing keys that are not included remain unchanged. * **Add or update a key**: include the key with a string value * **Remove a key**: include the key with a `null` value * **Clear all metadata**: send an empty object `{}` * **Leave metadata unchanged**: omit the `metadata` field from the request ```bash title="Create with metadata" theme={null} curl -X POST "https://public-api.stora.co/2025-09/webhook_endpoints" \ -H 'content-type: application/json' \ -H 'authorization: Bearer ACCESS_TOKEN' \ -d '{"name": "My Webhook", "url": "https://example.com/webhook", "api_version": "2025-09", "event_types": ["contact.created"], "metadata": {"source": "zapier", "external_id": "abc_123"}}' ``` ```bash title="Update: change one key, remove another" theme={null} curl -X PATCH "https://public-api.stora.co/2025-09/webhook_endpoints/we_..." \ -H 'content-type: application/json' \ -H 'authorization: Bearer ACCESS_TOKEN' \ -d '{"metadata": {"source": "make", "external_id": null}}' ``` After the update, the metadata will be `{"source": "make"}` — the `source` key was updated and `external_id` was removed. # Building a partner integration Source: https://docs.stora.co/2025-09/guides/partner-integrations Build integrations that work across multiple Stora operators and partner with us. If you're building a product or service that integrates with Stora on behalf of multiple operators, you're building a partner integration. This guide covers how it works, what we expect, and how to get started. ## Operator integrations vs partner integrations There are two ways to connect to the Stora API: | | Operator integration | Partner integration | | ----------------- | ----------------------------------------------------------- | ------------------------------------------------------- | | **Who builds it** | The operator (or their developer) | A third-party company | | **Auth flow** | Access Token or Client Credentials | Authorization Code (required) | | **Scope** | Single operator's data | Multiple operators, each authorising independently | | **Setup** | Manage a connected operator account in the Developer Portal | Create and submit a partner app in the Developer Portal | | **Example** | Internal reporting dashboard, custom booking widget | Smart entry provider, CRM connector, accounting sync | Operators must never share their access tokens or Client Credentials with third parties. If you're a third party building an integration, you must use the Authorization Code flow. ## How the partner programme works Use the [Developer Portal](/2025-09/guides/developer-portal) to create a partner application for your integration. Use test operators in the Developer Portal while you build your integration with the [Authorization Code flow](/2025-09/guides/authentication#option-c-oauth-2-0-%E2%80%94-authorization-code). Test applications can be used with your test operators, or with real operators that accept an application invitation. Manage your partner application listing for [`marketplace.stora.co`](https://marketplace.stora.co) in the Developer Portal. Submit your application approval request when your integration is ready. Before you submit, use the [partner approval checklist](/2025-09/guides/partner-approval-checklist) to review scopes, External Events, error handling, connection lifecycle, and operator-facing behavior. Once approved, your integration becomes available to operators through the marketplace. After launch, we monitor usage and operator feedback. Any changes to your integration's scopes go through a lightweight review process. ## Technical requirements These apply to all partner integrations. Meeting them is part of the review process. ### Naming and presentation Your integration must use a distinct product name and must not imply that Stora built, owns, operates, or endorses it. If you're distributing a plugin, extension, or packaged integration, follow the [third-party plugin naming rules](/2025-09/guides/third-party-plugin-naming) before you publish. ### Authentication * You **must** use the [OAuth 2.0 Authorization Code flow](/2025-09/guides/authentication#option-c-oauth-2-0-%E2%80%94-authorization-code). No other flow is accepted for partner integrations. * Handle [token refresh](/2025-09/guides/authentication#refreshing-tokens) correctly — access tokens expire after 2 hours. * Never ask operators to share or copy-paste credentials. Your integration should handle the OAuth flow end-to-end. * If you're distributing your integration as code that runs outside infrastructure you control (WordPress plugin, browser extension, client-side app extension, packaged on-premise tool, etc.), follow the [broker pattern](/2025-09/guides/distributing-public-plugins) — PKCE alone is not sufficient for multi-tenant distribution. ### Identify the connected operator Use [`GET /oauth2/token/info`](/2025-09/api-reference/oauth-2/retrieve-info-for-the-access-token) with the current access token to inspect the connected account and granted scopes. Your integration must display the returned `operator.name` in your app. A single Stora staff member can have access to multiple operators, so they need to know which operator account is connected. Use the returned scopes to check whether the current token includes the permissions your integration needs. If a newly added feature requires scopes that are missing, prompt the operator to reconnect through the Authorization Code flow so they can re-authorise the expanded scopes. You can use this endpoint as the initial request to verify the connection. ### Disconnect and reconnect Your integration **must** expose operator-facing Disconnect and Reconnect actions in its UI. Operators expect to manage the connection from inside the product they installed. * **Disconnect** must call [`POST /oauth2/revoke`](/2025-09/api-reference/oauth-2/revoke-an-access-token) with a valid token and your OAuth client credentials, then discard the locally stored tokens. Passing either the access token or the refresh token revokes the pair — one call is enough. * **Reconnect** is re-running the [Authorization Code flow](/2025-09/guides/authentication#option-c-oauth-2-0-%E2%80%94-authorization-code). Expose it in the UI even while the current tokens are still valid, so operators can re-grant expanded scopes or switch Stora accounts without uninstalling. * **On uninstall or deprovisioning**, your integration **must** automatically revoke tokens and discard local state. For distributed plugins, wire the same revoke-and-discard into the host's uninstall hook. For hosted marketplace apps, use the platform's deprovisioning webhook, such as Shopify's `app/uninstalled` webhook. These hooks are best-effort; treat them as a hygiene layer on top of the explicit Disconnect action, not a replacement for it. Operators may also terminate connections outside your integration. When your integration detects a refresh failure (a `400` or `401` from the token endpoint), surface a clear "reconnect" prompt to the operator. For plugin-distributed integrations, see [Disconnecting and reconnecting](/2025-09/guides/distributing-public-plugins#disconnecting-and-reconnecting) in the broker guide for the broker-side responsibilities. ### External Events When your integration takes an action in Stora on behalf of an operator, it must be visible to them. You're required to create **[Timeline Events](/2025-09/api-reference/timeline-events/create-an-event)** for actions originating on your platform. For example: * "Unit locked via YourApp" * "Customer contacted via YourApp" Use Timeline Sources and Templates to structure these events. If you need custom Templates specific to your integration, we'll review and create them during the onboarding process. ### Rate limits Standard rate limits apply (10 requests/second, 60 requests/minute) per operator, shared with their other API usage. Strategic partners may negotiate independent higher limits for operators using their integration. Your integration must implement backoff when receiving `429` responses — see [rate limiting](/2025-09/guides/requests#rate-limiting). ### Idempotency Use `Idempotency-Key` headers on all `POST` requests to prevent duplicate operations. See [idempotent requests](/2025-09/guides/requests#idempotent-requests). ### Error handling Handle all [documented error codes](/2025-09/guides/errors) gracefully. Do not retry indefinitely on `4xx` errors. ### Metadata Use the [`metadata` field](/2025-09/guides/metadata) to store your own references on supported resources (contacts, orders, tasks, notes, webhook endpoints) rather than maintaining external mapping tables. ### Webhooks Use [webhooks](/2025-09/guides/webhooks) to react to Stora events rather than polling. This is more efficient, gives you real-time data, and respects rate limits. ## Scope principles We follow the principle of **least privilege**. Your production credentials will be scoped to only the permissions your integration needs — nothing more. During development, use test operators in the Developer Portal so you can explore freely. Test applications can only be used with your test operators or operators that accept an application invitation. At review time, we'll agree on the minimum set of scopes for production. If you need additional scopes after launch (e.g. you're adding a new feature), submit a request with justification. Operators who have already connected will need to re-authorise to grant the new scopes. ## Get started Ready to build a partner integration? Create a partner app in the [Developer Portal](/2025-09/guides/developer-portal), develop with test operators, review the [partner approval checklist](/2025-09/guides/partner-approval-checklist), then submit your application for review. # Requests Source: https://docs.stora.co/2025-09/guides/requests Request formats, idempotency, and rate limiting for the Stora Public API. ## Request formats The API supports only **JSON** requests. You must specify the format using the `Content-Type` header. ```bash theme={null} curl -X POST "https://public-api.stora.co/2025-09/contacts" \ -H 'accept: application/json' \ -H 'content-type: application/json' \ -H 'authorization: Bearer ACCESS_TOKEN' \ -d '{"full_name": "John Doe"}' ``` ## Idempotent requests The API supports [idempotency](https://developer.mozilla.org/en-US/docs/Glossary/Idempotent) for any `POST` request. To make an idempotent `POST` request, provide an additional `Idempotency-Key: ` header. The key should be a unique value — a generated [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) is a good choice. ```bash theme={null} curl -X POST "https://public-api.stora.co/2025-09/contacts" \ -H 'accept: application/json' \ -H 'content-type: application/json' \ -H 'authorization: Bearer ACCESS_TOKEN' \ -H 'idempotency-key: UNIQUE_KEY' \ -d '{"full_name": "John Doe"}' ``` ### How it works When you make a request with a new idempotency key, the API stores the response status code and body, whether the request succeeds or fails, for 24 hours. If you send additional requests with the same key within that period, the API will return the exact same response, including any `400` or `500` errors. After 24 hours, the stored response is invalidated, and the next request with that key will be processed and stored as if it were new. ## Rate limiting The API uses rate limiting to ensure fair usage and maintain performance. The default rate limit is: * **10 requests per second** * **60 requests per minute** These limits may vary based on your subscription plan or specific agreements with Stora. ### Handling rate limits If you exceed the rate limit, the API responds with a `429 Too Many Requests` status code. The response includes headers with your current rate limit status: | Header | Description | | ----------------- | --------------------------------------------------------------------------- | | `RateLimit-Limit` | The maximum number of requests allowed in the current time window | | `RateLimit-Reset` | The time at which the current rate limit window resets in UTC epoch seconds | # Responses Source: https://docs.stora.co/2025-09/guides/responses Response formats, expanding, pagination, and links in the Stora Public API. ## Response formats The API supports JSON, CSV, and PDF response formats. Specify your preferred format using the `Accept` header. ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/contacts" \ -H 'accept: application/json' \ -H 'authorization: Bearer ACCESS_TOKEN' ``` ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/contacts" \ -H 'accept: text/csv' \ -H 'authorization: Bearer ACCESS_TOKEN' ``` PDF format is only supported for specific resources such as Invoices and Credit Notes. The endpoint returns a `406 Not Acceptable` if the content cannot be returned in PDF format. ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/invoices/inv_195h6kfm9ro15lof" \ -H 'accept: application/pdf' \ -H 'authorization: Bearer ACCESS_TOKEN' ``` ## Expanding responses Some endpoints support expanding nested objects in the response using the `expand` query parameter. You can expand multiple objects with a comma-separated list, and use dot notation for nested objects. For example, when retrieving an order, you can expand the `contact`, `line_items`, and `line_items.item`: ```bash theme={null} curl -X GET "https://public-api.stora.co/2025-09/orders?expand=contact,line_items,line_items.item" \ -H 'accept: application/json' \ -H 'authorization: Bearer ACCESS_TOKEN' ``` You can find examples of fully expanded responses in the "Show a \" endpoints. For each expanded resource, ensure the access token has read scope for that resource. For example, when expanding the `site` on `subscription`, the `public.site:read` scope is required. ## Pagination List endpoints are paginated. The defaults are: * **50 items per page** by default * **100 items per page** maximum (set via the `limit` query parameter) * Use the `page` query parameter to navigate through pages Pagination data (`count`, `last`, `limit`, `next`, `page`, `pages`, and `prev`) is returned in the response's `meta/pagination` field. ## Links Some resources include a `_links` field containing [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS)-style links. These provide URLs to related actions or pages, such as signing a contract on the storefront or starting a new booking for a unit type. ### Structure Each link is keyed by a namespaced relation name and contains an `href` (relative path) and a human-readable `title`: ```json theme={null} { "unit_type": { "id": "utype_f18fc91387cdf710", "name": "5x5 Storage Unit", "_links": { "sf:new_order": { "href": "/sites/belfast-self-storage/5x5-unit/order/contact-details?unit_type_slug=5x5-unit", "title": "New order" } } } } ``` ### CURIEs Link relation names use a [CURIE](https://www.w3.org/TR/curie/) (Compact URI) prefix to indicate which application the link points to: | Prefix | Application | Example | | ------ | ----------------------------------------- | ----------------- | | `sf` | Storefront (customer-facing booking site) | `sf:new_order` | | `bo` | Backoffice (operator dashboard) | `bo:view_contact` | The `meta` object includes a `curies` array that maps each prefix to a URL template. To resolve a full URL, replace `{rel}` in the CURIE `href` with the link's `href`: ```json theme={null} { "meta": { "curies": [ { "name": "bo", "href": "https://app.stora.co{rel}", "templated": true, "title": "Backoffice" }, { "name": "sf", "href": "https://acme.stora.co{rel}", "templated": true, "title": "Storefront" } ] } } ``` For example, a link `sf:new_order` with `href` `/sites/belfast/5x5-unit/order/contact-details?unit_type_slug=5x5-unit` resolves to: ``` https://acme.stora.co/sites/belfast/5x5-unit/order/contact-details?unit_type_slug=5x5-unit ``` ### Conditional links Links can be conditional — they only appear when the action is available. For example: * A **contract** only has `sf:show_contract` when it is not voided or deleted * A **unit type** only has `sf:new_order` when its status is `bookable` When no links are available, the `_links` field is an empty object `{}`. # Migrating to site-scoped protection levels Source: https://docs.stora.co/2025-09/guides/site-scoped-protection-levels Protection levels are moving from your account to your sites. Prepare your integration before your account is switched over. Protection levels are moving from your account to your sites. Today one set of levels is shared across every site you operate. After this change each site has its own levels, with its own prices. This guide covers what changes in the API, what you need to do, and when. ## Why * Protection prices can differ from site to site, which operators with mixed markets need and can't currently do. * It's a prerequisite for connecting multiple Stripe accounts to one Stora account, which we're building now. ## The two phases Your account moves through two phases. Every account is in **Preparation** until we switch it over. We'll tell you before we activate yours, and we'll give you notice. ### Preparation We've replicated every global protection level to every site on your account. Each site copy is a distinct product: * its own protection level ID * its own product and price in Stripe * its own base price Globals remain the source of truth. We mirror changes down to the copies — edit a global's base price and every site copy follows. Site copies can't be edited independently during this phase, so a customer sees the same price whichever route they come through. You'll see duplicate products and prices in your Stripe account as a result. Don't archive the duplicate products and prices in Stripe. They're the live products for your sites once you're switched over. Nothing in the API changes by default in this phase. Global levels are still returned by default and still accepted on orders. The Back Office and Storefront still work in terms of global protection levels; the site copies aren't surfaced there. This phase exists to give you time to migrate. ### Activation When your account is switched over, site protection levels become authoritative everywhere — API, Back Office and Storefront. * The API returns site levels and rejects global IDs on new orders. * Global levels can no longer be created, and their base prices can no longer be changed, in the API or the Back Office. * The Back Office moves protection management from your account to each site. ## Affected endpoints | Endpoint | What changes | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `GET /2025-09/protection_levels` | New `site_scoped` and `site_id` parameters available now. Returns site levels by default after activation. | | `GET /2025-09/protection_levels/{id}` | Resolves site levels as well as globals now. Keeps resolving globals after activation, for referential integrity. | | `POST /2025-09/protection_levels/{id}/base_price` | Still writes to global levels, and we mirror the change to every site copy. Rejected for global levels after activation. | | `POST /2025-09/orders` | Global protection IDs rejected after activation. | | `PATCH /2025-09/orders/{id}` | As above. | | `POST /2025-09/orders/{id}/finalize` | As above. | | `POST /2025-09/orders/{id}/line_items` | As above. | | `PATCH /2025-09/orders/{id}/line_items/{line_item_id}` | As above. | | `GET /2025-09/orders/{id}/validate` | An order holding a global protection level stops validating after activation. | | `POST /2025-09/deals` | Global protection IDs rejected after activation. | | `PATCH /2025-09/deals/{id}` | As above. | All of this stays within API version `2025-09`. You don't need to upgrade to a later version to migrate. ## Listing protection levels By default the collection returns your global levels, unchanged: ```text theme={null} GET /2025-09/protection_levels ``` To reach your site levels, add `site_scoped=true`. This is the parameter we recommend building against. It returns every site level across your account, with the globals excluded — which is exactly what the unfiltered collection will return once you're activated: ```text theme={null} GET /2025-09/protection_levels?site_scoped=true ``` Use it as your default listing call during preparation. It gives you the whole site-level catalogue in one place, it's the closest thing to the post-activation response you can see today, and it doesn't require you to know your site IDs up front. If you'd rather narrow to particular sites, filter by site ID instead. Pass several, comma-separated, to cover more than one: ```text theme={null} GET /2025-09/protection_levels?site_id=site_3e6ffcb963dc4b49,site_76cc3340ef180e55 ``` Both `site_scoped` and `site_id` return `409 Conflict` until we've finished preparing your account. If you see `Site-scoped protection levels are unavailable until insurance preparation is complete for this operator`, your site copies don't exist yet — retry once we've confirmed your account is prepared. `site_scoped` is a migration parameter. It exists for this transition, so you won't find it in the API reference — this guide is its documentation. It works while your account is being prepared, and once you're activated it's no longer needed, because by then the plain call returns exactly what it returns today: ```text theme={null} GET /2025-09/protection_levels ``` So the shape of the migration is: send `site_scoped=true` now, drop it once you're switched over. `site_id` is not temporary. It's part of the API and is the filter to keep in your integration long term. The collection never mixes globals and site levels. ### The `site` field Protection levels have a `site`. It's `null` on a global level, and on a site level it gives you the site's ID: ```json theme={null} { "id": "plvl_e8f3ad0a07e47566", "cover_level": { "amount": 2000000, "currency": "GBP", "formatted": "£20,000.00" }, "prices": [ { "billing_period": "weekly", "price": { "amount": 999, "currency": "GBP", "formatted": "£9.99" }, "stripe_price_id": "price_123" } ], "site": { "id": "site_14b419f1096013f1" }, "stripe_product_id": "prod_Sj6us30iGDiaXO", "public": true } ``` `site` is expandable, so ask for the full site object when you need more than the ID: ```text theme={null} GET /2025-09/protection_levels?site_scoped=true&expand=site ``` Note the `stripe_product_id` too — each site copy has its own, which is why the duplicates appear in your Stripe account. ## Setting base prices Base prices are still written to global levels, and we mirror each change down to every site copy: ```text theme={null} POST /2025-09/protection_levels/{id}/base_price ``` Passing the ID of a site copy returns `404`. Site copies can't be priced independently during preparation — that's what keeps a customer's price the same whichever route they come through. After activation this reverses: site levels become writable and global levels are rejected. ## Creating and updating orders Right now, orders accept either a global protection ID or a protection ID belonging to the order's site. After activation, only the latter. ```text theme={null} POST /2025-09/orders ``` ```json theme={null} { "site": { "id": "site_14b419f1096013f1" }, "line_items": [ { "type": "protection", "quantity": 1, "price": { "amount": 2999 }, "item": { "id": "plvl_e8f3ad0a07e47566" } } ] } ``` Other order fields are omitted here for brevity. The protection level is the one listed above, and it belongs to the site the order is for — that pairing is the whole change. Two rules to code against: 1. **Resolve the site first.** Pick the protection level from the site the order is for, not from a stored global ID. 2. **A protection level from the wrong site is rejected**, the same as a global one, once you're activated. Deals follow the same rule. A `protection` line item on `POST /2025-09/deals` or `PATCH /2025-09/deals/{id}` must reference either an account-level protection level or one belonging to the deal's site. If your integration holds a hardcoded or cached protection level ID anywhere, that's the thing most likely to break. Find it now. ## Webhooks Event types and API version are unchanged: ```text theme={null} protection_level.created protection_level.updated ``` Which resource they describe follows your phase: | Phase | Global level saved | Site level saved | | ----------- | ------------------ | -------------------------- | | Preparation | Event fires | No event | | Activated | No event | Event fires, with its site | So during preparation you'll keep receiving events for globals exactly as you do now, and replication won't flood your endpoint. At activation the resource in the payload becomes a site level. ## What doesn't change * **Existing orders and subscriptions.** They keep the protection level IDs they were created with. We don't rewrite stored references, and historical resources keep returning the IDs you already have. * **Prices your customers pay.** Site copies start identical to their globals and are kept in sync until activation. * **Tax.** Not affected by this change. * **API version.** Everything here is `2025-09`. * **The Back Office and Storefront**, until activation. ## Getting help Email [api\_support@stora.co](mailto:api_support@stora.co) with your account name and what you're building. We're happy to review your integration changes before you ship them, and to switch your account over early if you're ready ahead of us. See where protection line items sit in the end-to-end order flow. Configure endpoints and check which API version your payloads use. # Naming third-party plugins Source: https://docs.stora.co/2025-09/guides/third-party-plugin-naming Rules for naming third-party plugins, extensions, and integrations that work with Stora. When you publish a plugin, extension, or packaged integration that works with Stora, use your own product name. You may reference Stora to describe compatibility, but your name must not imply that Stora built, owns, operates, or endorses the integration. These rules are part of the [partner integration requirements](/2025-09/guides/partner-integrations). Do not name your plugin `Stora`, start the name with `Stora`, or combine `Stora` with another word. Names such as `Stora Connect`, `StoraWP`, `Stora Booking`, and `Stora Lead Sync` are not allowed for third-party plugins. ## Naming rules ### Use a distinct product name Your plugin needs its own name. The product name should be distinct from Stora and from Stora product names. | Not allowed | Allowed | | ------------------------ | ---------------------------------- | | `Stora Sync` | `SpaceSync for Stora` | | `Stora Lead Sync` | `Move-In Automations for Stora` | | `Stora WordPress Plugin` | `Self Storage Lead Sync for Stora` | | `Stora Booking` | `Storage Site Connector for Stora` | ### Use Stora only to describe compatibility Use Stora in a referential phrase that explains what your plugin works with. Good patterns: * `[Your product name] for Stora` * `[Your product name] integrates with Stora` * `[Your product name] is compatible with Stora` * `[Your product name] works with Stora` Avoid names where Stora is the main product name, the first word, or part of a new combined word. ### Do not imply official status Do not use words that suggest Stora has built, certified, approved, or endorsed your plugin unless you have written approval from Stora. Avoid: * `official` * `certified` * `approved` * `by Stora` * `Stora partner plugin` * `Stora recommended` You can say that your plugin integrates with Stora. You cannot say or imply that your plugin is a Stora product. ### Keep your brand primary Your plugin listing, website, icon, screenshots, and admin UI should make your brand the primary identity. Stora can appear as the platform your plugin connects to, but not as the plugin's brand. Do not use the Stora logo as your plugin icon, marketplace icon, app logo, or primary visual identity unless Stora gives written approval. ## Describing your plugin Use plain language that explains what your integration does. Good examples: * `SpaceSync connects your WordPress site to Stora.` * `Move-In Automations for Stora syncs booking events into your marketing tools.` * `Self Storage Lead Sync for Stora sends website enquiries into your Stora account.` Avoid descriptions that imply Stora owns or operates the plugin: * `The Stora plugin for WordPress` * `Official Stora booking extension` * `Stora's lead sync tool` ## Required disclaimer Include a disclaimer anywhere operators may otherwise assume the plugin is operated by Stora, such as marketplace listings, setup pages, and public documentation. ```text theme={null} This plugin is provided by [Partner Name] and is not built, owned, or operated by Stora. ``` You may adapt the wording to match your product, but keep the meaning clear. ## Examples | Scenario | Use this | Do not use this | | --------------------------------- | -------------------------------- | ---------------------- | | WordPress plugin that syncs leads | `LeadBridge for Stora` | `Stora Leads` | | Booking widget extension | `Booking Widget for Stora` | `Stora Booking Widget` | | Analytics connector | `StoreTrack Analytics for Stora` | `Stora Analytics` | | Access control integration | `GateLink for Stora` | `Stora GateLink` | | General connector | `Connector for Stora` | `StoraConnect` | ## Review checklist Before you publish, check that: * Your plugin has its own distinct name. * The name does not start with `Stora`. * The name does not combine `Stora` with another word, abbreviation, or suffix. * Stora is used only to describe compatibility, such as `for Stora` or `integrates with Stora`. * Your listing does not imply the plugin is official, certified, approved, or built by Stora. * Your logo and icon use your own brand, not the Stora logo. * Your listing includes a clear third-party disclaimer. If your preferred name follows the pattern `[Your product name] for Stora`, it is usually a good starting point. ## If you are unsure Send us the proposed plugin name, marketplace listing title, icon, and short description before you publish. It is easier to fix naming before launch than after operators have installed the plugin. # Webhooks Source: https://docs.stora.co/2025-09/guides/webhooks Receive real-time notifications when events happen in Stora. Webhooks are HTTP callbacks that send real-time `POST` requests to your configured endpoints when specific events occur in Stora. When an event happens — such as an invoice being paid or a unit becoming occupied — Stora immediately notifies all endpoints subscribed to that event type. ## Getting started Use the [Developer Portal](/2025-09/guides/developer-portal) to manage webhook endpoints for a test operator or connected operator account. You can also use the [Webhook Endpoints API](/2025-09/api-reference/webhook-endpoints/create-a-webhook-endpoint) to register a publicly accessible HTTPS URL, the event types you want to subscribe to, and the API version. When you create an endpoint, Stora generates a secret key. Store it securely — you'll use it to verify incoming requests. Build a handler at your URL that verifies the signature, processes the event, and returns a `2xx` response. ## Payload structure Every webhook delivers a JSON payload with this structure: ```json theme={null} { "event": { "id": "evt_1234567890", "type": "invoice.paid", "api_version": "2025-09", "created_at": "2025-01-15T10:30:00Z", "data": { "invoice": { // ... full invoice resource } } } } ``` | Field | Description | | ------------------- | ------------------------------------------------------------------- | | `event.id` | Unique identifier for the event — use this for idempotency | | `event.type` | The event type (e.g. `invoice.paid`, `contact.created`) | | `event.api_version` | Matches your endpoint's API version — determines the data structure | | `event.created_at` | ISO 8601 timestamp of when the event occurred | | `event.data` | The resource that triggered the event, keyed by resource type | ## Headers Every webhook request includes these headers: | Header | Description | | -------------------- | ---------------------------------------------------------- | | `Content-Type` | `application/json` | | `User-Agent` | `Stora-Webhooks/1.0` | | `X-Stora-Signature` | HMAC signature for verification (see below) | | `X-Stora-Request-Id` | Unique ID for this delivery attempt — useful for debugging | ## Signature verification All webhook requests are signed using HMAC SHA256. Always verify the signature before processing. The signature is in the `X-Stora-Signature` header: ``` t={timestamp},v1={signature} ``` To verify: 1. Extract the timestamp (`t`) and signature (`v1`) from the header 2. Reconstruct the signed payload: `{timestamp}.{raw_request_body}` 3. Compute the HMAC SHA256 using your endpoint's secret key 4. Compare the computed signature with `v1` 5. Optionally, check the timestamp is recent to prevent replay attacks ```ruby Ruby theme={null} def verify_webhook_signature(request_body, signature_header, secret) parts = signature_header.split(',').map { |p| p.split('=').last } timestamp = parts[0] signature = parts[1] signed_payload = "#{timestamp}.#{request_body}" computed = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload) computed == signature end ``` ```python Python theme={null} import hmac import hashlib def verify_webhook_signature(request_body, signature_header, secret): parts = signature_header.split(',') timestamp = parts[0].split('=')[1] signature = parts[1].split('=')[1] signed_payload = f"{timestamp}.{request_body}" computed = hmac.new( secret.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() return hmac.compare_digest(computed, signature) ``` ```php PHP theme={null} function verifyWebhookSignature(string $requestBody, string $signatureHeader, string $secret): bool { $parts = explode(',', $signatureHeader); $timestamp = explode('=', $parts[0])[1]; $signature = explode('=', $parts[1])[1]; $signedPayload = "{$timestamp}.{$requestBody}"; $computed = hash_hmac('sha256', $signedPayload, $secret); return hash_equals($computed, $signature); } ``` ```javascript Node.js theme={null} const crypto = require('crypto'); function verifyWebhookSignature(requestBody, signatureHeader, secret) { const parts = signatureHeader.split(','); const timestamp = parts[0].split('=')[1]; const signature = parts[1].split('=')[1]; const signedPayload = `${timestamp}.${requestBody}`; const computed = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(computed), Buffer.from(signature) ); } ``` ## Retries Stora automatically retries failed deliveries up to 6 times: | Attempt | Delay | | --------- | ---------- | | 1st retry | 1 minute | | 2nd retry | 5 minutes | | 3rd retry | 30 minutes | | 4th retry | 2 hours | | 5th retry | 6 hours | | 6th retry | 12 hours | A delivery is retried when your endpoint returns a non-`2xx` status code, a network error occurs, or the request times out (20-second limit). After 6 failed attempts, the delivery is marked as failed. If the endpoint is deleted or disabled before a scheduled retry, pending retries are cancelled. ## Best practices Return a `2xx` response as quickly as possible — even if you process the event asynchronously. This prevents unnecessary retries. * **Implement idempotency** — use `event.id` to ensure you don't process the same event twice. Store processed event IDs and check before processing. * **Process asynchronously** — for time-consuming operations, queue the webhook for background processing after returning a success response. * **Log the request ID** — use `X-Stora-Request-Id` to correlate retry attempts when debugging. * **Validate signatures** — always verify the HMAC signature before processing. * **Monitor your endpoint** — extended downtime may exhaust all retry attempts. ## Available events | Event | Description | | ----------------------------------------------------------------------- | ------------------------------------------------ | | [`contact.churned`](/2025-09/api-reference/webhooks/contactchurned) | Triggered when a contact churns. | | [`contact.converted`](/2025-09/api-reference/webhooks/contactconverted) | Triggered when a contact converts to a customer. | | [`contact.created`](/2025-09/api-reference/webhooks/contactcreated) | Triggered when a contact is created. | | [`contact.updated`](/2025-09/api-reference/webhooks/contactupdated) | Triggered when the contact is updated. | | Event | Description | | --------------------------------------------------------------------- | ------------------------------------- | | [`contract.created`](/2025-09/api-reference/webhooks/contractcreated) | Triggered when a contract is created. | | [`contract.signed`](/2025-09/api-reference/webhooks/contractsigned) | Triggered when a contract is signed. | | Event | Description | | ----------------------------------------------------------------- | ------------------------------------- | | [`coupon.created`](/2025-09/api-reference/webhooks/couponcreated) | Triggered when a coupon is created. | | [`coupon.updated`](/2025-09/api-reference/webhooks/couponupdated) | Triggered when the coupon is updated. | | Event | Description | | --------------------------------------------------------------------------- | ---------------------------------------- | | [`credit_note.created`](/2025-09/api-reference/webhooks/credit_notecreated) | Triggered when a new credit is created. | | [`credit_note.updated`](/2025-09/api-reference/webhooks/credit_noteupdated) | Triggered when a credit note is updated. | | Event | Description | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`deal.created`](/2025-09/api-reference/webhooks/dealcreated) | Triggered when a deal is created. | | [`deal.lost`](/2025-09/api-reference/webhooks/deallost) | Triggered when a deal is lost. | | [`deal.reopened`](/2025-09/api-reference/webhooks/dealreopened) | Triggered when a deal is reopened | | [`deal.stage_changed`](/2025-09/api-reference/webhooks/dealstage_changed) | Triggered when a deal's stage changes. Fires on every stage transition, including the first time a deal enters its initial stage on creation. In that case, this event is emitted alongside `deal.created`. | | [`deal.updated`](/2025-09/api-reference/webhooks/dealupdated) | Triggered when a deal is updated. | | [`deal.won`](/2025-09/api-reference/webhooks/dealwon) | Triggered when a deal is won. | | Event | Description | | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | [`identity_verification.cancelled`](/2025-09/api-reference/webhooks/identity_verificationcancelled) | Triggered when an identity verification is cancelled | | [`identity_verification.failed`](/2025-09/api-reference/webhooks/identity_verificationfailed) | Triggered when an identity verification failed | | [`identity_verification.processing`](/2025-09/api-reference/webhooks/identity_verificationprocessing) | Triggered when an identity verification is processing | | [`identity_verification.succeeded`](/2025-09/api-reference/webhooks/identity_verificationsucceeded) | Triggered when an identity verification succeeded | | Event | Description | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | [`invoice.created`](/2025-09/api-reference/webhooks/invoicecreated) | Triggered when a new invoice is created. | | [`invoice.finalized`](/2025-09/api-reference/webhooks/invoicefinalized) | Triggered when an invoice is finalized. | | [`invoice.marked_uncollectible`](/2025-09/api-reference/webhooks/invoicemarked_uncollectible) | Triggered when an invoice is marked as uncollectible. | | [`invoice.overdue`](/2025-09/api-reference/webhooks/invoiceoverdue) | Triggered when an invoice becomes overdue for payment. | | [`invoice.paid`](/2025-09/api-reference/webhooks/invoicepaid) | Triggered when an invoice is mark as paid. | | [`invoice.payment_failed`](/2025-09/api-reference/webhooks/invoicepayment_failed) | Triggered when an invoice payment fails. | | [`invoice.updated`](/2025-09/api-reference/webhooks/invoiceupdated) | Triggered when an invoice is updated. | | Event | Description | | ------------------------------------------------------------- | --------------------------------- | | [`note.created`](/2025-09/api-reference/webhooks/notecreated) | Triggered when a note is created. | | [`note.updated`](/2025-09/api-reference/webhooks/noteupdated) | Triggered when a note is updated. | | Event | Description | | ------------------------------------------------------------------- | ----------------------------------------------------------- | | [`order.abandoned`](/2025-09/api-reference/webhooks/orderabandoned) | Triggered when an order is abandoned. | | [`order.completed`](/2025-09/api-reference/webhooks/ordercompleted) | Triggered when the order is completed. | | [`order.created`](/2025-09/api-reference/webhooks/ordercreated) | Triggered when a new order is created. | | [`order.finalized`](/2025-09/api-reference/webhooks/orderfinalized) | Triggered when a new order is finalized (ready to be paid). | | Event | Description | | ------------------------------------------------------------------------------------- | --------------------------------------------- | | [`protection_level.created`](/2025-09/api-reference/webhooks/protection_levelcreated) | Triggered when a protection level is created. | | [`protection_level.updated`](/2025-09/api-reference/webhooks/protection_levelupdated) | Triggered when a protection level is updated. | | Event | Description | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [`subscription.cancelled`](/2025-09/api-reference/webhooks/subscriptioncancelled) | Triggered when the subscription is cancelled. | | [`subscription.created`](/2025-09/api-reference/webhooks/subscriptioncreated) | Triggered when the subscription is created. | | [`subscription.ended`](/2025-09/api-reference/webhooks/subscriptionended) | Triggered when the subscription is ended. | | [`subscription.resumed`](/2025-09/api-reference/webhooks/subscriptionresumed) | Triggered when a cancellation is undone and the subscription is resumed. | | [`subscription.started`](/2025-09/api-reference/webhooks/subscriptionstarted) | Triggered when the subscription is started. | | Event | Description | | ----------------------------------------------------------------- | -------------------------------------------- | | [`task.completed`](/2025-09/api-reference/webhooks/taskcompleted) | Triggered when the task is completed. | | [`task.created`](/2025-09/api-reference/webhooks/taskcreated) | Triggered when a task is created. | | [`task.reopened`](/2025-09/api-reference/webhooks/taskreopened) | Triggered when a completed task is reopened. | | [`task.updated`](/2025-09/api-reference/webhooks/taskupdated) | Triggered when a task is updated. | | Event | Description | | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | [`tenancy.auto_reservation.failed`](/2025-09/api-reference/webhooks/tenancyauto_reservationfailed) | Triggered when no requested units can be automatically reserved for a tenancy. | | [`tenancy.auto_reservation.partially_succeeded`](/2025-09/api-reference/webhooks/tenancyauto_reservationpartially_succeeded) | Triggered when some, but not all, requested units are automatically reserved for a tenancy. | | [`tenancy.auto_reservation.succeeded`](/2025-09/api-reference/webhooks/tenancyauto_reservationsucceeded) | Triggered when all requested units are automatically reserved for a tenancy. | | [`tenancy.created`](/2025-09/api-reference/webhooks/tenancycreated) | Triggered when a tenancy is created. | | [`tenancy.started`](/2025-09/api-reference/webhooks/tenancystarted) | Triggered when the tenancy is started. | | Event | Description | | --------------------------------------------------------------------- | ------------------------------------------ | | [`unit.available`](/2025-09/api-reference/webhooks/unitavailable) | Triggered when a unit is made available. | | [`unit.created`](/2025-09/api-reference/webhooks/unitcreated) | Triggered when a unit is created. | | [`unit.deallocated`](/2025-09/api-reference/webhooks/unitdeallocated) | Triggered when a unit is deallocated. | | [`unit.occupied`](/2025-09/api-reference/webhooks/unitoccupied) | Triggered when a unit is occupied. | | [`unit.overlocked`](/2025-09/api-reference/webhooks/unitoverlocked) | Triggered when a unit is overlocked. | | [`unit.repossessed`](/2025-09/api-reference/webhooks/unitrepossessed) | Triggered when a unit is repossessed. | | [`unit.reserved`](/2025-09/api-reference/webhooks/unitreserved) | Triggered when a unit is reserved. | | [`unit.unavailable`](/2025-09/api-reference/webhooks/unitunavailable) | Triggered when a unit is made unavailable. | | [`unit.updated`](/2025-09/api-reference/webhooks/unitupdated) | Triggered when a unit is updated. | | Event | Description | | ----------------------------------------------------------------------- | -------------------------------------- | | [`unit_type.created`](/2025-09/api-reference/webhooks/unit_typecreated) | Triggered when a unit type is created. | | [`unit_type.updated`](/2025-09/api-reference/webhooks/unit_typeupdated) | Triggered when a unit type is updated. | For full payload schemas, see the [Webhooks section in the API reference](/2025-09/api-reference/webhooks/). # Delete an Event Source: https://docs.stora.co/2025-09/api-reference/timeline-events/delete-an-event /2025-09/openapi.json delete /2025-09/timeline/events/{event_id} Permanently delete a timeline event. Required authorization scope: `public.timeline_event:write` # Update an Event Source: https://docs.stora.co/2025-09/api-reference/timeline-events/update-an-event /2025-09/openapi.json patch /2025-09/timeline/events/{event_id} Update an existing timeline event. Required authorization scope: `public.timeline_event:write` # List all Sources Source: https://docs.stora.co/2025-09/api-reference/timeline-sources/list-all-sources /2025-09/openapi.json get /2025-09/timeline/sources Retrieve a list of all sources (both global and operator-specific). Required authorization scope: `public.timeline_source:read` # Show a Source Source: https://docs.stora.co/2025-09/api-reference/timeline-sources/show-a-source /2025-09/openapi.json get /2025-09/timeline/sources/{source_id} Retrieve a specific source by its ID. Required authorization scope: `public.timeline_source:read` # List all Templates Source: https://docs.stora.co/2025-09/api-reference/timeline-templates/list-all-templates /2025-09/openapi.json get /2025-09/timeline/templates Retrieve a list of all templates. Templates define the message format for timeline events. The `message` field contains the default English Liquid template that will be translated. The `variables` object describes which variables are available to render the complete message — `predefined` are predefined system variables, `custom` are user-defined. Variables are not enforced, as we do not want to block events that have incomplete data. Required authorization scope: `public.timeline_template:read` # Show a Template Source: https://docs.stora.co/2025-09/api-reference/timeline-templates/show-a-template /2025-09/openapi.json get /2025-09/timeline/templates/{template_id} Retrieve a specific template by its ID. Required authorization scope: `public.timeline_template:read` # List all Unit Allocations Source: https://docs.stora.co/2025-09/api-reference/unit-allocations/list-all-unit-allocations /2025-09/openapi.json get /2025-09/unit_allocations Retrieve a list of all unit allocations. Required authorization scope: `public.unit_allocation:read` # Show a Unit Allocation Source: https://docs.stora.co/2025-09/api-reference/unit-allocations/show-a-unit-allocation /2025-09/openapi.json get /2025-09/unit_allocations/{unit_allocation_id} Retrieve a specific unit allocation by its ID. Required authorization scope: `public.unit_allocation:read` # Create Base Price for the Unit Type Source: https://docs.stora.co/2025-09/api-reference/unit-types/create-base-price-for-the-unit-type /2025-09/openapi.json post /2025-09/unit_types/{unit_type_id}/base_price Set base prices for the selected unit type. Required authorization scope: `public.unit_type:write` # List all Unit Types Source: https://docs.stora.co/2025-09/api-reference/unit-types/list-all-unit-types /2025-09/openapi.json get /2025-09/unit_types Retrieve a list of all unit types. Required authorization scope: `public.unit_type:read` # Show a Unit Type Source: https://docs.stora.co/2025-09/api-reference/unit-types/show-a-unit-type /2025-09/openapi.json get /2025-09/unit_types/{unit_type_id} Retrieve a specific unit type by its ID. Required authorization scope: `public.unit_type:read` # Deallocate a Unit Source: https://docs.stora.co/2025-09/api-reference/units/deallocate-a-unit /2025-09/openapi.json post /2025-09/units/{unit_id}/deallocate Deallocate a unit. **Requirements:** - The unit status must be one of: `reserved`, `occupied`, `overlock`, `repossessed` Required authorization scope: `public.unit:write` # Grant Access to a Unit Source: https://docs.stora.co/2025-09/api-reference/units/grant-access-to-a-unit /2025-09/openapi.json post /2025-09/units/{unit_id}/grant_access Grant immediate access to the provided tenancy, transitioning the unit to `occupied` status. - If the unit is `reserved`, access is granted for the existing reservation. The unit must be reserved for the same tenancy provided in the request. Required authorization scope: `public.unit:write` # List all Units Source: https://docs.stora.co/2025-09/api-reference/units/list-all-units /2025-09/openapi.json get /2025-09/units Retrieve a list of all units. Required authorization scope: `public.unit:read` # Overlock Units by Contact Source: https://docs.stora.co/2025-09/api-reference/units/overlock-units-by-contact /2025-09/openapi.json post /2025-09/units/overlock Overlock all occupied units for a contact. **Requirements:** - The contact must have occupied units that can be overlocked Required authorization scope: `public.unit:write` # Remove Overlock from Units by Contact Source: https://docs.stora.co/2025-09/api-reference/units/remove-overlock-from-units-by-contact /2025-09/openapi.json post /2025-09/units/remove_overlock Remove overlock from all overlocked units for a contact. **Requirements:** - The contact must have overlocked units that can have overlock removed Required authorization scope: `public.unit:write` # Reserve a Unit Source: https://docs.stora.co/2025-09/api-reference/units/reserve-a-unit /2025-09/openapi.json post /2025-09/units/{unit_id}/reserve Reserve a unit for a tenancy. The unit will be reserved for the specified tenancy until the move-in date, at which point access can be granted. **Requirements:** - The unit must be in "available" status - The tenancy must not have started yet (move-in date must be in the future) Required authorization scope: `public.unit:write` # Show a Unit Source: https://docs.stora.co/2025-09/api-reference/units/show-a-unit /2025-09/openapi.json get /2025-09/units/{unit_id} Retrieve a specific unit by its ID. Required authorization scope: `public.unit:read` # Create a Webhook Endpoint Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/create-a-webhook-endpoint /2025-09/openapi.json post /2025-09/webhook_endpoints Create a new webhook endpoint to receive event notifications. Required authorization scope: `public.webhook_endpoint:write` # Delete a Webhook Endpoint Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/delete-a-webhook-endpoint /2025-09/openapi.json delete /2025-09/webhook_endpoints/{webhook_endpoint_id} Delete a webhook endpoint by its ID. Required authorization scope: `public.webhook_endpoint:write` # Disable a Webhook Endpoint Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/disable-a-webhook-endpoint /2025-09/openapi.json post /2025-09/webhook_endpoints/{webhook_endpoint_id}/disable Disable a webhook endpoint. Required authorization scope: `public.webhook_endpoint:write` # Enable a Webhook Endpoint Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/enable-a-webhook-endpoint /2025-09/openapi.json post /2025-09/webhook_endpoints/{webhook_endpoint_id}/enable Enable a webhook endpoint. Required authorization scope: `public.webhook_endpoint:write` # List all Webhook Endpoints Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/list-all-webhook-endpoints /2025-09/openapi.json get /2025-09/webhook_endpoints Retrieve a list of all webhook endpoints. Required authorization scope: `public.webhook_endpoint:read` # Show a Webhook Endpoint Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/show-a-webhook-endpoint /2025-09/openapi.json get /2025-09/webhook_endpoints/{webhook_endpoint_id} Retrieve the details of a specific webhook endpoint by its ID. Required authorization scope: `public.webhook_endpoint:read` # Update a Webhook Endpoint Source: https://docs.stora.co/2025-09/api-reference/webhook-endpoints/update-a-webhook-endpoint /2025-09/openapi.json patch /2025-09/webhook_endpoints/{webhook_endpoint_id} Update an existing webhook endpoint. Required authorization scope: `public.webhook_endpoint:write` # contact.churned Source: https://docs.stora.co/2025-09/api-reference/webhooks/contactchurned /2025-09/openapi.json webhook contact.churned Triggered when a contact churns. # contact.converted Source: https://docs.stora.co/2025-09/api-reference/webhooks/contactconverted /2025-09/openapi.json webhook contact.converted Triggered when a contact converts to a customer. # contact.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/contactcreated /2025-09/openapi.json webhook contact.created Triggered when a contact is created. # contact.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/contactupdated /2025-09/openapi.json webhook contact.updated Triggered when the contact is updated. # contract.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/contractcreated /2025-09/openapi.json webhook contract.created Triggered when a contract is created. # contract.signed Source: https://docs.stora.co/2025-09/api-reference/webhooks/contractsigned /2025-09/openapi.json webhook contract.signed Triggered when a contract is signed. # coupon.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/couponcreated /2025-09/openapi.json webhook coupon.created Triggered when a coupon is created. # coupon.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/couponupdated /2025-09/openapi.json webhook coupon.updated Triggered when the coupon is updated. # credit_note.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/credit_notecreated /2025-09/openapi.json webhook credit_note.created Triggered when a new credit is created. # credit_note.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/credit_noteupdated /2025-09/openapi.json webhook credit_note.updated Triggered when a credit note is updated. # deal.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/dealcreated /2025-09/openapi.json webhook deal.created Triggered when a deal is created. # deal.lost Source: https://docs.stora.co/2025-09/api-reference/webhooks/deallost /2025-09/openapi.json webhook deal.lost Triggered when a deal is lost. # deal.reopened Source: https://docs.stora.co/2025-09/api-reference/webhooks/dealreopened /2025-09/openapi.json webhook deal.reopened Triggered when a deal is reopened # deal.stage_changed Source: https://docs.stora.co/2025-09/api-reference/webhooks/dealstage_changed /2025-09/openapi.json webhook deal.stage_changed Triggered when a deal's stage changes. Fires on every stage transition, including the first time a deal enters its initial stage on creation. In that case, this event is emitted alongside `deal.created`. # deal.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/dealupdated /2025-09/openapi.json webhook deal.updated Triggered when a deal is updated. # deal.won Source: https://docs.stora.co/2025-09/api-reference/webhooks/dealwon /2025-09/openapi.json webhook deal.won Triggered when a deal is won. # identity_verification.cancelled Source: https://docs.stora.co/2025-09/api-reference/webhooks/identity_verificationcancelled /2025-09/openapi.json webhook identity_verification.cancelled Triggered when an identity verification is cancelled # identity_verification.failed Source: https://docs.stora.co/2025-09/api-reference/webhooks/identity_verificationfailed /2025-09/openapi.json webhook identity_verification.failed Triggered when an identity verification failed # identity_verification.processing Source: https://docs.stora.co/2025-09/api-reference/webhooks/identity_verificationprocessing /2025-09/openapi.json webhook identity_verification.processing Triggered when an identity verification is processing # identity_verification.succeeded Source: https://docs.stora.co/2025-09/api-reference/webhooks/identity_verificationsucceeded /2025-09/openapi.json webhook identity_verification.succeeded Triggered when an identity verification succeeded # invoice.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoicecreated /2025-09/openapi.json webhook invoice.created Triggered when a new invoice is created. # invoice.finalized Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoicefinalized /2025-09/openapi.json webhook invoice.finalized Triggered when an invoice is finalized. # invoice.marked_uncollectible Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoicemarked_uncollectible /2025-09/openapi.json webhook invoice.marked_uncollectible Triggered when an invoice is marked as uncollectible. # invoice.overdue Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoiceoverdue /2025-09/openapi.json webhook invoice.overdue Triggered when an invoice becomes overdue for payment. # invoice.paid Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoicepaid /2025-09/openapi.json webhook invoice.paid Triggered when an invoice is mark as paid. # invoice.payment_failed Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoicepayment_failed /2025-09/openapi.json webhook invoice.payment_failed Triggered when an invoice payment fails. # invoice.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/invoiceupdated /2025-09/openapi.json webhook invoice.updated Triggered when an invoice is updated. # note.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/notecreated /2025-09/openapi.json webhook note.created Triggered when a note is created. # note.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/noteupdated /2025-09/openapi.json webhook note.updated Triggered when a note is updated. # order.abandoned Source: https://docs.stora.co/2025-09/api-reference/webhooks/orderabandoned /2025-09/openapi.json webhook order.abandoned Triggered when an order is abandoned. # order.completed Source: https://docs.stora.co/2025-09/api-reference/webhooks/ordercompleted /2025-09/openapi.json webhook order.completed Triggered when the order is completed. # order.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/ordercreated /2025-09/openapi.json webhook order.created Triggered when a new order is created. # order.finalized Source: https://docs.stora.co/2025-09/api-reference/webhooks/orderfinalized /2025-09/openapi.json webhook order.finalized Triggered when a new order is finalized (ready to be paid). # protection_level.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/protection_levelcreated /2025-09/openapi.json webhook protection_level.created Triggered when a protection level is created. # protection_level.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/protection_levelupdated /2025-09/openapi.json webhook protection_level.updated Triggered when a protection level is updated. # subscription.cancelled Source: https://docs.stora.co/2025-09/api-reference/webhooks/subscriptioncancelled /2025-09/openapi.json webhook subscription.cancelled Triggered when the subscription is cancelled. # subscription.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/subscriptioncreated /2025-09/openapi.json webhook subscription.created Triggered when the subscription is created. # subscription.ended Source: https://docs.stora.co/2025-09/api-reference/webhooks/subscriptionended /2025-09/openapi.json webhook subscription.ended Triggered when the subscription is ended. # subscription.resumed Source: https://docs.stora.co/2025-09/api-reference/webhooks/subscriptionresumed /2025-09/openapi.json webhook subscription.resumed Triggered when a cancellation is undone and the subscription is resumed. # subscription.started Source: https://docs.stora.co/2025-09/api-reference/webhooks/subscriptionstarted /2025-09/openapi.json webhook subscription.started Triggered when the subscription is started. # task.completed Source: https://docs.stora.co/2025-09/api-reference/webhooks/taskcompleted /2025-09/openapi.json webhook task.completed Triggered when the task is completed. # task.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/taskcreated /2025-09/openapi.json webhook task.created Triggered when a task is created. # task.reopened Source: https://docs.stora.co/2025-09/api-reference/webhooks/taskreopened /2025-09/openapi.json webhook task.reopened Triggered when a completed task is reopened. # task.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/taskupdated /2025-09/openapi.json webhook task.updated Triggered when a task is updated. # tenancy.auto_reservation.failed Source: https://docs.stora.co/2025-09/api-reference/webhooks/tenancyauto_reservationfailed /2025-09/openapi.json webhook tenancy.auto_reservation.failed Triggered when no requested units can be automatically reserved for a tenancy. # tenancy.auto_reservation.partially_succeeded Source: https://docs.stora.co/2025-09/api-reference/webhooks/tenancyauto_reservationpartially_succeeded /2025-09/openapi.json webhook tenancy.auto_reservation.partially_succeeded Triggered when some, but not all, requested units are automatically reserved for a tenancy. # tenancy.auto_reservation.succeeded Source: https://docs.stora.co/2025-09/api-reference/webhooks/tenancyauto_reservationsucceeded /2025-09/openapi.json webhook tenancy.auto_reservation.succeeded Triggered when all requested units are automatically reserved for a tenancy. # tenancy.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/tenancycreated /2025-09/openapi.json webhook tenancy.created Triggered when a tenancy is created. # tenancy.started Source: https://docs.stora.co/2025-09/api-reference/webhooks/tenancystarted /2025-09/openapi.json webhook tenancy.started Triggered when the tenancy is started. # unit_type.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/unit_typecreated /2025-09/openapi.json webhook unit_type.created Triggered when a unit type is created. # unit_type.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/unit_typeupdated /2025-09/openapi.json webhook unit_type.updated Triggered when a unit type is updated. # unit.available Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitavailable /2025-09/openapi.json webhook unit.available Triggered when a unit is made available. # unit.created Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitcreated /2025-09/openapi.json webhook unit.created Triggered when a unit is created. # unit.deallocated Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitdeallocated /2025-09/openapi.json webhook unit.deallocated Triggered when a unit is deallocated. # unit.occupied Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitoccupied /2025-09/openapi.json webhook unit.occupied Triggered when a unit is occupied. # unit.overlocked Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitoverlocked /2025-09/openapi.json webhook unit.overlocked Triggered when a unit is overlocked. # unit.repossessed Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitrepossessed /2025-09/openapi.json webhook unit.repossessed Triggered when a unit is repossessed. # unit.reserved Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitreserved /2025-09/openapi.json webhook unit.reserved Triggered when a unit is reserved. # unit.unavailable Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitunavailable /2025-09/openapi.json webhook unit.unavailable Triggered when a unit is made unavailable. # unit.updated Source: https://docs.stora.co/2025-09/api-reference/webhooks/unitupdated /2025-09/openapi.json webhook unit.updated Triggered when a unit is updated.