{
  "openapi": "3.0.3",
  "info": {
    "title": "East End Business Services API",
    "description": "Public API for East End Business Services (eebs.us). Versioned by URL path (/api/v1/, /api/v2/, ...). The current version is v1, exposed under /api/v1. The unversioned /api/* paths are deprecated aliases kept working for backward compatibility; every response from a deprecated path carries an RFC 8594 `Deprecation: true` header and a `Link` header with `rel=\"successor-version\"` pointing at its versioned replacement. A `Sunset` header will be added once a removal date for a deprecated path is scheduled — none is scheduled today. See the developer portal at https://eebs.us/developers for a quickstart.\n\nRate limiting: /api, /api/v1, /api/contact, /api/v1/contact, /api/v1/orders (all methods), and any unknown /api/* path are limited to 120 requests per 60-second window per client IP (POST /api/v1/orders/upload-url has its own, stricter limit of 20 per 60 seconds), tracked in a fixed window. Every response on those paths carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` (seconds until the window resets), per the conventions in draft-ietf-httpapi-ratelimit-headers, including error responses. A request over the limit gets `429` plus `Retry-After` (seconds). No authentication is required — the limit is per IP, not per key — and no API key exists to raise it; see /developers.",
    "version": "2.2.0",
    "contact": {
      "email": "takeoffs@eebs.us"
    }
  },
  "servers": [
    {
      "url": "https://www.eebs.us"
    }
  ],
  "paths": {
    "/api/v1/contact": {
      "post": {
        "summary": "Submit the contact form (v1, canonical)",
        "description": "Submits a contact inquiry from the eebs.us website and forwards it to East End Business Services.",
        "operationId": "submitContactFormV1",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message accepted and forwarded successfully.",
            "headers": {
              "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/ContactSuccess"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body (not valid JSON, or not a JSON object).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. Only POST (and OPTIONS for CORS preflight) is supported.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error — a required field is missing or Correo is not a valid email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The upstream form-delivery service failed or was unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1": {
      "get": {
        "summary": "API discovery index (v1, canonical)",
        "description": "Lists current endpoints and the versioning/deprecation policy.",
        "operationId": "getApiIndexV1",
        "responses": {
          "200": {
            "description": "Index of available v1 endpoints.",
            "headers": {
              "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/ApiIndex"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "summary": "Submit the contact form (deprecated alias)",
        "description": "Deprecated. Use POST /api/v1/contact instead. Kept working for backward compatibility; responses carry `Deprecation: true` and `Link: <.../api/v1/contact>; rel=\"successor-version\"` headers.",
        "operationId": "submitContactFormLegacy",
        "deprecated": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message accepted and forwarded successfully.",
            "headers": {
              "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/ContactSuccess"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body (not valid JSON, or not a JSON object).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. Only POST (and OPTIONS for CORS preflight) is supported.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error — a required field is missing or Correo is not a valid email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The upstream form-delivery service failed or was unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api": {
      "get": {
        "summary": "API discovery index (deprecated alias)",
        "description": "Deprecated. Use GET /api/v1 instead.",
        "operationId": "getApiIndexLegacy",
        "deprecated": true,
        "responses": {
          "200": {
            "description": "Index of available endpoints.",
            "headers": {
              "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/ApiIndex"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/orders/upload-url": {
      "post": {
        "summary": "Get a presigned upload URL for one plan PDF",
        "description": "Validates the file (application/pdf, at most 500 MB) and returns a 15-minute presigned PUT URL. Upload the file body directly to upload_url with the returned headers (Content-Type: application/pdf). The first call creates a site reference (ref); pass that ref on the following calls to add more PDFs to the same order. Rate limited to 20 requests per 60 seconds per IP. Anti-spam: the first call must include turnstile_token (Cloudflare Turnstile, obtained by the web form); the response then carries upload_token, which later calls for the same ref send instead (valid 30 minutes). These endpoints are intended for the web form at https://eebs.us/pedido — automated agents should use /api/v1/contact.",
        "operationId": "createOrderUploadUrlV1",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadUrlRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presigned upload URL created.",
            "headers": {
              "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/UploadUrlResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body (not valid JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. Only POST (and OPTIONS) is supported.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "FILE_TOO_LARGE — the file is larger than 500 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "UNSUPPORTED_MEDIA_TYPE — content_type is not application/pdf.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (filename, size or ref).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "NOT_CONFIGURED — uploads are temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "TURNSTILE_FAILED — the anti-spam token (or upload_token) is missing, invalid or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders": {
      "post": {
        "summary": "Submit a takeoff order",
        "description": "Submits a takeoff order whose PDFs were already uploaded with /api/v1/orders/upload-url. The order is delivered to East End Business Services, which emails the customer a quote and invoice; there is no online payment on this endpoint. The customer receives an 'order received' email. Returns pedido_id when the EEBS panel assigns one immediately, otherwise null (use ref as the reference). Requires acepto_terminos: true (acceptance of https://eebs.us/terminos and https://eebs.us/privacidad) and anti-spam proof: the upload_token returned by upload-url for this ref, or a fresh turnstile_token.",
        "operationId": "submitOrderV1",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order received.",
            "headers": {
              "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/OrderSuccess"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body (not valid JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed. Only POST (and OPTIONS) is supported.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "ORDER_ALREADY_SUBMITTED — this ref was already submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "FILE_TOO_LARGE — an uploaded file is larger than 500 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "415": {
            "description": "UNSUPPORTED_MEDIA_TYPE — an uploaded file is not a PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation error, TERMS_NOT_ACCEPTED when acepto_terminos is not true, or FILE_NOT_FOUND when a listed PDF was not uploaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "INTAKE_FAILED — the order could not be delivered; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "NOT_CONFIGURED — orders are temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "TURNSTILE_FAILED — the anti-spam token (or upload_token) is missing, invalid or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ContactRequest": {
        "type": "object",
        "required": [
          "Nombre",
          "Correo"
        ],
        "properties": {
          "Nombre": {
            "type": "string",
            "description": "Full name of the person contacting EEBS.",
            "example": "Maria Lopez"
          },
          "Correo": {
            "type": "string",
            "format": "email",
            "description": "Reply-to email address.",
            "example": "maria@example.com"
          },
          "Teléfono": {
            "type": "string",
            "description": "Optional phone number.",
            "example": "(631) 000-0000"
          },
          "Servicio": {
            "type": "string",
            "description": "Service the inquiry is about.",
            "enum": [
              "Takeoffs de Construcción",
              "Nómina / Payroll",
              "Organización Financiera",
              "Desarrollo Web",
              "Agente de WhatsApp con IA",
              "Todos los servicios",
              "Tengo una pregunta general"
            ]
          },
          "Mensaje": {
            "type": "string",
            "description": "Optional free-text message.",
            "example": "Necesito un estimado para un proyecto de framing."
          }
        }
      },
      "ContactSuccess": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "message": {
            "type": "string",
            "example": "Mensaje enviado. Te responderemos en menos de 24 horas."
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "example": "v1"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "developer_portal": {
            "type": "string",
            "format": "uri"
          },
          "versioning_policy": {
            "type": "object",
            "properties": {
              "scheme": {
                "type": "string"
              },
              "deprecation": {
                "type": "string"
              }
            }
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "method": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable error identifier.",
                "example": "VALIDATION_ERROR"
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation of what went wrong.",
                "example": "Missing required field(s): Correo."
              },
              "resolution": {
                "type": "string",
                "description": "How to fix the request.",
                "example": "Include Nombre and Correo in the request body."
              }
            }
          }
        }
      },
      "UploadUrlRequest": {
        "type": "object",
        "required": [
          "filename",
          "content_type",
          "size"
        ],
        "properties": {
          "filename": {
            "type": "string",
            "example": "planos-casa.pdf",
            "description": "Must end in .pdf."
          },
          "content_type": {
            "type": "string",
            "enum": [
              "application/pdf"
            ]
          },
          "size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 524288000,
            "description": "File size in bytes (max 500 MB)."
          },
          "ref": {
            "type": "string",
            "pattern": "^w-[a-z0-9]{12}$",
            "description": "Omit on the first file; reuse the returned ref for the rest."
          },
          "turnstile_token": {
            "type": "string",
            "description": "Cloudflare Turnstile token. Required on the first file (no ref)."
          },
          "upload_token": {
            "type": "string",
            "description": "Pass returned by the first call; required together with ref on the following files."
          }
        }
      },
      "UploadUrlResponse": {
        "type": "object",
        "properties": {
          "ref": {
            "type": "string",
            "example": "w-k3j9x0a2b7cd"
          },
          "key": {
            "type": "string",
            "example": "web/w-k3j9x0a2b7cd/planos-casa.pdf"
          },
          "upload_url": {
            "type": "string",
            "format": "uri"
          },
          "method": {
            "type": "string",
            "enum": [
              "PUT"
            ]
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "Content-Type": "application/pdf"
            }
          },
          "expires_in": {
            "type": "integer",
            "example": 900
          },
          "upload_token": {
            "type": "string",
            "description": "Signed pass for this ref (30 minutes); present when anti-spam is enabled."
          }
        }
      },
      "OrderRequest": {
        "type": "object",
        "required": [
          "ref",
          "tipo",
          "nombre",
          "email",
          "direccion",
          "ciudad_estado",
          "files",
          "acepto_terminos"
        ],
        "properties": {
          "ref": {
            "type": "string",
            "pattern": "^w-[a-z0-9]{12}$"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "roof",
              "exterior",
              "comercial",
              "otro"
            ]
          },
          "nombre": {
            "type": "string",
            "maxLength": 200
          },
          "compania": {
            "type": "string",
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "telefono": {
            "type": "string",
            "maxLength": 200
          },
          "direccion": {
            "type": "string",
            "maxLength": 200,
            "description": "Job site address."
          },
          "ciudad_estado": {
            "type": "string",
            "maxLength": 200,
            "example": "Southampton, NY"
          },
          "fecha_necesita": {
            "type": "string",
            "format": "date",
            "description": "Date the takeoff is needed (YYYY-MM-DD)."
          },
          "notas": {
            "type": "string",
            "maxLength": 5000
          },
          "files": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "type": "string"
            },
            "description": "Keys returned by upload-url for this ref."
          },
          "lang": {
            "type": "string",
            "enum": [
              "es",
              "en"
            ],
            "default": "es"
          },
          "acepto_terminos": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Must be true: the customer accepts https://eebs.us/terminos and https://eebs.us/privacidad."
          },
          "turnstile_token": {
            "type": "string",
            "description": "Fresh Cloudflare Turnstile token (single use). Either this or upload_token is required."
          },
          "upload_token": {
            "type": "string",
            "description": "Pass returned by upload-url for this ref (30 minutes). Either this or turnstile_token is required."
          }
        }
      },
      "OrderSuccess": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "ref": {
            "type": "string"
          },
          "pedido_id": {
            "type": "string",
            "nullable": true
          }
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "The request quota for the current 60-second window, per client IP.",
        "schema": {
          "type": "integer",
          "example": 120
        }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window for this client IP.",
        "schema": {
          "type": "integer",
          "example": 119
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the current window resets and the quota refills.",
        "schema": {
          "type": "integer",
          "example": 42
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Rate limit exceeded for this client IP on this path.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying — always equal to RateLimit-Reset.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "description": "Always 0 on a 429.",
            "schema": {
              "type": "integer",
              "example": 0
            }
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "RATE_LIMITED",
                "message": "Rate limit exceeded for /api/v1/contact.",
                "resolution": "This IP has exceeded the request limit for this window. Wait for Retry-After seconds, then retry. See /developers for the documented limit."
              }
            }
          }
        }
      }
    }
  }
}