{
  "openapi": "3.1.0",
  "info": {
    "title": "PrepCentre centres feed and quote requests",
    "version": "1.0.0",
    "summary": "Listing data for the prep centres, 3PLs and fulfilment warehouses on PrepCentre.com, and the two quote request endpoints.",
    "description": "One static-style JSON document with every public listing on PrepCentre.com. It carries only what the public listing pages show, refreshes within minutes of those pages, and needs no key. Quote requests are made on the website, at each centre's quoteUrl, or by POST to /api/rfp (every matching centre) or /api/quotes (named centres). Requests sent through the API are held until the seller confirms by email within 48 hours: the seller gets one email with a confirm link, and no centre sees the request until they press Confirm. Sellers can send as many quote requests as they need.",
    "contact": {
      "name": "PrepCentre",
      "url": "https://prepcentre.com/"
    }
  },
  "servers": [
    {
      "url": "https://prepcentre.com"
    }
  ],
  "tags": [
    {
      "name": "Centres",
      "description": "Public listing data."
    },
    {
      "name": "Quote requests",
      "description": "Send a seller's quote request. Use the seller's own name and email, show them the exact request and get a clear yes first, and submit once."
    }
  ],
  "paths": {
    "/centres.json": {
      "get": {
        "operationId": "getCentres",
        "tags": ["Centres"],
        "summary": "Every public listing",
        "description": "All live listings, sorted by slug. The set matches the listing URLs in /sitemap.xml.",
        "security": [],
        "responses": {
          "200": {
            "description": "The centres feed.",
            "headers": {
              "Cache-Control": {
                "description": "public, max-age=3600",
                "schema": { "type": "string" }
              },
              "Access-Control-Allow-Origin": {
                "description": "Always *, so the feed can be read from any origin.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CentresFeed" }
              }
            }
          }
        }
      }
    },
    "/api/rfp": {
      "post": {
        "operationId": "requestQuotesFromMatchingCentres",
        "tags": ["Quote requests"],
        "summary": "Request quotes from every matching centre",
        "description": "One request, sent to every centre that offers a chosen service in the chosen country. The seller's receipt carries a private link to compare replies. Sent with source agent, from another origin or by a known AI agent, the request is held until the seller confirms by email within 48 hours (202); nothing reaches a centre before then.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RfpQuoteRequest" }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/Sent" },
          "202": { "$ref": "#/components/responses/PendingConfirmation" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/SendFailed" }
        }
      }
    },
    "/api/quotes": {
      "post": {
        "operationId": "requestQuotesFromNamedCentres",
        "tags": ["Quote requests"],
        "summary": "Request quotes from one to three named centres",
        "description": "One request to each named centre, by the slug in /centres.json. Sent with source agent, from another origin or by a known AI agent, the request is held until the seller confirms by email within 48 hours (202); nothing reaches a centre before then.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/DirectQuoteRequest" }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/Sent" },
          "202": { "$ref": "#/components/responses/PendingConfirmation" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/SendFailed" }
        }
      }
    }
  },
  "components": {
    "responses": {
      "Sent": {
        "description": "Sent straight to the centres, as from the website form. An agent request gets this only while the confirmation step is switched off.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/SentResponse" }
          }
        }
      },
      "PendingConfirmation": {
        "description": "Held. The seller has been emailed a confirm link; nothing reaches a centre until they press Confirm within 48 hours. After that the request expires and is never sent.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/PendingResponse" }
          }
        }
      },
      "BadRequest": {
        "description": "A field is missing or invalid. Fix the field named in field and send again.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests from this address or for this email. Wait a few minutes and try again.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "SendFailed": {
        "description": "The email could not be sent, so nothing was stored. Try again.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    },
    "schemas": {
      "CentresFeed": {
        "type": "object",
        "additionalProperties": false,
        "required": ["generatedAt", "count", "centres"],
        "properties": {
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When this copy of the feed was generated (UTC)."
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of entries in centres."
          },
          "centres": {
            "type": "array",
            "description": "Every public listing, sorted by slug.",
            "items": { "$ref": "#/components/schemas/Centre" }
          }
        }
      },
      "Centre": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name", "slug", "url", "quoteUrl", "description", "city", "region", "country",
          "services", "integrations", "marketplaces", "avgTurnaround", "yearFounded",
          "warehouseSqft", "monthlyCapacityUnits", "rating", "claimed", "warehouses"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The centre's name, as the listing page heading shows it."
          },
          "slug": {
            "type": "string",
            "pattern": "^[^/?#\\s]+$",
            "description": "Stable identifier; the listing page lives at /{slug}/."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The public listing page."
          },
          "quoteUrl": {
            "type": "string",
            "format": "uri",
            "description": "The quote request form, preselected for this centre. Sellers send quote requests at no cost."
          },
          "description": {
            "type": ["string", "null"],
            "description": "The About text on the listing page, or null when there is none."
          },
          "city": {
            "type": "string",
            "description": "City of the centre's primary location."
          },
          "region": {
            "type": ["string", "null"],
            "description": "State or province code of the primary location (for example NH or ON), or null where the listing shows none."
          },
          "country": {
            "$ref": "#/components/schemas/CountryCode"
          },
          "services": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Services listed for the centre (for example FBA Prep, Kitting & Bundling)."
          },
          "integrations": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Software integrations listed for the centre. Empty when none are listed."
          },
          "marketplaces": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["Amazon FBA", "Amazon FBM", "Walmart WFS", "TikTok Shop", "Shopify"]
            },
            "description": "Marketplaces the centre's owner says it serves. Empty when the owner has entered none."
          },
          "avgTurnaround": {
            "type": ["string", "null"],
            "description": "Average turnaround as the centre states it (free text), or null."
          },
          "yearFounded": {
            "type": ["integer", "null"],
            "description": "Year the centre was established, or null."
          },
          "warehouseSqft": {
            "type": ["integer", "null"],
            "minimum": 1,
            "description": "Warehouse size in square feet, or null."
          },
          "monthlyCapacityUnits": {
            "type": ["integer", "null"],
            "minimum": 1,
            "description": "Monthly capacity in units, or null."
          },
          "rating": {
            "$ref": "#/components/schemas/Rating"
          },
          "claimed": {
            "type": "boolean",
            "description": "True when the centre's owner has claimed the listing and manages it."
          },
          "warehouses": {
            "type": "array",
            "description": "Individual warehouse locations. Empty when the listing has none on record; city, region and country above are then its only location.",
            "items": { "$ref": "#/components/schemas/Warehouse" }
          }
        }
      },
      "Rating": {
        "type": "object",
        "additionalProperties": false,
        "required": ["average", "count"],
        "description": "Approved seller reviews.",
        "properties": {
          "average": {
            "type": ["number", "null"],
            "minimum": 1,
            "maximum": 5,
            "description": "Average star rating, one decimal place, or null with no reviews."
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of approved reviews."
          }
        }
      },
      "Warehouse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name", "city", "region", "country", "address", "postalCode", "description",
          "services", "warehouseSqft", "monthlyCapacityUnits", "avgTurnaround"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Warehouse name."
          },
          "city": {
            "type": "string",
            "description": "City."
          },
          "region": {
            "type": ["string", "null"],
            "description": "State or province code, or null where the listing shows none."
          },
          "country": {
            "$ref": "#/components/schemas/CountryCode"
          },
          "address": {
            "type": ["string", "null"],
            "description": "Street address lines, comma-separated, or null."
          },
          "postalCode": {
            "type": ["string", "null"],
            "description": "Postal or ZIP code, or null."
          },
          "description": {
            "type": ["string", "null"],
            "description": "About this warehouse. Given only for centres with more than one warehouse, as on the listing page; otherwise null."
          },
          "services": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Services offered at this warehouse."
          },
          "warehouseSqft": {
            "type": ["integer", "null"],
            "minimum": 1,
            "description": "Size in square feet, or null."
          },
          "monthlyCapacityUnits": {
            "type": ["integer", "null"],
            "minimum": 1,
            "description": "Monthly capacity in units, or null."
          },
          "avgTurnaround": {
            "type": ["string", "null"],
            "description": "Average turnaround at this warehouse (free text), or null."
          }
        }
      },
      "CountryCode": {
        "type": "string",
        "pattern": "^[A-Z]{2}$",
        "description": "ISO 3166-1 alpha-2 country code (for example US, CA, GB, DE)."
      },
      "RfpQuoteRequest": {
        "type": "object",
        "required": ["name", "email"],
        "description": "Give at least one service, or describe the requirements in message.",
        "properties": {
          "name": { "$ref": "#/components/schemas/SellerName" },
          "email": { "$ref": "#/components/schemas/SellerEmail" },
          "store": { "$ref": "#/components/schemas/Store" },
          "services": { "$ref": "#/components/schemas/Services" },
          "monthlyUnits": { "$ref": "#/components/schemas/MonthlyUnits" },
          "location": { "$ref": "#/components/schemas/Location" },
          "country": {
            "type": "string",
            "enum": ["US", "CA", "DE", "GB", "AT", "AU", "BR", "CN", "CZ", "ES", "FR", "IN", "IT", "MX", "PL", "PT", "SA", "TZ"],
            "description": "Where the centre should be, as an ISO 3166-1 alpha-2 code. Centres offering a chosen service in this country are matched."
          },
          "message": { "$ref": "#/components/schemas/Message" },
          "source": { "$ref": "#/components/schemas/Source" },
          "agentName": { "$ref": "#/components/schemas/AgentName" },
          "idempotencyKey": { "$ref": "#/components/schemas/IdempotencyKey" }
        }
      },
      "DirectQuoteRequest": {
        "type": "object",
        "required": ["name", "email", "centres"],
        "properties": {
          "name": { "$ref": "#/components/schemas/SellerName" },
          "email": { "$ref": "#/components/schemas/SellerEmail" },
          "centres": {
            "type": "array",
            "minItems": 1,
            "maxItems": 3,
            "uniqueItems": true,
            "items": { "type": "string", "minLength": 1 },
            "description": "Slugs of the centres to ask, from /centres.json. Up to 3."
          },
          "store": { "$ref": "#/components/schemas/Store" },
          "services": { "$ref": "#/components/schemas/Services" },
          "monthlyUnits": { "$ref": "#/components/schemas/MonthlyUnits" },
          "location": { "$ref": "#/components/schemas/Location" },
          "message": { "$ref": "#/components/schemas/Message" },
          "source": { "$ref": "#/components/schemas/Source" },
          "agentName": { "$ref": "#/components/schemas/AgentName" },
          "idempotencyKey": { "$ref": "#/components/schemas/IdempotencyKey" }
        }
      },
      "SellerName": {
        "type": "string",
        "minLength": 1,
        "description": "The seller's own name. Never invent one."
      },
      "SellerEmail": {
        "type": "string",
        "format": "email",
        "description": "The seller's own email. The confirm link and every reply go here, so never use your own address."
      },
      "Store": {
        "type": "string",
        "description": "Store or company name."
      },
      "Services": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": ["FBA Prep", "D2C Fulfilment", "Kitting & Bundling", "Labelling", "Storage", "Returns Processing", "Freight Forwarding", "Hazmat"]
        },
        "description": "Services needed. Hazmat tells the centre the products are hazardous; it is not matched on."
      },
      "MonthlyUnits": {
        "type": "string",
        "description": "Monthly units. The website offers Under 500, 500 to 2,000, 2,000 to 10,000, 10,000 to 50,000 and 50,000+."
      },
      "Location": {
        "type": "string",
        "description": "Preferred state, province or city, in words."
      },
      "Message": {
        "type": "string",
        "maxLength": 5000,
        "description": "Additional details: marketplaces, product category, SKU count, special handling, start date."
      },
      "Source": {
        "type": "string",
        "const": "agent",
        "description": "Send agent when you are an AI agent acting for the seller. Requests from another origin or a known AI agent User-Agent are treated the same way."
      },
      "AgentName": {
        "type": "string",
        "maxLength": 100,
        "description": "Your product name, for example the assistant's name. Stored as plain text."
      },
      "IdempotencyKey": {
        "type": "string",
        "minLength": 1,
        "maxLength": 100,
        "description": "Optional. Sending the same key with the same email within 24 hours returns the first response and stores and sends nothing new, so a retry is safe."
      },
      "SentResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["success"],
        "properties": {
          "success": { "type": "boolean", "const": true, "description": "Always true." }
        }
      },
      "PendingResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["status", "message"],
        "properties": {
          "status": { "type": "string", "const": "pending_confirmation", "description": "Held until the seller confirms by email." },
          "message": { "type": "string", "description": "Text to pass on to the seller." }
        }
      },
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "required": ["error", "code"],
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong, in words a seller can read."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_json", "invalid_contact", "unsupported_country", "invalid_collection", "invalid_idempotency_key",
              "missing_requirements", "missing_centre", "centre_not_found", "rate_limited", "send_failed"
            ],
            "description": "Stable error code. invalid_json: the body is not a JSON object. invalid_contact: name or email missing or malformed. unsupported_country: country is not one of the listed codes. invalid_collection: a collection (sent only by the website's own pages) is unknown. invalid_idempotency_key: not a string of 1 to 100 characters. missing_requirements: no service and no message. missing_centre: no centre slug given. centre_not_found: none of the slugs is a live centre. rate_limited: wait and try again. send_failed: the email could not be sent and nothing was stored."
          },
          "field": {
            "type": "string",
            "description": "The request field at fault, when there is one."
          }
        }
      }
    }
  }
}
