{
  "openapi": "3.1.0",
  "info": {
    "title": "Diamond Stars Dry Cleaners, AI agent booking API",
    "version": "2.2.0",
    "summary": "Book a free laundry and dry cleaning pickup in Dubai on behalf of a customer.",
    "description": "For AI agents and assistants (ChatGPT agent, Claude, Gemini, Grok, Manus, Meta AI, Perplexity, Copilot and others). One call creates a pickup request for a chosen date and time. Diamond Stars then confirms the pickup with the customer on WhatsApp. Pickup and delivery are free across Dubai. Payment is on delivery, so no card or payment step is needed. Always get the customer's consent before booking and send their real details: WhatsApp number, email, pickup address, pickup date and pickup time are all required. Opening hours (Dubai time, UTC+4): Monday to Thursday, Saturday and Sunday 08:30 to 14:00 and 16:00 to 23:30; Friday 14:00 to 23:30. Before booking: GET /api/agent/services for the exact service names, GET /api/agent/prices for the price list and GET /api/agent/pickup-slots for valid pickup dates and times. The same price list is a static file at https://diamondstarcleaners.com/prices.json, and business facts are in https://diamondstarcleaners.com/llms.txt",
    "contact": {
      "name": "Diamond Stars Dry Cleaners",
      "url": "https://diamondstarcleaners.com/",
      "email": "info@diamondstarcleaners.com"
    }
  },
  "servers": [{ "url": "https://diamondstarcleaners.com" }],
  "paths": {
    "/api/agent/pickup-request": {
      "post": {
        "operationId": "requestPickup",
        "summary": "Request a laundry or dry cleaning pickup in Dubai",
        "description": "Creates a pickup request. Name the agent in the agent field so the team knows who booked. Required: agent, name, whatsapp, email, address, pickupDate, pickupTime and services. pickupDate is YYYY-MM-DD, from today up to 30 days ahead, Dubai time. pickupTime is HH:MM (24 hour, Dubai time) inside opening hours: Monday to Thursday, Saturday and Sunday 08:30 to 14:00 and 16:00 to 23:30; Friday 14:00 to 23:30. Closing time itself is not bookable. For a pickup today the time must be at least 30 minutes from now. Not offered: upholstery, sofa and mattress cleaning, blind cleaning and self-service laundromat (removable sofa and cushion covers and mattress protectors are fine). Minimum order: AED 65 for dry cleaning, AED 100 for carpets and curtains. Wedding dresses have no minimum (from AED 400). Send the body as JSON (Content-Type: application/json; text/plain holding JSON is accepted too). CORS is open for this endpoint (no credentials).",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional unique key (UUID). Same as the idempotencyKey body field (a valid body field wins; an invalid one is ignored and this header is used). Sending the same key again returns the first response instead of a duplicate, even after the pickup date has passed.",
            "schema": { "type": "string", "maxLength": 128, "pattern": "^[A-Za-z0-9_-]+$" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PickupRequest" },
              "examples": {
                "booking": {
                  "summary": "Dry cleaning and curtains pickup",
                  "description": "pickupDate and pickupTime here are placeholders that go out of date. Use a real slot from GET /api/agent/pickup-slots: a date from today up to 30 days ahead and a time inside one of that date's windows (Dubai time).",
                  "value": {
                    "agent": { "name": "ChatGPT agent", "platform": "OpenAI" },
                    "name": "Aisha Al Mansoori",
                    "whatsapp": "+971501234567",
                    "email": "aisha@example.com",
                    "address": "Marina Gate 1, Apt 1204, Dubai Marina, Dubai",
                    "pickupDate": "2027-01-12",
                    "pickupTime": "10:30",
                    "services": ["Dry Cleaning", "Curtains Cleaning"],
                    "notes": "Please call from the lobby.",
                    "idempotencyKey": "b7c1d2e3-4f56-4a7b-8c9d-0e1f2a3b4c5d"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pickup request received. A replay with the same idempotency key returns the first orderId with duplicate set to true.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PickupResponse" }
              }
            }
          },
          "400": {
            "description": "Missing or invalid field, or a body that is not valid JSON. The message names the field, and errors lists each field with its problem.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "A request with the same idempotency key is still being processed. Retry after the Retry-After seconds with the same key to get its result.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "415": {
            "description": "The body is not JSON. Send it with Content-Type: application/json.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": { "description": "Too many requests (per network, per customer WhatsApp number or per customer email). Retry after the number of seconds in the Retry-After header." },
          "500": {
            "description": "Server error. The request may not have been saved. Retry once with the same idempotency key (a replay never creates a duplicate). If it fails again, ask the customer to book on WhatsApp at +971 54 393 2526.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/api/agent/services": {
      "get": {
        "operationId": "listServices",
        "summary": "List the services you can book, with turnaround, minimum order and starting price",
        "description": "Returns the 7 exact service names the services field of POST /api/agent/pickup-request accepts, each with a short description, turnaround, minimum order in AED (null when there is none: wedding dresses have no minimum and start from AED 400; commercial laundry is quoted) and the starting price. Built from the same price data as https://diamondstarcleaners.com/prices/. CORS is open.",
        "responses": {
          "200": {
            "description": "The bookable services.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ServicesResponse" }
              }
            }
          },
          "503": { "description": "Price data not available on this server. Use https://diamondstarcleaners.com/prices.json instead." }
        }
      }
    },
    "/api/agent/prices": {
      "get": {
        "operationId": "getPrices",
        "summary": "Get the full price list in AED",
        "description": "The complete published price list: the 7 bookable services plus every price category and item (clean and press, press only, or a text price such as From AED 400), with item names in English, Arabic, Russian, Persian, French, Spanish, German and Dutch. Identical to the static file https://diamondstarcleaners.com/prices.json. CORS is open.",
        "responses": {
          "200": {
            "description": "The price list.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PriceList" }
              }
            }
          },
          "503": { "description": "Price data not available on this server. Use https://diamondstarcleaners.com/prices.json instead." }
        }
      }
    },
    "/api/agent/pickup-slots": {
      "get": {
        "operationId": "listPickupSlots",
        "summary": "List bookable pickup dates and time windows (Dubai time)",
        "description": "Pickup windows from the opening hours, checked with the same rules as POST /api/agent/pickup-request: Dubai time (UTC+4), today up to 30 days ahead, never in the past or within 30 minutes of now, and the closing time itself is not bookable. Any time from a window's start up to (not including) its end is accepted; times lists suggestions every 30 minutes. These are request windows, not reserved slots: staff confirm the exact pickup time with the customer on WhatsApp. CORS is open.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": false,
            "description": "One date, YYYY-MM-DD, from today up to 30 days ahead. Without it the next 7 days are listed.",
            "schema": { "type": "string", "format": "date", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "How many days to list from today when date is not given (1 to 31, default 7). Dates past the 30 day booking limit are never listed.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 31, "default": 7 }
          }
        ],
        "responses": {
          "200": {
            "description": "Bookable windows per date.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PickupSlotsResponse" }
              }
            }
          },
          "400": {
            "description": "Invalid date or days value. The message says what is accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PickupRequest": {
        "type": "object",
        "required": ["agent", "name", "whatsapp", "email", "address", "pickupDate", "pickupTime", "services"],
        "properties": {
          "agent": {
            "description": "Who is booking. Required. Either a name string such as \"Manus\" or an object such as {\"name\": \"Manus\", \"platform\": \"manus.im\"}.",
            "oneOf": [
              { "type": "string", "minLength": 2, "maxLength": 40 },
              {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string", "maxLength": 40, "examples": ["ChatGPT agent", "Claude", "Gemini", "Grok", "Manus", "Meta AI", "Perplexity", "Copilot"] },
                  "platform": { "type": "string", "maxLength": 60 },
                  "operator": { "type": "string", "maxLength": 60 }
                }
              }
            ]
          },
          "name": { "type": "string", "description": "Customer full name.", "minLength": 2, "maxLength": 80 },
          "whatsapp": {
            "type": "string",
            "description": "Customer WhatsApp number. Required. International format such as +971501234567 is best. Also accepted: 00 followed by the country code (00971501234567), and UAE local formats 0501234567, 501234567 and 971501234567. Spaces, brackets, dots and dashes are ignored. The aliases phone, mobile, customer.whatsapp and customer.phone are read too.",
            "pattern": "^\\+?[0-9 ().-]{7,24}$"
          },
          "email": { "type": "string", "format": "email", "maxLength": 254, "description": "Customer email. Required." },
          "address": {
            "type": "string",
            "description": "Full pickup address in Dubai: building, apartment or villa number and area. Required. The aliases customer.address and location are read too. A | character is stored as a comma.",
            "minLength": 5,
            "maxLength": 500
          },
          "pickupDate": {
            "type": "string",
            "format": "date",
            "description": "Pickup date as YYYY-MM-DD, from today up to 30 days ahead (Dubai time). Required. GET /api/agent/pickup-slots lists the valid dates and times.",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "pickupTime": {
            "type": "string",
            "description": "Pickup time as HH:MM, 24 hour, Dubai time. Required. Must be inside opening hours: Monday to Thursday, Saturday and Sunday 08:30 to 14:00 and 16:00 to 23:30; Friday 14:00 to 23:30. Closing time itself is not bookable. For a pickup today it must be at least 30 minutes from now.",
            "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
          },
          "services": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": ["Dry Cleaning", "Pressing", "Wash & Fold", "Carpet Cleaning", "Curtains Cleaning", "Wedding Dress Cleaning", "Commercial Laundry Cleaning"]
            },
            "description": "One or more services: send the exact names from GET /api/agent/services. Loose names such as curtains, carpet, rug, wedding dress, ironing, wash and fold, laundry, sofa or couch covers, duvet, comforter, blanket, bed sheets or mattress protector are also understood. Not offered: upholstery, sofa and mattress cleaning, blind cleaning and self-service laundromat."
          },
          "notes": { "type": "string", "maxLength": 300, "description": "Optional note for the team, for example access instructions. No links." },
          "customer": {
            "type": "object",
            "description": "Optional nested form. Fields here are read when the top-level field is missing.",
            "properties": {
              "name": { "type": "string" },
              "whatsapp": { "type": "string" },
              "phone": { "type": "string" },
              "email": { "type": "string", "format": "email", "maxLength": 254 },
              "address": { "type": "string", "maxLength": 500 }
            }
          },
          "promoCode": { "type": "string", "maxLength": 32 },
          "idempotencyKey": { "type": "string", "maxLength": 128, "pattern": "^[A-Za-z0-9_-]+$", "description": "Optional unique key (UUID). Sending the same key again returns the first response instead of a duplicate, even after the pickup date has passed. The Idempotency-Key header works too." }
        }
      },
      "PickupResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean" },
          "orderId": { "type": "integer" },
          "status": { "type": "string", "enum": ["received"] },
          "agent": { "type": "string", "description": "The agent name as the team sees it, for example ChatGPT Agent." },
          "duplicate": { "type": "boolean", "description": "true when this is a replay of an earlier request with the same idempotency key. No new order was created." },
          "pickupDate": { "type": "string", "format": "date" },
          "pickupTime": { "type": "string" },
          "nextStep": { "type": "string" }
        }
      },
      "AgentService": {
        "type": "object",
        "required": ["name", "description", "turnaround", "minimumOrderAed", "fromPriceAed", "fromPrice"],
        "properties": {
          "name": { "type": "string", "enum": ["Dry Cleaning", "Pressing", "Wash & Fold", "Carpet Cleaning", "Curtains Cleaning", "Wedding Dress Cleaning", "Commercial Laundry Cleaning"], "description": "Exact name to send in the services field of the booking request." },
          "description": { "type": "string" },
          "turnaround": { "type": "string", "examples": ["Next day (for example picked up Monday, delivered Tuesday)", "3 to 4 days"] },
          "turnaroundDays": { "type": ["object", "null"], "properties": { "min": { "type": "integer" }, "max": { "type": "integer" } } },
          "minimumOrderAed": { "type": ["number", "null"], "description": "Minimum order in AED. null when there is none (wedding dresses, from AED 400) or the price is quoted (commercial laundry)." },
          "fromPriceAed": { "type": ["number", "null"], "description": "Starting price in AED. null for commercial laundry (custom quote)." },
          "fromPrice": { "type": "string", "description": "The starting price as published, with its unit." },
          "page": { "type": "string", "format": "uri" }
        }
      },
      "ServicesResponse": {
        "type": "object",
        "properties": {
          "currency": { "type": "string", "const": "AED" },
          "services": { "type": "array", "items": { "$ref": "#/components/schemas/AgentService" }, "minItems": 7, "maxItems": 7 },
          "minimumOrders": { "type": "object" },
          "pickupAndDelivery": { "type": "string" },
          "payment": { "type": "string" },
          "note": { "type": "string" },
          "prices": { "type": "string", "format": "uri" }
        }
      },
      "PriceList": {
        "type": "object",
        "properties": {
          "currency": { "type": "string", "const": "AED" },
          "pickupAndDelivery": { "type": "string" },
          "payment": { "type": "string" },
          "minimumOrders": { "type": "object" },
          "services": { "type": "array", "items": { "$ref": "#/components/schemas/AgentService" } },
          "categories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "title": { "type": "string" },
                "fromPriceAed": { "type": ["number", "null"] },
                "note": { "type": "string" },
                "items": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": { "type": "string" },
                      "name": { "type": "string" },
                      "names": { "type": "object", "additionalProperties": { "type": "string" } },
                      "cleanAndPressAed": { "type": ["number", "string", "null"], "description": "Clean and press price in AED (a few items list two sizes as text, for example 6/8)." },
                      "pressOnlyAed": { "type": ["number", "null"], "description": "Press only (ironing) price in AED, when offered." },
                      "price": { "type": "string", "description": "Text price for assessed items, for example From AED 400." }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "PickupSlotsResponse": {
        "type": "object",
        "properties": {
          "timezone": { "type": "string", "const": "Asia/Dubai" },
          "utcOffset": { "type": "string", "const": "+04:00" },
          "now": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "time": { "type": "string" } } },
          "openingHours": { "type": "string" },
          "rules": {
            "type": "object",
            "properties": {
              "minLeadMinutes": { "type": "integer", "const": 30 },
              "maxDaysAhead": { "type": "integer", "const": 30 },
              "lastBookableDate": { "type": "string", "format": "date" },
              "closingTimeBookable": { "type": "boolean", "const": false }
            }
          },
          "note": { "type": "string" },
          "dates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": { "type": "string", "format": "date" },
                "weekday": { "type": "string" },
                "windows": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "start": { "type": "string", "description": "First bookable time, HH:MM." },
                      "end": { "type": "string", "description": "Closing time, HH:MM, not itself bookable." }
                    }
                  }
                },
                "times": { "type": "array", "items": { "type": "string" }, "description": "Suggested pickup times, HH:MM, every 30 minutes." }
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "message": { "type": "string", "description": "What is wrong, naming the field." },
          "errors": {
            "type": "object",
            "additionalProperties": { "type": "array", "items": { "type": "string" } }
          }
        }
      }
    }
  }
}
