{
  "openapi": "3.1.0",
  "info": {
    "title": "99 Junk Removal agent API",
    "version": "1.0.0",
    "description": "Check whether 99 Junk Removal serves a ZIP code, and request a junk removal pickup for a customer who asked for one. 99 Junk Removal will contact the customer by text, email or phone within an hour during business hours (8 a.m. to 6 p.m., every day) to confirm a time and price. Phone: (253) 722-4285."
  },
  "servers": [
    {
      "url": "https://99junkremoval.com"
    }
  ],
  "paths": {
    "/api/agent/service-area": {
      "get": {
        "operationId": "checkServiceArea",
        "summary": "Check whether a ZIP code is in the service area",
        "parameters": [
          {
            "name": "zip",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{5}$"
            },
            "description": "Five-digit ZIP code",
            "example": "98042"
          }
        ],
        "responses": {
          "200": {
            "description": "served is true or false when the ZIP code is known, and null when it is not on our list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "zip",
                    "served",
                    "phone",
                    "note"
                  ],
                  "properties": {
                    "zip": {
                      "type": "string"
                    },
                    "served": {
                      "type": [
                        "boolean",
                        "null"
                      ]
                    },
                    "city": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "page_url": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "phone": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The zip parameter is missing or is not five digits",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "BAD_ZIP"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/request": {
      "post": {
        "operationId": "requestPickup",
        "summary": "Request a junk removal pickup for a customer",
        "description": "Only call this when the customer asked you to request the pickup, and set customer_confirmed to true. Send first_name and last_name. Send description, photo_urls, or both.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PickupRequest"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/PickupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request received",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PickupReceived"
                }
              }
            }
          },
          "400": {
            "description": "A field is missing or invalid. fields maps each bad field to a code: REQUIRED, BAD_EMAIL, BAD_PHONE, BAD_ZIP, TOO_LONG, BAD_URL, BAD_TEXT, BAD_VALUE, NOT_CONFIRMED. BAD_TEXT means a name, street, unit or city value uses characters outside what that field allows, or contains a link, domain, \"@\" or angle bracket. An empty body gives REQUIRED for first_name, last_name, email, mobile, street, zip and preferred_date (the code for \"send a date or a timing\"), and NOT_CONFIRMED for customer_confirmed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "VALIDATION"
                    },
                    "fields": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 64 KB (16 KB for form-encoded)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "TOO_LARGE"
                    }
                  }
                }
              }
            }
          },
          "415": {
            "description": "Send application/json or application/x-www-form-urlencoded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "UNSUPPORTED_MEDIA"
                    },
                    "accepted": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this connection, phone number or email. Wait and retry, or call.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "RATE_LIMITED"
                    },
                    "retry_after_s": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The request could not be stored. Nothing was sent. Call (253) 722-4285.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "TEMPORARILY_UNAVAILABLE"
                    },
                    "phone": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PickupRequest": {
        "type": "object",
        "required": [
          "email",
          "mobile",
          "street",
          "zip",
          "customer_confirmed"
        ],
        "allOf": [
          {
            "anyOf": [
              {
                "required": [
                  "first_name",
                  "last_name"
                ]
              },
              {
                "required": [
                  "name"
                ]
              }
            ]
          },
          {
            "anyOf": [
              {
                "required": [
                  "description"
                ]
              },
              {
                "required": [
                  "photo_urls"
                ]
              }
            ]
          },
          {
            "anyOf": [
              {
                "required": [
                  "preferred_date"
                ]
              },
              {
                "required": [
                  "preferred_timing"
                ]
              }
            ]
          }
        ],
        "properties": {
          "first_name": {
            "type": "string",
            "maxLength": 40,
            "pattern": "^[\\p{L}\\p{M}' .-]+$",
            "description": "Letters, spaces, hyphens, apostrophes and periods only",
            "example": "Pat"
          },
          "last_name": {
            "type": "string",
            "maxLength": 40,
            "pattern": "^[\\p{L}\\p{M}' .-]+$",
            "example": "Example"
          },
          "name": {
            "type": "string",
            "maxLength": 81,
            "description": "Alternative to first_name and last_name, split on the last space. One word is rejected (last_name REQUIRED). Same character rules."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200
          },
          "mobile": {
            "type": "string",
            "description": "US mobile number with area code",
            "example": "(253) 555-0100"
          },
          "street": {
            "type": "string",
            "maxLength": 100,
            "pattern": "^\\d{1,6}[A-Za-z]? \\S",
            "description": "Pickup street address. Must start with the house number."
          },
          "unit": {
            "type": "string",
            "maxLength": 10,
            "pattern": "^[A-Za-z0-9 #-]+$",
            "example": "Apt 4B"
          },
          "city": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional and usually ignored: the city comes from the ZIP code. Used only when the ZIP code is not on our list."
          },
          "zip": {
            "type": "string",
            "pattern": "^\\d{5}$"
          },
          "preferred_date": {
            "type": "string",
            "description": "Preferred pickup date from today (Pacific time) to 60 days out. Send YYYY-MM-DD, or 11/7/2026, 11/7/26, Nov 7, November 7 or 7 Nov, with or without a weekday in front (no year means the next one). Send this or preferred_timing; the date wins if both are sent.",
            "example": "2026-11-07"
          },
          "preferred_timing": {
            "enum": [
              "today",
              "tomorrow",
              "this-week",
              "flexible"
            ],
            "description": "Rough timing when the customer has no date. Send this or preferred_date."
          },
          "preferred_window": {
            "enum": [
              "morning",
              "afternoon",
              "anytime"
            ],
            "default": "anytime"
          },
          "description": {
            "type": "string",
            "maxLength": 3000,
            "description": "What needs to go. Required unless photo_urls is given."
          },
          "photo_urls": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string",
              "format": "uri",
              "pattern": "^https://"
            },
            "description": "Up to five https links. They are stored as text and never opened by our servers."
          },
          "estimated_volume": {
            "enum": [
              "min",
              "1-8",
              "1-4",
              "1-2",
              "3-4",
              "full",
              "unknown"
            ],
            "description": "min, 1-8, 1-4, 1-2, 3-4, full or unknown. Used for the estimate in the response."
          },
          "is_business": {
            "type": "boolean"
          },
          "company": {
            "type": "string",
            "maxLength": 200
          },
          "agent_name": {
            "type": "string",
            "maxLength": 60,
            "description": "Name of the assistant making the request"
          },
          "customer_confirmed": {
            "type": "boolean",
            "const": true,
            "description": "You assert the customer asked you to request this pickup"
          }
        }
      },
      "PickupReceived": {
        "type": "object",
        "required": [
          "request_id",
          "status",
          "next_step",
          "phone",
          "minimum"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "const": "received"
          },
          "estimate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Price range and wording when estimated_volume was a tier"
          },
          "next_step": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "minimum": {
            "type": "string"
          }
        }
      }
    }
  }
}
