{
  "openapi": "3.0.3",
  "info": {
    "title": "Dumpster Controls Booking API",
    "version": "1.0.0",
    "description": "The open Booking API of Dumpster Controls: everything the hosted booking page does, for your own booking screens. Sizes with live prices, availability, service area and promo checks, quotes, and the order itself with card payment through the Stripe Payment Element (POST /intents, then POST /confirm). Included free on every plan, no approval. Authentication: a publishable key (dc_pk_live_...) from a browser page on one of the company's allowed domains, or a server key (dc_sk_booking_...) from your backend. The server prices every order and runs every booking rule; no price is accepted from the client. Junk removal under /junk/*. Reference: https://dumpstercontrols.io/developers/booking/api ; course: https://dumpstercontrols.io/developers/booking/guide",
    "contact": {
      "email": "support@dumpstercontrols.io",
      "url": "https://dumpstercontrols.io/developers/booking/api"
    }
  },
  "servers": [
    {
      "url": "https://gcwyoiihrupbfqqlcurh.supabase.co/functions/v1/booking-api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bookingKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bookingKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "dc_pk_live_... (browser, Origin must be an allowed domain) or dc_sk_booking_... (server-side only)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "field": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Extra data on some errors: allowed (material slugs) on invalid_body for material; allowed_sizes (id, label, yards, price) and material on material_not_allowed."
              }
            }
          }
        }
      },
      "Size": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string"
          },
          "yards": {
            "type": "number"
          },
          "description": {
            "type": "string"
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "price_from": {
            "type": "number",
            "description": "Starting price in dollars."
          },
          "included_tons": {
            "type": "number"
          },
          "overage_per_ton": {
            "type": "number",
            "nullable": true
          },
          "prices": {
            "type": "object",
            "properties": {
              "d3": {
                "type": "number",
                "nullable": true
              },
              "d7": {
                "type": "number",
                "nullable": true
              },
              "d10": {
                "type": "number",
                "nullable": true
              },
              "d14": {
                "type": "number",
                "nullable": true
              },
              "d30": {
                "type": "number",
                "nullable": true
              },
              "extra_day": {
                "type": "number"
              }
            }
          },
          "tax": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "percent": {
                "type": "number"
              }
            }
          },
          "fuel_environmental_fee": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "amount": {
                "type": "number"
              }
            }
          },
          "is_default": {
            "type": "boolean"
          },
          "is_popular": {
            "type": "boolean"
          }
        }
      },
      "MaterialSize": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string"
          },
          "yards": {
            "type": "number"
          },
          "price": {
            "type": "number",
            "nullable": true,
            "description": "Material price in this size, replaces the 7-day base. null = the size price."
          },
          "max_weight_tons": {
            "type": "number",
            "nullable": true
          },
          "overage_mode": {
            "type": "string",
            "enum": [
              "inherit",
              "flat",
              "per_ton"
            ]
          },
          "overage_per_ton": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "Material": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "explanation": {
            "type": "string",
            "nullable": true
          },
          "requires_ack": {
            "type": "boolean"
          },
          "is_catch_all": {
            "type": "boolean"
          },
          "is_heavy": {
            "type": "boolean"
          },
          "sort_order": {
            "type": "integer"
          },
          "size_policy": {
            "type": "string",
            "enum": [
              "all",
              "listed"
            ]
          },
          "sizes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MaterialSize"
            },
            "description": "With size_policy listed, only these sizes accept the material. Empty with size_policy all = every active size at its general price."
          }
        }
      }
    }
  },
  "paths": {
    "/company": {
      "get": {
        "summary": "Company profile and booking status",
        "description": "Name, contact, address, currency, timezone, branding, whether booking is online and payments are ready, card fee handling, non-working days, service area and the hosted page URL. Same data the hosted page shows.",
        "responses": {
          "200": {
            "description": "OK booking.materials { enabled, required } says whether to ask the material before the size."
          },
          "401": {
            "description": "Missing, invalid or expired key, or publishable key used without an Origin."
          },
          "403": {
            "description": "Origin not allowed, or server key used from a browser."
          }
        }
      }
    },
    "/sizes": {
      "get": {
        "summary": "Active dumpster sizes with live prices",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Size"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/materials": {
      "get": {
        "summary": "Materials (what goes in the dumpster), which sizes accept each and the material price",
        "description": "Since 2026-10-05. enabled = the company prices by material; required = ask the material before the size. When enabled is false the list still comes, without prices, and material is ignored for pricing. Materials, sizes, prices and included tons are configured by the company in the app (Inventory, Materials); this API is read-only for them, there is no endpoint to create or edit materials.",
        "responses": {
          "200": {
            "description": "data: { enabled, required, materials: Material[], note }",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "required": {
                          "type": "boolean"
                        },
                        "materials": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Material"
                          }
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/availability": {
      "get": {
        "summary": "Closed days in a date range",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Defaults to today in the company's timezone."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Defaults to from + 59 days. At most 120 days from from."
          }
        ],
        "responses": {
          "200": {
            "description": "today, min_date, from, to, max_days and closed[] with { date, reason } where reason is past, holiday_or_blackout or non_working_day."
          },
          "400": {
            "description": "invalid_parameter"
          }
        }
      }
    },
    "/check": {
      "post": {
        "summary": "Check a delivery date, a ZIP and a promo code before quoting",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "delivery_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "zip": {
                    "type": "string"
                  },
                  "promo_code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "date { ok, code?, message? }, service_area { ok, code?, message?, unverified? }, promo { ok, discount_type?, discount_percent?, discount_amount?, reason? }, booking_online, payments_ready. Only the fields you sent are evaluated."
          }
        }
      }
    },
    "/quote": {
      "post": {
        "summary": "Price a rental",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "size_id",
                  "rental_days"
                ],
                "properties": {
                  "size_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "rental_days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365
                  },
                  "promo_code": {
                    "type": "string"
                  },
                  "material": {
                    "type": "string",
                    "description": "Material slug from GET /materials. When the company prices by material, the material price in this size replaces the 7-day base; the response says pricing: material or general."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "lines (base, extra_days, fuel_environmental_fee, promo_discount, subtotal, tax_percent, tax), promo, total and total_cents (what the company invoices), processing_fee and processing_fee_cents (what the customer pays under the company's card fee handling), card_fee_handling, gross_total and gross_total_cents, currency. The server recomputes everything at payment time."
          },
          "400": {
            "description": "invalid_body"
          },
          "404": {
            "description": "Size not found for this company."
          },
          "409": {
            "description": "material_not_allowed: the material is not available in this size. details.allowed_sizes lists the sizes that take it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/intents": {
      "post": {
        "summary": "Start the order: server-side price, rules, fraud screening and Stripe PaymentIntent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "size_id",
                  "rental_days",
                  "delivery_date",
                  "delivery_address",
                  "delivery_zip",
                  "customer",
                  "terms_accepted"
                ],
                "properties": {
                  "size_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "rental_days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 365
                  },
                  "delivery_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "pickup_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Defaults to delivery_date + rental_days."
                  },
                  "delivery_address": {
                    "type": "string",
                    "description": "Street number and name."
                  },
                  "delivery_city": {
                    "type": "string"
                  },
                  "delivery_state": {
                    "type": "string"
                  },
                  "delivery_zip": {
                    "type": "string"
                  },
                  "placement_lat": {
                    "type": "number"
                  },
                  "placement_lng": {
                    "type": "number"
                  },
                  "debris_type": {
                    "type": "string"
                  },
                  "customer": {
                    "type": "object",
                    "required": [
                      "name",
                      "email",
                      "phone"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "residential",
                          "contractor"
                        ]
                      },
                      "company_name": {
                        "type": "string"
                      }
                    }
                  },
                  "promo_code": {
                    "type": "string"
                  },
                  "terms_accepted": {
                    "type": "boolean",
                    "description": "Must be true: the customer accepted the rental agreement and terms."
                  },
                  "session_id": {
                    "type": "string",
                    "description": "Optional idempotency id (8 to 64 chars). Generated when absent."
                  },
                  "material": {
                    "type": "string",
                    "description": "Material slug from GET /materials. Prices the order by material when the company prices by material. A debris_type equal to an active material slug is treated the same way."
                  },
                  "material_acknowledged": {
                    "type": "boolean",
                    "description": "Required true when the chosen material has requires_ack: the customer confirmed the explanation."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "session_id, payment_intent_id, client_secret, stripe { publishable_key, account }, currency, amount { total, total_cents, processing_fee, processing_fee_cents, gross_total, gross_total_cents, card_fee_handling }, next."
          },
          "400": {
            "description": "invalid_body (field says which)."
          },
          "409": {
            "description": "booking_offline, payments_not_ready, date_past, date_closed, out_of_service_area, account_customer, already_authorized. Also material_not_allowed (details.allowed_sizes)."
          },
          "502": {
            "description": "upstream"
          }
        }
      }
    },
    "/confirm": {
      "post": {
        "summary": "Create the order after the card was confirmed with Stripe.js",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id",
                  "payment_intent_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "payment_intent_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "state (confirmed or held), order_number, order_id, invoice_id, customer_id. Idempotent: a second call returns the same order with already: true."
          },
          "404": {
            "description": "Session not found for this company."
          },
          "409": {
            "description": "payment_intent_mismatch or unconfirmed (message says why; poll /sessions/:id)."
          },
          "502": {
            "description": "upstream: the authorized payment is kept; call POST /confirm again with the same session_id and payment_intent_id (idempotent) and poll GET /sessions/:id. Nobody is charged without an order."
          }
        }
      }
    },
    "/sessions/{session_id}": {
      "get": {
        "summary": "State of a booking session",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "session_id, state (intent_created, confirmed, held, unconfirmed, failed), payment_intent_id, order_number, order_id, last_error, updated_at. Plus material { slug, label, acknowledged_at } and amount_cents when the session was quoted."
          },
          "404": {
            "description": "Session not found for this company."
          }
        }
      }
    },
    "/junk/config": {
      "get": {
        "summary": "Junk removal setup: loads with prices, arrival windows, same-day rule, photo policy, terms",
        "responses": {
          "200": {
            "description": "junk_online, payments_ready, hosted_page, loads[], windows[], same_day, max_advance_days, photo, locations, not_accepted, extra_fee, terms, cancellation_policy, card_fee_handling"
          }
        }
      }
    },
    "/junk/availability": {
      "get": {
        "summary": "Per day, the arrival windows that still fit",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Defaults to from + 29 days; at most 120 days."
          }
        ],
        "responses": {
          "200": {
            "description": "today, from, to, max_advance_days, same_day, days[] { date, open, windows[], reason }"
          }
        }
      }
    },
    "/junk/quote": {
      "post": {
        "summary": "Price a junk load",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "load_fraction"
                ],
                "properties": {
                  "load_fraction": {
                    "type": "string"
                  },
                  "promo_code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "lines { base, promo_discount }, promo, total, total_cents, processing_fee, processing_fee_cents, card_fee_handling, gross_total, gross_total_cents, currency"
          },
          "404": {
            "description": "Unknown or inactive load_fraction."
          }
        }
      }
    },
    "/junk/intents": {
      "post": {
        "summary": "Start the junk job: server-side price, slot check and Stripe PaymentIntent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "load_fraction",
                  "job_date",
                  "time_window_start",
                  "time_window_end",
                  "address_line1",
                  "zip",
                  "customer",
                  "terms_accepted"
                ],
                "properties": {
                  "load_fraction": {
                    "type": "string"
                  },
                  "job_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "time_window_start": {
                    "type": "string",
                    "example": "08:00"
                  },
                  "time_window_end": {
                    "type": "string",
                    "example": "12:00"
                  },
                  "address_line1": {
                    "type": "string"
                  },
                  "city": {
                    "type": "string"
                  },
                  "state": {
                    "type": "string"
                  },
                  "zip": {
                    "type": "string"
                  },
                  "job_type": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "crew_size": {
                    "type": "integer"
                  },
                  "placement_lat": {
                    "type": "number"
                  },
                  "placement_lng": {
                    "type": "number"
                  },
                  "customer": {
                    "type": "object",
                    "required": [
                      "name",
                      "email",
                      "phone"
                    ],
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      }
                    }
                  },
                  "promo_code": {
                    "type": "string"
                  },
                  "terms_accepted": {
                    "type": "boolean"
                  },
                  "session_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Same shape as POST /intents."
          },
          "409": {
            "description": "booking_offline, slot_* codes with field job_date, account_customer."
          }
        }
      }
    },
    "/junk/confirm": {
      "post": {
        "summary": "Create the junk job after the card was confirmed",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id",
                  "payment_intent_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "payment_intent_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "state confirmed, job_number, job_id, photos (note when photos are accepted)."
          },
          "409": {
            "description": "processing, payment_intent_mismatch or unconfirmed."
          }
        }
      }
    },
    "/junk/photos": {
      "post": {
        "summary": "Send a photo of the material after the confirm (up to 3 within 2 hours)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id",
                  "content_base64"
                ],
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "content_base64": {
                    "type": "string",
                    "description": "JPEG, PNG or WebP bytes in base64, 8 MB max"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ok, url"
          },
          "409": {
            "description": "photos_not_available"
          },
          "410": {
            "description": "photos_expired"
          },
          "413": {
            "description": "photo_too_large"
          },
          "415": {
            "description": "photo_type"
          }
        }
      }
    }
  },
  "x-errors": [
    "unauthorized",
    "origin_required",
    "origin_not_allowed",
    "secret_in_browser",
    "company_inactive",
    "rate_limited",
    "booking_api_off",
    "invalid_body",
    "invalid_parameter",
    "not_found",
    "method_not_allowed",
    "internal",
    "booking_offline",
    "payments_not_ready",
    "date_past",
    "date_closed",
    "out_of_service_area",
    "account_customer",
    "already_authorized",
    "payment_intent_mismatch",
    "unconfirmed",
    "upstream",
    {
      "0": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "1": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "2": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "3": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "4": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "5": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "6": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "7": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "8": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "9": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "10": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it.",
      "11": "POST /quote or POST /intents named a material the company does not accept in that size; details.allowed_sizes lists the sizes that take it."
    }
  ],
  "x-limits": {
    "per_ip_per_minute": 120,
    "per_key_per_minute": 120,
    "plan_windows": "none: included free on every plan"
  },
  "x-webhooks": {
    "description": "Register endpoints in Online Booking, Developers (no approval). Signed with X-DC-Signature: t=<unix>,v1=<hex HMAC-SHA256 of '<t>.<body>' with the endpoint secret dcwh_...>. At-least-once; backoff retries; auto-disable after 20 consecutive failures.",
    "events": [
      "booking.completed",
      "booking.held",
      "booking.released"
    ],
    "payload": {
      "event": "booking.completed",
      "created_at": "ISO-8601",
      "data": {
        "order_number": "string",
        "status": "string",
        "payment_status": "string",
        "delivery_date": "date",
        "pickup_date": "date",
        "total_price": "number",
        "booking_channel": "link | widget | api",
        "order_id": "uuid",
        "customer_ref": "uuid",
        "created_at": "ISO-8601",
        "previous_status": "string (booking.released only)",
        "material": "string | null",
        "material_label": "string | null"
      }
    }
  }
}
