Cancellation & Reschedule Policies
Policies are the rules that decide whether a booking can be cancelled or rescheduled, and how much the customer is refunded when they do. They live at the supplier level and can be scoped to specific products, experiences, categories, or "all".
Tiered policies (Phase 1.9+)โ
A policy can carry a tier ladder โ multiple refund thresholds keyed by how many hours before the booking start the customer cancels. The matching tier is the highest applies_after_hours whose value is less than or equal to the actual hours-before-cancellation. Cancellations that miss every tier (e.g. cancelling 1 hour before when the lowest tier is "24h+ before") receive 0% refund.
Each tier has its own optional refund_method. The policy-level refund_method is used as the default when the tier doesn't specify one. A non_refundable_fee (per-policy fixed amount) can be deducted from any refund.
Legacy single-tier fields (refund_percentage, time_before_booking, time_before_booking_unit, applies_up_to) are retained on the response for backwards compatibility with integrators that pre-date tiers. New clients should read tiers[] and ignore the legacy fields.
Endpointsโ
| Method | Path | Scope |
|---|---|---|
GET | /v1/settings/policies | policies:read or settings:read |
GET | /v1/settings/policies/applicable | policies:read or settings:read |
GET | /v1/settings/policies/{id} | policies:read or settings:read |
POST | /v1/settings/policies | policies:write |
PUT | /v1/settings/policies/{id} | policies:write |
DELETE | /v1/settings/policies/{id} | policies:write |
List Policiesโ
GET /v1/settings/policies
Response (200 OK)โ
{
"data": [
{
"id": 1,
"name": "Flexible",
"description": "Full refund up to 7 days before, 50% up to 24h before.",
"applies_to": "all-experiences",
"allow_reschedule": true,
"reschedule_cutoff_hours": 48,
"allow_cancellations": true,
"tiers": [
{ "id": 11, "applies_after_hours": 168, "human_threshold": "1 week", "refund_percentage": 100.0, "refund_method": "original_payment" },
{ "id": 12, "applies_after_hours": 24, "human_threshold": "1 day", "refund_percentage": 50.0, "refund_method": "original_payment" },
{ "id": 13, "applies_after_hours": 0, "human_threshold": "Within window", "refund_percentage": 0.0, "refund_method": "original_payment" }
],
"non_refundable_fee": 5.00,
"refund_method": "original_payment",
"currency": "USD",
"refund_percentage": null,
"time_before_booking": null,
"time_before_booking_unit": null,
"applies_up_to": "cutOff",
"applications": [],
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-04-01T00:00:00+00:00"
}
],
"meta": { "request_id": "req_abc123", "timestamp": "2026-04-22T12:00:00+00:00" }
}
Response Fieldsโ
| Field | Type | Description |
|---|---|---|
id | integer | Policy ID |
name | string | Policy name |
description | string|null | Customer-facing description |
applies_to | string | Scope. One of: product, category, experience, experience-category, all-products, all-experiences, all-products-experiences, all |
allow_reschedule | boolean | Whether rescheduling is allowed |
reschedule_cutoff_hours | integer|null | Hours before the booking start that a customer may still self-reschedule. null = any time before start. Customer self-service only โ staff are never blocked. |
allow_cancellations | boolean | Whether cancellations are allowed |
tiers | array | Refund tiers, sorted descending by applies_after_hours (most-generous first) |
tiers[].id | integer | Tier ID |
tiers[].applies_after_hours | integer | Lead-time threshold (hours before booking start) |
tiers[].human_threshold | string | Human-readable label e.g. "1 week", "24 hours", "Within window" |
tiers[].refund_percentage | float | 0โ100 |
tiers[].refund_method | string | original_payment or store_credit |
non_refundable_fee | float | Fixed amount deducted from any refund (default 0) |
refund_method | string|null | Default refund method when a tier doesn't set its own |
currency | string|null | ISO 4217 currency code |
applications | array | Items this policy is scoped to (see below) |
created_at, updated_at | string | ISO 8601 datetimes |
refund_percentage (legacy) | float|null | Single-tier % โ deprecated; use tiers |
time_before_booking (legacy) | integer|null | Single-tier threshold value โ deprecated |
time_before_booking_unit (legacy) | string|null | Single-tier threshold unit (hours, days, weeks) โ deprecated |
applies_up_to (legacy) | string|null | cutOff or any โ deprecated |
Application Objectsโ
| Field | Type | Description |
|---|---|---|
type | string | Application type: product, category, experience, experience-category, all-products, all-experiences, all-products-experiences, all |
product_id, product_name | integer, string | Set when type is product |
category_id, category_name | integer, string | Set when type is category |
experience_id, experience_name | integer, string | Set when type is experience |
experience_category_id, experience_category_name | integer, string | Set when type is experience-category |
Get Applicable Policy For Itemโ
Resolves which policy will apply to a single product / experience / bundle / store product, using the same hierarchy as the widget detail page and portal cart:
- Specific product / experience policies
- Category / experience-category policies
all-products/all-experiencespoliciesall-products-experiences/all(catch-all)
Within a bucket, the most-generous top-tier wins as a deterministic tiebreaker.
GET /v1/settings/policies/applicable?item_type={type}&item_id={id}
Query Parametersโ
| Param | Type | Description |
|---|---|---|
item_type | string | product | experience | bundle | store_product |
item_id | integer | The item ID |
Response (200 OK)โ
{
"data": {
"item_type": "experience",
"item_id": 42,
"policy": {
"id": 1,
"name": "Flexible",
"tiers": [ ... ],
"non_refundable_fee": 5.00,
"...": "..."
}
},
"meta": { "request_id": "req_abc123", "timestamp": "..." }
}
policy is null when no policy applies. Returns 404 when the item itself doesn't exist for this supplier.
Get Single Policyโ
GET /v1/settings/policies/{id}
Returns the same shape as one element in the list response.
Create Policyโ
POST /v1/settings/policies
Request Bodyโ
{
"name": "Flexible",
"description": "Full refund up to 7 days before, 50% up to 24h before.",
"applies_to": "all-experiences",
"allow_cancellations": true,
"allow_reschedule": true,
"reschedule_cutoff_hours": 48,
"non_refundable_fee": 5.00,
"refund_method": "original_payment",
"currency": "USD",
"tiers": [
{ "applies_after_hours": 168, "refund_percentage": 100, "refund_method": "original_payment" },
{ "applies_after_hours": 24, "refund_percentage": 50, "refund_method": "original_payment" },
{ "applies_after_hours": 0, "refund_percentage": 0 }
],
"applications": [
{ "type": "all-experiences" }
]
}
Body Fieldsโ
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Max 255 chars |
description | string | no | Max 5000 chars |
applies_to | enum | yes | See enum list above |
allow_cancellations | boolean | no | Default true |
allow_reschedule | boolean | no | Default true |
reschedule_cutoff_hours | integer | no | Hours before start a customer may still self-reschedule. Omit / null = any time before start. Staff are never blocked. |
non_refundable_fee | number | no | โฅ 0, default 0 |
refund_method | enum | no | original_payment | store_credit |
currency | string | no | ISO 4217, 3 chars |
tiers | array | no | When omitted with no legacy fields, no refund will run |
tiers[].applies_after_hours | integer | yes (in tier) | โฅ 0 |
tiers[].refund_percentage | number | yes (in tier) | 0โ100 |
tiers[].refund_method | enum | no | Falls back to policy refund_method |
applications | array | no | One row per scope binding |
applications[].type | enum | yes (in app) | product, category, experience, experience-category, all-products, all-experiences, all-products-experiences, all |
applications[].{product_id,category_id,experience_id,experience_category_id} | integer | conditional | Required for the matching type |
Response (201 Created)โ
Returns the created policy with tiers and applications populated.
Update Policyโ
PUT /v1/settings/policies/{id}
Same body as create. Any field omitted is left unchanged. Sending tiers (or applications) replaces the full set atomically โ partial updates per row are not supported. To preserve existing tiers, fetch them first then send them back.
Response (200 OK)โ
The updated policy.
Delete Policyโ
DELETE /v1/settings/policies/{id}
Deletes the policy plus all of its tiers and applications. Returns 204 No Content.
Errorsโ
| Status | Code | Description |
|---|---|---|
| 401 | authentication_required | Missing API key |
| 403 | insufficient_scope | API key lacks the required scope |
| 404 | resource_not_found | Policy not found for your supplier |
| 422 | validation_error | Invalid body (e.g. tiers[].refund_percentage > 100) |
| 422 | policy_create_failed / policy_update_failed | DB-level failure (constraint violation, etc.) |