{"openapi":"3.0.1","info":{"title":"Lugg Partner API","version":"0.0.1","description":"# Introduction\n\nThe Lugg API is built on HTTP. Our API is RESTful. It has predictable resource URLs. It returns HTTP response codes to indicate errors. It also accepts and returns JSON in the HTTP body. You can use your favorite HTTP/REST library for your programming language to use Lugg's API.\n\n# Media types\n\n**IMPORTANT:** All API requests must include the following `Accept` header to specify the API version:\n\n```\nAccept: application/vnd.lugg+json; version=2\n```\n\nThis custom media type directs your requests to the proper API version. Requests without this header may fail or return unexpected results.\n\n# Authentication\n\nYou must send an OAuth2 access token in an `Authorization` header with each request.\n\n### Creating a client\n\nTo obtain OAuth credentials, please contact your Lugg partner representative or reach out to our partnerships team. We'll provide you with a Client ID and Client Secret for your organization. Keep these credentials secure as they'll be required for all API requests.\n\n### Generating a token\n\nLugg uses the `client_credentials` flow to create access tokens for applications that\nyou have created for your organization. Once obtained you'll use this access token to make\nrequests on behalf of your organization.\n\nPartner access tokens currently do not expire automatically. Reuse the token until it is\nrevoked, and create a new token if you revoke or rotate credentials.\n\n```\ncurl -X POST 'https://api.lugg.com/oauth/token' \\\n     -H 'Accept: application/vnd.lugg+json; version=2' \\\n     -d '{ \\\n       \"grant_type\": \"client_credentials\", \\\n       \"client_id\": <client_id>, \\\n       \"client_secret\": <client_secret>, \\\n       \"scope\": \"public org\" \\\n     }'\n```\n\n### Making authenticated requests\n\nTo authenticate subsequent API requests, you must provide both the Accept header and a valid bearer token:\n\n```\ncurl -X GET 'https://api.lugg.com/bookings' \\\n     -H 'Accept: application/vnd.lugg+json; version=2' \\\n     -H 'Authorization: Bearer <bearer_token>'\n```\n\n### Scopes\n\nOAuth tokens require specific scopes to access API resources. When requesting a token, include the `scope` parameter with space-separated scope values.\n\n#### Available Scopes\n\n| Scope | Description | Use Case |\n|-------|-------------|----------|\n| `public` | Base access scope | Included by default in all tokens |\n| `org` | Full organization access | Read and write permissions for all resources |\n| `org:read` | Read-only organization access | List and view resources only |\n| `org:write` | Write-only organization access | Create, update, and delete resources only |\n\n#### Scope Examples\n\n| Access Level | Scope Value | Use Case |\n|--------------|-------------|----------|\n| Full access | `\"scope\": \"public org\"` | Recommended for most integrations |\n| Read-only | `\"scope\": \"public org:read\"` | Reporting and monitoring |\n| Write-only | `\"scope\": \"public org:write\"` | Booking creation only |\n| Explicit read+write | `\"scope\": \"public org:read org:write\"` | Equivalent to `org` |\n\n**Note:** The `org` scope is a parent scope that grants both read and write access. For granular control, use `org:read` or `org:write` individually. Endpoints accept either the parent `org` scope or the appropriate granular scope (`org:read` for GET requests, `org:write` for POST/PATCH/DELETE requests).\n\n<SecurityDefinitions />\n\n# Sandbox Environment\n\nThe Lugg API provides a sandbox environment for testing and development purposes at `https://api-sandbox.lugg.com`.\n\n## Key Characteristics\n\n- **Separate credentials**: The sandbox requires its own OAuth client ID and secret. Contact your Lugg partner representative to obtain sandbox credentials.\n- **Safe testing**: Sandbox data is isolated from production. No real workers are dispatched, and no real charges are processed.\n- **Non-functional features**: Share URLs, tracking links, and customer-facing interfaces generated in sandbox are for testing purposes only and will not function for end users.\n\n## When to Use Sandbox\n\nUse the sandbox environment to:\n- Test your integration during development\n- Validate API requests and responses\n- Test error handling and edge cases\n- Demonstrate your integration to stakeholders\n\nAlways test thoroughly in sandbox before deploying to production.\n\n## Automatic Booking Simulation\n\nTo test a complete booking lifecycle, add `test_specifications.mode: auto` to a `POST /bookings` request:\n\n```json\n{\n  \"test_specifications\": {\n    \"mode\": \"auto\"\n  }\n}\n```\n\nThe request must include a valid arrival window. The simulation starts immediately and does not wait for the requested arrival-window time.\n\nAutomatic simulation:\n- Assigns a synthetic crew\n- Progresses every route stop through arrival and completion\n- Attaches a representative completion photo to each completed stop\n- Calculates the final fare and creates normal sandbox billing records\n- Sends the existing booking webhooks\n\nThe booking response has the same shape as a normal booking response. Use booking webhooks or `GET /bookings/{id}` to observe its progress.\n\n`test_specifications` is only accepted in the sandbox. Production requests that include it return a `422` response.\n\n# Errors\n\nLugg uses conventional HTTP response codes to indicate the success or failure of an API request. In general: Codes in the 2xx range indicate success. Codes in the 4xx range indicate an error that failed given the information provided (e.g., a required parameter was omitted, a arrival window is no longer available, etc.). Codes in the 5xx range indicate an error with Lugg's servers.\n\n## HTTP Status codes\n\n| Code | Title                 | Description                              |\n|------|-----------------------|------------------------------------------|\n| 200  | OK                    | The request was successful.              |\n| 400  | Bad request           | Bad request.                             |\n| 404  | Not found             | Some resource does not exist.            |\n| 401  | Unauthorized          | Your access token is missing or invalid. |\n| 429  | Too Many Requests     | The rate limit was exceeded.             |\n| 5xx  | Internal Server Error | An error occurred with our API.          |\n\n## Error types\n\n| Type                  | Description                                                       |\n|-----------------------|-------------------------------------------------------------------|\n| api_error             | Internal API error.                                               |\n| validation_error      | Your parameters were not valid.                                   |\n| authentication_error  | You are not authorized.                                           |\n| invalid_request_error | The parameters were valid but the request could not be completed. |\n| rate_limit_error      | The request has been rate limited.                                |\n\nRate-limited responses use `type: rate_limit_error` and typically include `code: rate_limit_exceeded`.\n\nExample error response.\n\n```\n  {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"Arrival window no longer available.\",\n    \"param\": \"arrival_window_id\",\n    \"code\": null\n  }\n```\n\n# Rate Limiting\n\nTo keep the API stable, requests are rate limited at the organization level.\n\nCurrent default:\n- **300 requests per 60 seconds** across your organization's API usage.\n\nIf you exceed this limit, you'll receive `429 Too Many Requests`. Continued excessive traffic may result in temporary blocking.\n\n## Rate limit headers\n\n| Header | Description |\n|--------|-------------|\n| `Retry-After` | Seconds to wait before retrying. |\n| `RateLimit-Limit` | Maximum requests allowed in the current window. |\n| `RateLimit-Remaining` | Requests remaining in the current window. |\n| `RateLimit-Reset` | Unix timestamp when the current window resets. |\n\n## Integration guidance\n\n- Prefer webhook/event-driven flows over polling when possible.\n- Keep request patterns efficient and avoid broad scans for single-entity lookups.\n- Control concurrency centrally if multiple workers/services call the API.\n- Honor `Retry-After` and back off on `429` responses.\n\nLimits may change over time. Build clients to handle `429` responses gracefully.\n\n# Pagination\n\nThe Lugg API uses cursor-based pagination for list endpoints. This provides consistent results even as data changes.\n\n## Request Parameters\n\n| Parameter | Type    | Description                                                    |\n|-----------|---------|----------------------------------------------------------------|\n| limit     | integer | Number of results per page (1-100, default: 20)               |\n| after     | string  | Cursor for forward pagination (returns next page)             |\n| before    | string  | Cursor for backward pagination (returns previous page)        |\n\n**Note:** You cannot use both `after` and `before` in the same request.\n\n## Response Format\n\nAll paginated endpoints return a consistent structure with:\n- `data`: Array of requested resources\n- `next`: Complete URL for the next page (null if no more pages)\n- `previous`: Complete URL for the previous page (null if at the beginning)\n\n## Example\n\nRequest:\n```\nGET https://api.lugg.com/bookings?limit=10\n```\n\nResponse:\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n      \"hid\": \"ABC123\",\n      \"state\": \"created\"\n      // ... other booking fields\n    }\n  ],\n  \"next\": \"https://api.lugg.com/bookings?limit=10&after=eyJpZCI6IjA5ODc2NTQzMjEiLCJjcmVhdGVkX2F0IjoiMjAyNC0wMS0wMlQxMjowMDowMFoifQ==\",\n  \"previous\": null\n}\n```\n\n## Navigation\n\n- **First page**: Omit both `after` and `before` parameters\n- **Next page**: Use the complete URL provided in the `next` field\n- **Previous page**: Use the complete URL provided in the `previous` field\n- **Last page**: When `next` is null\n\nThe pagination URLs include all necessary parameters. Simply follow these URLs directly without modification.\n"},"tags":[{"name":"OAuth","description":"OAuth2 authentication for generating access tokens. Use the client_credentials flow for organization-level access."},{"name":"Bookings","description":"Create, retrieve, update, and cancel bookings. Track booking state transitions from creation through completion."},{"name":"Webhook Endpoints","description":"Configure webhook endpoints to receive real-time notifications for booking lifecycle events. Supports Svix signature verification for security."},{"name":"Quotes","description":"Generate fare estimates for deliveries between locations. Quotes include pricing breakdown and travel distance/duration estimates."},{"name":"Booking Stops","description":"Add, remove, and reorder stops on existing bookings."},{"name":"Schedules","description":"Retrieve available arrival windows for booking deliveries. Supports both on-demand and scheduled time slots."}],"x-tagGroups":[{"name":"Authentication","tags":["OAuth"]},{"name":"Booking Flow","tags":["Bookings","Booking Stops","Quotes","Schedules"]},{"name":"Integrations","tags":["Webhook Endpoints"]}],"paths":{"/bookings/{booking_id}/stops":{"post":{"summary":"Add a stop to a booking","tags":["Booking Stops"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"Created","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable Content","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/LocationParam"}},"required":["location"]}}},"required":true}}},"/bookings/{booking_id}/stops/{position}":{"delete":{"summary":"Remove a stop from a booking","tags":["Booking Stops"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"position","in":"path","required":true,"schema":{"type":"integer"},"description":"Stop position (0-indexed)"}],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable Content","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bookings/{booking_id}/stops/reorder":{"patch":{"summary":"Reorder stops on a booking","tags":["Booking Stops"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"parameters":[{"name":"booking_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable Content","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"from_position":{"type":"integer","description":"Current position of the stop to move"},"to_position":{"type":"integer","description":"Target position to move the stop to"}},"required":["from_position","to_position"]}}},"required":true}}},"/bookings/{id}/cancel":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"summary":"Cancel a booking","tags":["Bookings"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Bad Request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"cancel_reason":{"$ref":"#/components/schemas/CancelReasonEnum"}}}}}}}},"/bookings":{"post":{"summary":"Create a booking","tags":["Bookings"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"description":"Creates a new booking for a Lugg delivery service.\n\nYou can create a booking in three ways:\n- **Using a quote**: Provide `quote_id` + arrival window details\n- **Two-stop booking**: Provide `origin` + `destination` + `product` + arrival window details\n- **Multi-stop booking**: Provide `stops` array + `product` + arrival window details\n\nArrival window is required. Provide either `arrival_window_id` (from the schedules endpoint) or `arrival_window_from` for a custom future ISO 8601 time. `arrival_window_to` is optional and defaults to one hour after `arrival_window_from`. `arrival_window_ref` defaults to `origin`; use `destination` for dropoff-window scheduling. Booking can fail if the requested window cannot be reserved.\n\n`customer` is optional and organization-only.\n\nIn the sandbox, set `test_specifications.mode` to `auto` to automatically dispatch and complete the booking through the normal webhook and billing flow. The simulation starts immediately and does not wait for the requested arrival-window time.\n\nAll other parameters are optional.\n","parameters":[],"responses":{"201":{"description":"Created","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Invalid request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Business logic errors","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Service unavailable","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Insufficient scopes","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"quote_id":{"type":"string","format":"uuid","description":"Pre-calculated quote ID that contains origin, destination, and product."},"stops":{"type":"array","items":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/LocationParam"},"instructions":{"type":"string","nullable":true,"description":"Operational instructions for this stop."}},"required":["location"]},"minItems":2,"maxItems":7,"description":"Array of stops in route order. First stop is origin, last is destination."},"origin":{"$ref":"#/components/schemas/LocationParam","deprecated":true,"description":"DEPRECATED: Use stops array instead. Pickup location."},"destination":{"$ref":"#/components/schemas/LocationParam","deprecated":true,"description":"DEPRECATED: Use stops array instead. Dropoff location."},"product":{"$ref":"#/components/schemas/ProductEnum","description":"Service type (pickup, van, xl, etc.)."},"arrival_window_id":{"type":"string","description":"Pre-selected arrival window ID from the schedule endpoint. Cannot be used together with arrival_window_from/to."},"arrival_window_from":{"type":"string","format":"date-time","description":"Custom future arrival window start time (ISO 8601). Cannot be used together with arrival_window_id."},"arrival_window_to":{"type":"string","format":"date-time","description":"Custom future arrival window end time (ISO 8601). Optional; defaults to 1 hour after arrival_window_from."},"arrival_window_ref":{"type":"string","enum":["origin","destination"],"description":"Reference point for the arrival window timing. Defaults to 'origin'. Use 'destination' for dropoff-window scheduling."},"description":{"type":"string","description":"Optional description or special instructions for the booking."},"customer":{"$ref":"#/components/schemas/Customer","description":"Optional customer contact details. Only available for organization bookings."},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactInput"},"description":"Optional notification contacts. Each contact may set `stop_positions` to scope it to specific route stops ([0] = pickup, last = dropoff); omitted/empty applies to all stops. When contacts are provided they are the booking's notification recipients — the `customer` record is not also added as a contact."},"photo_urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Array of photo URLs showing items to be moved."},"metadata":{"type":"object","description":"Custom metadata object for storing additional information."},"order_number":{"type":"string","description":"Order number or reference identifier for the booking."},"tip":{"oneOf":[{"type":"integer","enum":[0]},{"type":"integer","minimum":100}],"description":"Optional booking-level tip in integer cents (USD). Use 0 for no tip; positive values must be at least 100 cents. The tip can only be set when creating a booking and is included in the final fare."},"test_specifications":{"type":"object","writeOnly":true,"additionalProperties":false,"properties":{"mode":{"type":"string","enum":["auto"]}},"required":["mode"],"description":"Sandbox only. Automatically dispatches and completes the booking through the normal webhook and billing flow. Simulation starts immediately and does not wait for the requested arrival-window time."}},"anyOf":[{"required":["arrival_window_id"]},{"required":["arrival_window_from"]}]}}},"required":true}},"get":{"summary":"List bookings","tags":["Bookings"],"security":[{"OAuth2":["org","org:read"]},{"OAuth2":["user","user:read"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:read","position":"after","color":"#c9e7ff"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:read","position":"after","color":"#d9f2c4"}],"parameters":[{"name":"after","in":"query","required":false,"description":"Cursor for forward pagination (next page)","schema":{"type":"string"}},{"name":"before","in":"query","required":false,"description":"Cursor for backward pagination (previous page)","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Number of bookings to return (1-100)","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"state","in":"query","required":false,"description":"Filter by booking state(s)","schema":{"type":"array","items":{"$ref":"#/components/schemas/BookingStateEnum"}},"style":"form","explode":false}],"responses":{"200":{"description":"successful","content":{"application/vnd.lugg+json;version=2":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedResponse"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}}}}]}}}},"401":{"description":"unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bookings/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Show a booking","tags":["Bookings"],"security":[{"OAuth2":["org","org:read"]},{"OAuth2":["user","user:read"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:read","position":"after","color":"#c9e7ff"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:read","position":"after","color":"#d9f2c4"}],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a booking","tags":["Bookings"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"description":"Updates an existing booking. All fields are optional.\n\nYou can update any combination of fields independently, or provide a complete replacement for specific sections like arrival windows or customer details.\n\nTo update arrival windows, provide either `arrival_window_id` from the schedules endpoint or a custom future ISO 8601 `arrival_window_from`. `arrival_window_to` is optional and defaults to one hour after `arrival_window_from`. Updates can fail if the requested window cannot be reserved.\n\n`customer` is optional and organization-only.\n","parameters":[],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Bad Request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"origin":{"$ref":"#/components/schemas/LocationParam","description":"Updated pickup location. Can be a location UUID, coordinates array [lng, lat], or address string."},"destination":{"$ref":"#/components/schemas/LocationParam","description":"Updated dropoff location. Can be a location UUID, coordinates array [lng, lat], or address string."},"description":{"type":"string","description":"Updated description or special instructions for the booking."},"photo_urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Updated array of photo URLs showing items to be moved. Completely replaces existing photos."},"metadata":{"type":"object","additionalProperties":true,"description":"Updated custom metadata object. Merges with existing metadata - provide only the fields you want to change or add."},"order_number":{"type":"string","description":"Updated order number or reference identifier for the booking."},"customer":{"$ref":"#/components/schemas/Customer","description":"Updated optional customer contact details. Only available for organization bookings. Completely replaces existing customer information."},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactInput"},"description":"Updated notification contacts. Completely replaces existing contacts; omit to leave unchanged, send [] to clear. Each contact may set `stop_positions` to scope it to specific route stops."}},"description":"Update booking without changing arrival window"},{"type":"object","properties":{"origin":{"$ref":"#/components/schemas/LocationParam","description":"Updated pickup location. Can be a location UUID, coordinates array [lng, lat], or address string."},"destination":{"$ref":"#/components/schemas/LocationParam","description":"Updated dropoff location. Can be a location UUID, coordinates array [lng, lat], or address string."},"description":{"type":"string","description":"Updated description or special instructions for the booking."},"arrival_window_id":{"type":"string","description":"Pre-selected arrival window ID from the schedule endpoint. Use this to change to a scheduled time slot. Cannot be used together with arrival_window_from/to. Update can fail if the window cannot be reserved."},"photo_urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Updated array of photo URLs showing items to be moved. Completely replaces existing photos."},"metadata":{"type":"object","additionalProperties":true,"description":"Updated custom metadata object. Merges with existing metadata - provide only the fields you want to change or add."},"order_number":{"type":"string","description":"Updated order number or reference identifier for the booking."},"customer":{"$ref":"#/components/schemas/Customer","description":"Updated optional customer contact details. Only available for organization bookings. Completely replaces existing customer information."},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactInput"},"description":"Updated notification contacts. Completely replaces existing contacts; omit to leave unchanged, send [] to clear. Each contact may set `stop_positions` to scope it to specific route stops."}},"required":["arrival_window_id"],"description":"Update booking with pre-selected arrival window"},{"type":"object","properties":{"origin":{"$ref":"#/components/schemas/LocationParam","description":"Updated pickup location. Can be a location UUID, coordinates array [lng, lat], or address string."},"destination":{"$ref":"#/components/schemas/LocationParam","description":"Updated dropoff location. Can be a location UUID, coordinates array [lng, lat], or address string."},"description":{"type":"string","description":"Updated description or special instructions for the booking."},"arrival_window_from":{"type":"string","format":"date-time","description":"Custom future arrival window start time (ISO 8601). Use this to change to a flexible time window. Cannot be used together with arrival_window_id."},"arrival_window_to":{"type":"string","format":"date-time","description":"Custom future arrival window end time (ISO 8601). Optional - defaults to 1 hour after arrival_window_from if not provided."},"photo_urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Updated array of photo URLs showing items to be moved. Completely replaces existing photos."},"metadata":{"type":"object","additionalProperties":true,"description":"Updated custom metadata object. Merges with existing metadata - provide only the fields you want to change or add."},"order_number":{"type":"string","description":"Updated order number or reference identifier for the booking."},"customer":{"$ref":"#/components/schemas/Customer","description":"Updated optional customer contact details. Only available for organization bookings. Completely replaces existing customer information."},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactInput"},"description":"Updated notification contacts. Completely replaces existing contacts; omit to leave unchanged, send [] to clear. Each contact may set `stop_positions` to scope it to specific route stops."}},"required":["arrival_window_from"],"description":"Update booking with custom arrival window"}]}}}}}},"/oauth/token":{"post":{"summary":"Token Creation","tags":["OAuth"],"security":[],"parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"400":{"description":"Bad Request"},"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"type":"object","properties":{"access_token":{"type":"string","description":"The access token string"},"token_type":{"type":"string","description":"The type of token (usually 'Bearer')"},"expires_in":{"type":"integer","nullable":true,"description":"Token lifetime in seconds. Null means the token does not expire automatically and remains valid until revoked."},"scope":{"type":"string","description":"Space-delimited list of scopes"},"refresh_token":{"type":"string","description":"Refresh token (only for authorization_code and password flows)"},"created_at":{"type":"integer","description":"Token creation timestamp"}},"required":["access_token","token_type"]}}}}},"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"grant_type":{"type":"string","enum":["client_credentials"]},"client_id":{"type":"string"},"client_secret":{"type":"string"}},"required":["grant_type","client_id","client_secret"]},{"type":"object","properties":{"grant_type":{"type":"string","enum":["password"]},"client_id":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}},"required":["grant_type","client_id","username","password"]},{"type":"object","properties":{"grant_type":{"type":"string","enum":["authorization_code"]},"client_id":{"type":"string"},"client_secret":{"type":"string"},"code":{"type":"string"},"redirect_uri":{"type":"string"}},"required":["grant_type","client_id","client_secret","code","redirect_uri"]}]}}},"required":true}}},"/oauth/revoke":{"post":{"summary":"Token Revocation","tags":["OAuth"],"parameters":[],"description":"Token successfully revoked","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}},"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"token":{"type":"string"}}}},"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}}}}},"required":true}}},"/quotes/batch":{"post":{"summary":"Create batch quotes for all available products","tags":["Quotes"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"description":"Batch quotes created successfully","parameters":[],"responses":{"201":{"description":"Created","content":{"application/vnd.lugg+json;version=2":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Quote"}},"recommended":{"type":"string","description":"ID of the recommended quote from the data array","nullable":true}},"required":["data"]}}}},"400":{"description":"Bad Request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable Content","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"stops":{"type":"array","items":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/LocationParam"},"instructions":{"type":"string","nullable":true,"description":"Operational instructions for this stop."}},"required":["location"]},"minItems":2,"maxItems":7,"description":"Stops in route order. First is origin and last is destination."},"origin":{"$ref":"#/components/schemas/LocationParam","deprecated":true},"destination":{"$ref":"#/components/schemas/LocationParam","deprecated":true},"items":{"type":"array","items":{"type":"object","properties":{"dimensions":{"type":"array","items":{"type":"number"},"minItems":3,"maxItems":3,"description":"Width, height, length in inches"},"weight":{"type":"number","description":"Weight in pounds"}}},"description":"Optional items for product recommendation."}},"oneOf":[{"required":["stops"]},{"required":["origin","destination"]}]}}},"required":true}}},"/quotes":{"post":{"summary":"Create a quote","tags":["Quotes"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"description":"Creates a fare estimate for a delivery.\n\nYou can create a quote in two ways:\n- **Two-stop quote**: Provide `origin` + `destination` + `product`\n- **Multi-stop quote**: Provide `stops` array + `product`\n\n`fare_estimate` returns one fare object for a fixed price, or two fare objects as `[low, high]` for variable-labor products. Fare amounts are integer cents in USD. Quote IDs are intended for near-term booking creation; create a new quote if an unused quote is stale or no longer found.\n","parameters":[],"responses":{"201":{"description":"Created","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"400":{"description":"Bad Request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable Content","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"stops":{"type":"array","items":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/LocationParam"},"instructions":{"type":"string","nullable":true,"description":"Operational instructions for this stop."}},"required":["location"]},"minItems":2,"maxItems":7,"description":"Array of stops in route order. First stop is origin, last is destination."},"origin":{"$ref":"#/components/schemas/LocationParam","deprecated":true,"description":"DEPRECATED: Use stops array instead. Pickup location."},"destination":{"$ref":"#/components/schemas/LocationParam","deprecated":true,"description":"DEPRECATED: Use stops array instead. Dropoff location."},"product":{"$ref":"#/components/schemas/ProductEnum","description":"Service type (pickup, van, xl, etc.)."}}}}},"required":true}}},"/quotes/{id}":{"get":{"summary":"Retrieve a quote","tags":["Quotes"],"security":[{"OAuth2":["org","org:read"]},{"OAuth2":["user","user:read"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:read","position":"after","color":"#c9e7ff"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:read","position":"after","color":"#d9f2c4"}],"description":"Retrieves a quote. `fare_estimate` returns one fare object for a fixed price, or two fare objects as `[low, high]` for variable-labor products. Fare amounts are integer cents in USD. Quote IDs are intended for near-term booking creation; create a new quote if an unused quote is stale or no longer found.\n","parameters":[{"name":"id","in":"path","description":"Quote ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/schedule":{"post":{"summary":"Get arrival windows schedule for a product and route","tags":["Schedules"],"security":[{"OAuth2":["org","org:read"]},{"OAuth2":["user","user:read"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:read","position":"after","color":"#c9e7ff"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:read","position":"after","color":"#d9f2c4"}],"parameters":[],"description":"Schedule retrieved successfully","responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Schedule"}}}},"400":{"description":"Bad Request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unprocessable Content","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"quote_id":{"type":"string","format":"uuid","description":"Quote ID to extract product and locations from"},"ref":{"type":"string","enum":["origin","destination"],"description":"Reference point (origin = pickup, destination = dropoff)"}},"required":["quote_id"]},{"type":"object","properties":{"product":{"$ref":"#/components/schemas/ProductEnum","description":"Product type"},"origin":{"$ref":"#/components/schemas/LocationParam","description":"Origin location"},"destination":{"$ref":"#/components/schemas/LocationParam","description":"Destination location"},"ref":{"type":"string","enum":["origin","destination"],"description":"Reference point (origin = pickup, destination = dropoff)"}},"required":["product","origin","destination"]}]}}}}}},"/webhooks":{"post":{"summary":"Create webhook endpoint","tags":["Webhook Endpoints"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"parameters":[],"responses":{"201":{"description":"Created","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The URL where webhook events will be sent"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventEnum"},"description":"List of event types to subscribe to"},"description":{"type":"string","description":"Optional description for the webhook endpoint"}},"required":["url"]}}},"required":true}},"get":{"summary":"List webhook endpoints","tags":["Webhook Endpoints"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}":{"parameters":[{"name":"id","in":"path","description":"Webhook endpoint ID","required":true,"schema":{"type":"string"}}],"delete":{"summary":"Delete webhook endpoint","tags":["Webhook Endpoints"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"responses":{"204":{"description":"No Content"},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"Get webhook endpoint details","tags":["Webhook Endpoints"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update webhook endpoint","tags":["Webhook Endpoints"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"404":{"description":"Not Found","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"description":"Bad Request","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The URL where webhook events will be sent (required by Svix API)"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventEnum"},"description":"List of event types to subscribe to"},"description":{"type":"string","description":"Optional description for the webhook endpoint"},"disabled":{"type":"boolean","description":"Whether to disable the endpoint"}},"required":["url"]}}},"required":true}}},"/webhooks/portal":{"get":{"summary":"Get webhook management portal URL","tags":["Webhook Endpoints"],"security":[{"OAuth2":["org","org:write"]},{"OAuth2":["user","user:write"]}],"x-badges":[{"name":"org","position":"after","color":"#b3d9f2"},{"name":"org:write","position":"after","color":"#a0c8e8"},{"name":"user","position":"after","color":"#c5e8b0"},{"name":"user:write","position":"after","color":"#b1dd9e"}],"description":"Returns a URL to the Svix self-service portal where users can manage their webhook endpoints, view logs, and test webhooks","responses":{"200":{"description":"OK","content":{"application/vnd.lugg+json;version=2":{"schema":{"type":"object","properties":{"portal_url":{"type":"string","format":"uri","description":"URL to access the Svix webhook management portal"}}}}}},"401":{"description":"Unauthorized","content":{"application/vnd.lugg+json;version=2":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"servers":[{"url":"http://api.lugg.com"}],"security":[{"OAuth2":[]}],"components":{"securitySchemes":{"OAuth2":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"/oauth/token","scopes":{"org":"Organization access","org:read":"Organization read access","org:write":"Organization write access"}}}}},"headers":{"RetryAfter":{"description":"Number of seconds to wait before retrying a throttled request.","schema":{"type":"integer"},"example":12},"RateLimitLimit":{"description":"Maximum number of requests allowed in the current rolling window.","schema":{"type":"integer"},"example":300},"RateLimitRemaining":{"description":"Remaining requests available in the current rolling window.","schema":{"type":"integer"},"example":42},"RateLimitReset":{"description":"Unix timestamp when the current rolling window resets.","schema":{"type":"integer"},"example":1738627200}},"responses":{"RateLimited":{"description":"Too many requests. Reduce request volume and retry later.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"type":"rate_limit_error","message":"Too many requests. Please try again shortly.","code":"rate_limit_exceeded","http_status":429}}}}},"schemas":{"Address":{"type":"object","additionalProperties":false,"required":["locality","region"],"properties":{"number":{"type":"string","example":"78"},"street":{"type":"string","example":"1st St."},"street2":{"type":"string","nullable":true,"description":"Unit, apartment, suite, or other secondary address","example":"APT C"},"locality":{"type":"string","example":"San Francisco"},"region":{"type":"string","example":"CA"},"postal_code":{"type":"string","example":"94131"}}},"ArrivalWindow":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid","description":"Arrival window ID (UUID format)"},"available":{"type":"boolean","description":"Whether this window is available for booking"},"ondemand":{"type":"boolean","description":"Whether this is an on-demand window"},"start_at":{"type":"string","format":"date-time","nullable":true,"description":"Start time of the arrival window (null for on-demand)"},"end_at":{"type":"string","format":"date-time","nullable":true,"description":"End time of the arrival window (null for on-demand)"},"label":{"type":"string","nullable":true,"description":"Human-readable label for the window","example":"2:00 PM - 4:00 PM"}},"required":["id","available","ondemand"]},"Booking":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"hid":{"type":"string","description":"Booking confirmation code","example":"67GH8J"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"product":{"$ref":"#/components/schemas/ProductEnum"},"origin":{"$ref":"#/components/schemas/Location"},"destination":{"$ref":"#/components/schemas/Location"},"route":{"type":"array","items":{"$ref":"#/components/schemas/RouteStop"},"description":"All stops in order (origin through destination)"},"arrival_window":{"$ref":"#/components/schemas/ArrivalWindow"},"arrival_window_ref":{"type":"string","enum":["origin","destination"],"description":"Delivery reference for the arrival window","nullable":true},"description":{"type":"string","description":"Description of the trip","nullable":true},"order_number":{"type":"string","description":"Order number or reference identifier for the booking","nullable":true,"example":"ORD-12345"},"tip":{"type":"integer","minimum":0,"description":"Booking-level tip in integer cents (USD). Set only during booking creation; 0 means no tip. Positive values must be at least 100 cents. Included in the final fare."},"photo_urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"URLs of photos associated with the trip"},"state":{"$ref":"#/components/schemas/BookingStateEnum"},"dispatch_state":{"type":"string","description":"Legacy booking-level dispatch tracking state, gated to the dispatched booking state (reverts to `origin_pending` once the booking completes). For multi-stop bookings, stop_* states indicate progress at an intermediate stop. Prefer the `dispatch` object (and its normalized `dispatch.state`) for accurate, per-stop progress.","enum":["origin_pending","origin_driving","origin_at","origin_done","stop_pending","stop_driving","stop_at","stop_done","destination_pending","destination_driving","destination_at","destination_done"],"x-enum-descriptions":{"origin_pending":"Waiting to dispatch to origin","origin_driving":"Luggers driving to origin","origin_at":"Luggers arrived at origin","origin_done":"Origin pickup completed","stop_pending":"Waiting to dispatch to an intermediate stop","stop_driving":"Luggers driving to an intermediate stop","stop_at":"Luggers arrived at an intermediate stop","stop_done":"Intermediate stop completed","destination_pending":"Waiting to dispatch to destination","destination_driving":"Luggers driving to destination","destination_at":"Luggers arrived at destination","destination_done":"Delivery completed at destination"}},"share_url":{"type":"string","format":"uri","description":"Shareable tracking URL for the booking"},"customer":{"$ref":"#/components/schemas/Customer","nullable":true,"description":"Optional customer information for organization bookings."},"contacts":{"type":"array","items":{"$ref":"#/components/schemas/Contact"},"description":"Notification contacts for the booking, each scoped to one or more route stops."},"crew":{"$ref":"#/components/schemas/Crew","nullable":true,"description":"Information about the crew assigned to the booking"},"dispatch":{"$ref":"#/components/schemas/Dispatch","nullable":true,"description":"Current (or most recent) dispatch progress with per-stop routing, present whenever a dispatch exists — including completed bookings. Null before any dispatch. Unlike the legacy `dispatch_state`, this is not gated on booking state; prefer it over `dispatch_state`."},"quote":{"$ref":"#/components/schemas/Quote","nullable":true,"description":"Pricing information for the booking"},"fare":{"$ref":"#/components/schemas/Fare","nullable":true,"description":"Final charged fare; populated after fulfillment, otherwise null"},"metadata":{"type":"object","nullable":true,"description":"Custom metadata associated with the booking"}}},"BookingStateEnum":{"type":"string","enum":["created","dispatched","cancelled","completed"],"description":"Current state of the booking","x-enum-descriptions":{"created":"Booking has been created but not yet dispatched","dispatched":"Booking has been assigned to luggers","cancelled":"Booking was cancelled","completed":"Booking has been successfully completed"}},"CancelReasonEnum":{"type":"string","enum":["vehicle","long_eta","im_not_ready","other"],"description":"Reason for canceling a booking","example":"long_eta","x-enum-descriptions":{"vehicle":"Customer needs a different vehicle type","long_eta":"Estimated arrival time is too long","im_not_ready":"Customer is not ready for pickup","other":"Other reason"}},"Coordinates":{"type":"array","description":"Coordinates as [longitude, latitude] in decimal degrees","items":{"type":"number","format":"float"},"minItems":2,"maxItems":2,"example":[125.6,10.1]},"Crew":{"type":"object","description":"Information about the crew assigned to the booking","nullable":true,"properties":{"eta":{"type":"string","format":"date-time","description":"Estimated time of arrival at the next location","nullable":true},"location":{"$ref":"#/components/schemas/Coordinates","description":"Current crew location","nullable":true},"phone_number":{"type":"string","description":"Booking-scoped phone number for contacting the assigned crew. Sandbox simulations return a synthetic test number.","nullable":true,"example":"+14155550100"},"vehicle_type":{"type":"string","description":"Type of vehicle being used","enum":["pickup_truck","van","sprinter_van","box_truck_14ft","box_truck_16ft","box_truck_18ft","box_truck_24ft"],"x-enum-descriptions":{"pickup_truck":"Standard pickup truck","van":"Cargo van","sprinter_van":"Large sprinter van","box_truck_14ft":"14-foot box truck","box_truck_16ft":"16-foot box truck","box_truck_18ft":"18-foot box truck","box_truck_24ft":"24-foot box truck"}},"rating":{"type":"number","format":"float","description":"Average crew rating","minimum":0,"maximum":5},"members":{"type":"array","description":"List of crew members","items":{"$ref":"#/components/schemas/CrewMember"}}}},"CrewMember":{"type":"object","description":"Information about a crew member","properties":{"first_name":{"type":"string","description":"Crew member's first name"},"avatar_url":{"type":"string","format":"uri","description":"URL to crew member's avatar image"}}},"Customer":{"type":"object","additionalProperties":false,"description":"Customer contact information. Must provide at least email or phone_number (or both).","properties":{"name":{"type":"string","description":"Customer's name","example":"John Doe"},"email":{"type":"string","format":"email","description":"Customer's email address","example":"john@example.com"},"phone_number":{"type":"string","description":"Customer's phone number in E.164 format","example":"+14155551234"}}},"ContactInput":{"type":"object","additionalProperties":false,"description":"A notification contact for the booking, optionally scoped to specific route stops. Contacts are de-duplicated by phone number — if two contacts share a phone number, only the first is saved.","properties":{"name":{"type":"string","description":"Contact's name","example":"Store Team"},"phone_number":{"type":"string","description":"Contact's phone number in E.164 format","example":"+14155550100"},"email":{"type":"string","format":"email","description":"Contact's email address","example":"store@example.com"},"stop_positions":{"type":"array","items":{"type":"integer","minimum":0},"description":"Route positions (0-indexed) this contact applies to. 0 = origin/pickup, last = destination/dropoff, intermediate values = intermediate stops. Omitted or empty applies the contact to all stops. Positions outside the route (negative or >= the number of stops) are rejected with a 400.","example":[0]}},"required":["phone_number"]},"Contact":{"type":"object","description":"A notification contact for the booking and the route stops it applies to.","properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for the contact"},"name":{"type":"string","nullable":true,"description":"Contact's name","example":"Store Team"},"phone_number":{"type":"string","description":"Contact's phone number in E.164 format","example":"+14155550100"},"email":{"type":"string","format":"email","nullable":true,"description":"Contact's email address","example":"store@example.com"},"notification_scope":{"type":"string","enum":["all","selected"],"description":"'all' applies the contact to every stop; 'selected' applies it only to the listed stops."},"stop_positions":{"type":"array","items":{"type":"integer","minimum":0},"description":"Route positions this contact applies to. Empty when notification_scope is 'all'.","example":[0]},"stops":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"position":{"type":"integer","minimum":0}},"required":["id","position"]},"description":"Route stops this contact applies to. Empty when notification_scope is 'all'."}},"required":["id","phone_number","notification_scope","stop_positions","stops"]},"Error":{"type":"object","additionalProperties":false,"properties":{"type":{"type":"string","description":"Type of error","enum":["api_error","authentication_error","invalid_request_error","rate_limit_error","validation_error"]},"message":{"type":"string","description":"Explanation of error"},"param":{"type":"string","description":"Parameter related to the error","nullable":true},"code":{"type":"string","description":"A short string indicating the error code reported","nullable":true},"http_status":{"type":"integer","description":"HTTP status code of the error response"},"request_id":{"type":"string","format":"uuid","description":"Unique identifier for this request for debugging purposes","nullable":true}},"required":["type","message","http_status"]},"Fare":{"type":"object","description":"Fare amounts are integer cents in USD.","properties":{"total":{"type":"integer","description":"Total fare in cents"},"base":{"type":"integer","description":"Base fare in cents"},"labor":{"type":"integer","description":"Labor fare in cents (origin + destination + intermediate stops)"},"travel_distance":{"type":"integer","description":"Distance-based fare in cents. Fixed-price distance charges are rounded down to a whole dollar."},"billed_miles":{"type":"integer","minimum":0,"description":"Whole distance miles used to calculate the distance-based fare. Route miles are rounded up before included mileage is subtracted."},"travel_duration":{"type":"integer","description":"Time-based travel fare in cents"},"stops":{"type":"integer","description":"Flat-rate charge total for intermediate stops in cents (excludes origin/destination)"},"adjustments":{"type":"array","description":"Post-creation fare adjustments (refunds, fees, credits, discounts, tips)","items":{"type":"object","properties":{"kind":{"type":"string","enum":["refund","fee","credit","discount","tip"],"description":"Adjustment type discriminator"},"name":{"type":"string","description":"Human-readable adjustment label"},"amount":{"type":"integer","description":"Adjustment amount in cents (negative for refunds/credits, positive for fees)"},"created_at":{"type":"string","format":"date-time"}}}}}},"FareStructure":{"type":"object","properties":{"base":{"type":"integer","description":"Base fare structure in cents"},"per_mile":{"type":"integer","description":"Per-mile rate in cents"},"included_miles":{"type":"integer","minimum":0,"description":"Whole route miles included before per-mile charges begin"},"per_minute_labor":{"type":"integer","description":"Per-minute labor rate in cents"},"per_minute_travel":{"type":"integer","description":"Per-minute travel rate in cents"},"per_stop":{"type":"integer","description":"Flat-rate charge per intermediate stop in cents"}}},"Location":{"type":"object","additionalProperties":false,"required":["id","coordinates","address"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"IKEA Emeryville","nullable":true},"address2":{"type":"string","nullable":true,"description":"Unit, apartment, suite, or other secondary address","example":"APT C"},"coordinates":{"$ref":"#/components/schemas/Coordinates"},"address":{"$ref":"#/components/schemas/Address"},"location_type":{"type":"string","enum":["street_address","establishment","approximate"],"description":"Type of location (street_address, establishment, or approximate for zip codes/cities)"}}},"LocationParam":{"oneOf":[{"type":"string","format":"uuid","description":"Location UUID from a previously created location","example":"123e4567-e89b-12d3-a456-426614174000"},{"type":"array","items":{"type":"number","format":"float"},"minItems":2,"maxItems":2,"description":"Coordinates as [longitude, latitude] in decimal degrees","example":[-122.4194,37.7749]},{"type":"string","description":"Free-form address string (street address, business name, or place)","example":"123 Main St, San Francisco, CA 94105"}]},"Organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Name of organization","example":"Lugg"}}},"PaginatedResponse":{"type":"object","properties":{"data":{"type":"array","description":"Array of requested resources"},"next":{"type":"string","format":"uri","nullable":true,"description":"URL for next page of results","example":"https://api.lugg.com/bookings?after=eyJ0aW1lc3RhbXAiOi..."},"previous":{"type":"string","format":"uri","nullable":true,"description":"URL for previous page of results","example":"https://api.lugg.com/bookings?before=eyJ0aW1lc3RhbXAiOi..."}},"required":["data","next","previous"]},"ProductEnum":{"type":"string","description":"Product type for the service","enum":["lite","pickup","van","xl","box"],"example":"pickup","x-enum-descriptions":{"lite":"Single lugger in a pickup truck","pickup":"Two luggers in a pickup truck","van":"Two luggers in a cargo van","xl":"Two luggers in a sprinter van","box":"Two luggers in a box truck"}},"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"origin":{"$ref":"#/components/schemas/Location"},"destination":{"$ref":"#/components/schemas/Location"},"route":{"type":"array","items":{"$ref":"#/components/schemas/RouteStop"},"description":"All stops in order (origin through destination)"},"travel_duration":{"type":"integer","description":"Travel duration in seconds"},"travel_distance":{"type":"number","format":"float","description":"Travel distance in miles"},"product":{"$ref":"#/components/schemas/ProductEnum"},"fare_structure":{"$ref":"#/components/schemas/FareStructure"},"fare_estimate":{"type":"array","items":{"$ref":"#/components/schemas/Fare"},"minItems":1,"maxItems":2,"description":"Fare estimate in cents. One fare object is a fixed quoted price. Two fare objects are [low, high] for variable-labor products; final fare may be calculated from actual job labor/time. Quote IDs are intended for near-term booking creation; create a new quote if an unused quote is stale or no longer found."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"RouteStop":{"type":"object","description":"A stop along the route","properties":{"id":{"type":"string","format":"uuid","description":"Unique identifier for the stop"},"position":{"type":"integer","description":"Position in the route (0 = origin, max = destination)","minimum":0},"location":{"$ref":"#/components/schemas/Location"},"instructions":{"type":"string","nullable":true,"description":"Operational instructions for this stop"}},"required":["id","position","location","instructions"]},"DispatchState":{"type":"string","description":"Normalized stop-based dispatch state.","enum":["stop_pending","stop_driving","stop_at","stop_done"],"x-enum-descriptions":{"stop_pending":"Waiting to dispatch to the current stop","stop_driving":"Luggers driving to the current stop","stop_at":"Luggers arrived at the current stop","stop_done":"Current stop completed"}},"DispatchEvent":{"type":"object","description":"A dispatch progress event scoped to this booking's stops","properties":{"state":{"$ref":"#/components/schemas/DispatchState"},"route_position":{"type":"integer","description":"Route position the event occurred at (0 = origin, 1..N = intermediate stops, N+1 = destination)","minimum":0},"route_stop_id":{"type":"string","format":"uuid","nullable":true,"description":"ID of the route stop the event occurred at"},"triggered_at":{"type":"string","format":"date-time","description":"When the event occurred"},"photo_urls":{"type":"array","items":{"type":"string","format":"uri"},"description":"Completion photo URLs captured for this stop event"}},"required":["state","route_position","triggered_at","photo_urls"]},"Dispatch":{"type":"object","description":"Current dispatch progress for a booking, with per-stop routing","properties":{"state":{"$ref":"#/components/schemas/DispatchState"},"route_position":{"type":"integer","description":"Current route position (0 = origin, 1..N = intermediate stops, N+1 = destination)","minimum":0},"route_stop_id":{"type":"string","format":"uuid","nullable":true,"description":"ID of the current route stop, referencing an entry in route[]"},"events":{"type":"array","items":{"$ref":"#/components/schemas/DispatchEvent"},"description":"Dispatch events scoped to this booking's stops, ordered by route position"}},"required":["state","route_position","events"]},"Schedule":{"type":"object","properties":{"product":{"$ref":"#/components/schemas/ProductEnum"},"ref":{"type":"string","enum":["origin","destination"],"description":"Reference point for arrival windows","example":"origin"},"time_zone":{"type":"string","description":"IANA time zone identifier for the arrival windows","example":"America/Los_Angeles"},"arrival_windows":{"type":"array","items":{"$ref":"#/components/schemas/ArrivalWindow"},"description":"Array of available arrival windows"}},"required":["product","ref","time_zone","arrival_windows"]},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the webhook endpoint","format":"uuid"},"url":{"type":"string","format":"uri","description":"The webhook endpoint URL"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventEnum"},"description":"List of subscribed event types","minItems":1},"description":{"type":"string","description":"Endpoint description","nullable":true},"channels":{"type":"array","items":{"type":"string"},"description":"List of channels for the endpoint"},"disabled":{"type":"boolean","description":"Whether the endpoint is disabled"},"created_at":{"type":"string","format":"date-time","description":"When the endpoint was created"}},"required":["id","url","events"]},"WebhookEventEnum":{"type":"string","enum":["booking.created","booking.dispatched","booking.completed","booking.cancelled","booking.dispatch_state_changed"],"description":"Types of webhook events that can be subscribed to","example":"booking.created","x-enum-descriptions":{"booking.created":"Fired when a new booking is created","booking.dispatched":"Fired when a booking is assigned to luggers","booking.completed":"Fired when a booking is successfully completed","booking.cancelled":"Fired when a booking is cancelled","booking.dispatch_state_changed":"Fired when luggers progress through pickup/delivery stages"}}}},"x-webhooks":{"booking.created":{"post":{"x-svix-group-name":"Bookings","summary":"Booking Created","description":"Fired when a new booking is created. Webhooks are delivered with signature headers for verification.","parameters":[{"in":"header","name":"svix-id","schema":{"type":"string"},"required":true,"description":"Unique webhook delivery ID"},{"in":"header","name":"svix-timestamp","schema":{"type":"string"},"required":true,"description":"Unix timestamp of when the webhook was sent"},{"in":"header","name":"svix-signature","schema":{"type":"string"},"required":true,"description":"Base64 encoded list of HMAC signatures for verification"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique event ID (hashed)","example":"a3f4b2c1d5e6f7a8"},"type":{"type":"string","description":"Type of event","example":"booking.created"},"triggered_at":{"type":"string","format":"date-time","description":"When the event was triggered"},"data":{"type":"object","properties":{"object":{"$ref":"#/components/schemas/Booking"}}}}}}}},"responses":{"200":{"description":"Webhook processed successfully"},"400":{"description":"Bad request"},"500":{"description":"Internal server error"}}}},"booking.dispatched":{"post":{"x-svix-group-name":"Bookings","summary":"Booking Dispatched","description":"Fired when a booking is dispatched to luggers. Webhooks are delivered with signature headers for verification.","parameters":[{"in":"header","name":"svix-id","schema":{"type":"string"},"required":true,"description":"Unique webhook delivery ID"},{"in":"header","name":"svix-timestamp","schema":{"type":"string"},"required":true,"description":"Unix timestamp of when the webhook was sent"},{"in":"header","name":"svix-signature","schema":{"type":"string"},"required":true,"description":"Base64 encoded list of HMAC signatures for verification"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique event ID (hashed)","example":"b4c5d6e7f8a9b1c2"},"type":{"type":"string","description":"Type of event","example":"booking.dispatched"},"triggered_at":{"type":"string","format":"date-time","description":"When the event was triggered"},"data":{"type":"object","properties":{"object":{"$ref":"#/components/schemas/Booking"}}}}}}}},"responses":{"200":{"description":"Webhook processed successfully"},"400":{"description":"Bad request"},"500":{"description":"Internal server error"}}}},"booking.completed":{"post":{"x-svix-group-name":"Bookings","summary":"Booking Completed","description":"Fired when a booking is completed. Webhooks are delivered with signature headers for verification.","parameters":[{"in":"header","name":"svix-id","schema":{"type":"string"},"required":true,"description":"Unique webhook delivery ID"},{"in":"header","name":"svix-timestamp","schema":{"type":"string"},"required":true,"description":"Unix timestamp of when the webhook was sent"},{"in":"header","name":"svix-signature","schema":{"type":"string"},"required":true,"description":"Base64 encoded list of HMAC signatures for verification"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique event ID (hashed)","example":"c6d7e8f9a1b2c3d4"},"type":{"type":"string","description":"Type of event","example":"booking.completed"},"triggered_at":{"type":"string","format":"date-time","description":"When the event was triggered"},"data":{"type":"object","properties":{"object":{"$ref":"#/components/schemas/Booking"}}}}}}}},"responses":{"200":{"description":"Webhook processed successfully"},"400":{"description":"Bad request"},"500":{"description":"Internal server error"}}}},"booking.cancelled":{"post":{"x-svix-group-name":"Bookings","summary":"Booking Cancelled","description":"Fired when a booking is cancelled. Webhooks are delivered with signature headers for verification.","parameters":[{"in":"header","name":"svix-id","schema":{"type":"string"},"required":true,"description":"Unique webhook delivery ID"},{"in":"header","name":"svix-timestamp","schema":{"type":"string"},"required":true,"description":"Unix timestamp of when the webhook was sent"},{"in":"header","name":"svix-signature","schema":{"type":"string"},"required":true,"description":"Base64 encoded list of HMAC signatures for verification"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique event ID (hashed)","example":"d8e9f1a2b3c4d5e6"},"type":{"type":"string","description":"Type of event","example":"booking.cancelled"},"triggered_at":{"type":"string","format":"date-time","description":"When the event was triggered"},"data":{"type":"object","properties":{"object":{"$ref":"#/components/schemas/Booking"}}}}}}}},"responses":{"200":{"description":"Webhook processed successfully"},"400":{"description":"Bad request"},"500":{"description":"Internal server error"}}}},"booking.dispatch_state_changed":{"post":{"x-svix-group-name":"Bookings","summary":"Booking Dispatch State Changed","description":"Fired when a booking's dispatch state changes (crew progress updates). Webhooks are delivered with signature headers for verification.","parameters":[{"in":"header","name":"svix-id","schema":{"type":"string"},"required":true,"description":"Unique webhook delivery ID"},{"in":"header","name":"svix-timestamp","schema":{"type":"string"},"required":true,"description":"Unix timestamp of when the webhook was sent"},{"in":"header","name":"svix-signature","schema":{"type":"string"},"required":true,"description":"Base64 encoded list of HMAC signatures for verification"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique event ID (hashed)","example":"e9f1a2b3c4d5e6f7"},"type":{"type":"string","description":"Type of event","example":"booking.dispatch_state_changed"},"triggered_at":{"type":"string","format":"date-time","description":"When the event was triggered"},"data":{"type":"object","properties":{"object":{"$ref":"#/components/schemas/Booking"}}}}}}}},"responses":{"200":{"description":"Webhook processed successfully"},"400":{"description":"Bad request"},"500":{"description":"Internal server error"}}}}}}