# 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` # 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` # 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. # 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` # 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` # 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` # 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` # 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` # 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` # 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` # 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 all protection levels. Required authorization scope: `public.protection_level:read` # 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` # 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` # 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` # 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` # 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, or manually via the API. ### Auto-reservation If the operator has auto-reservation enabled, Stora automatically reserves a unit when a storefront booking completes. It selects an available unit of the correct type and creates the allocation. If no units of the correct type are available at the time of booking, the tenancy is created without an allocation and the operator is notified to assign a unit manually. Auto-reservation only applies to bookings made through the operator's storefront. Orders created via the API do not trigger auto-reservation — your integration controls when and how units are allocated. ### 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. # 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 Stora account with an active operator * A staff member with permission to manage API settings * Access to BackOffice → Settings → Public API Stora doesn't currently offer self-service sandbox access. Contact us directly if you need a test environment. ## 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 directly in BackOffice. Use it immediately in requests — no token exchange needed. **Good for:** scripts, internal tools, quick automations, exploring the API. | | | | -------- | ------------------------------------------- | | Setup | Instant — generate in BackOffice | | 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 an application in BackOffice | | 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 BackOffice → Settings → Public API and click **Generate Access Token**. 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 BackOffice → Settings → Public API and click **Create Application**. 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 the Stora BackOffice 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) adds an extra layer of security to the Authorization Code flow. It's optional but recommended for: * Distributed plugins (e.g. WordPress, Shopify) where the client secret is in distributed source code * Mobile or desktop applications where secrets can't be stored securely * Single-page applications where source code is visible in the browser To use PKCE, 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 ``` Then 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.order:read` | Order (Read) | Allow to read all orders | | `public.order:write` | Order (Write) | Allow to manage orders | | `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. ```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: Order App->>API: Finalize order API-->>App: 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"], "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: * **`prices`** — an array with an entry per billing period. All amounts are in the smallest currency unit (pence for GBP, cents for USD). Use the `formatted` field for display. * **`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. ### 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. ### 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", "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9500 }, "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", "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9500 }, "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. * **`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 response includes a `payment_url`. Redirect the customer there to collect their payment details. * **`cancel_redirect_url`** (required) — where the customer lands if they abandon checkout. * **`payment_url`** — redirect the customer here. Once finalized, the order is locked. If the customer needs to make changes, create a new order. ### Create and finalize in one step For single-page checkouts, include the `finalize` field in the create 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", "line_items": [ { "type": "unit_type", "quantity": 1, "price": { "amount": 9500 }, "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" } }' ``` ## Step 5: Handle completion After payment, Stora processes the booking automatically. 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. If the operator has auto-reservation enabled, Stora reserves an available unit automatically. Otherwise, the operator assigns one manually. Your application can also handle this — listen for `tenancy.created` and use the [reserve endpoint](/2025-09/api-reference/units/reserve-a-unit) to allocate a specific unit. 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 tenancy/subscription are created | | `tenancy.created` | A tenancy is created for the completed order | | `subscription.created` | A subscription is created for the completed order | | `unit.reserved` | A unit has been reserved | `order.completed` is the primary signal that the booking is done. 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`. The `price.amount` should come from the relevant resource's `prices` array for the selected billing period. 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. ### 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. Disable either per order: ```json theme={null} { "emails": { "booking_confirmation": false, "move_in_day": false } } ``` ### 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** * **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 (beta)**: 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) * Base prices on [unit types](/2025-09/api-reference/unit-types/list-all-unit-types), [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. ### 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 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. # 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. ## 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. 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 * **Build partner integrations** — use the Authorization Code flow to act on behalf of operators # 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** | Self-serve in BackOffice | Managed by Stora | | **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 Contact us with details of what your integration does, which Stora resources it needs, and the problem it solves for operators. We'll set up OAuth 2.0 testing credentials with a set of selected test operators. You build your integration using the [Authorization Code flow](/2025-09/guides/authentication#option-c-oauth-2-0-%E2%80%94-authorization-code) and our test environment. Submit your integration for review. We'll check your use of scopes, External Events implementation, error handling, and how you present the connection flow to operators. Once approved, we'll create production credentials scoped to the minimum permissions your integration needs. Your integration becomes available to all operators. 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. ### 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. * Consider using [PKCE](/2025-09/guides/authentication#pkce-optional) if your integration runs in a distributed or client-side environment. ### 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). Strategic partners may negotiate higher limits. 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, your test credentials will have all scopes so you can explore freely. 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? [Contact us](https://stora.co/contact) with: * What your integration does * Which Stora resources it needs access to * The problem it solves for self-storage operators We'll evaluate the fit and get you set up with testing credentials. # 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 `{}`. # 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 [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.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.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.paid`](/2025-09/api-reference/webhooks/invoicepaid) | Triggered when an invoice is mark as paid. | | [`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.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.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.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/). # 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` # 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.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.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.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.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.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.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.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.