{
  "openapi": "3.1.0",
  "info": {
    "title": "Reserp Google Search API",
    "version": "1.0.0",
    "description": "Submit a Google Search URL and receive visible result blocks as structured JSON."
  },
  "externalDocs": {
    "description": "API documentation",
    "url": "https://reserp.ai/docs"
  },
  "servers": [
    {
      "url": "https://api.reserp.ai",
      "description": "Production"
    }
  ],
  "paths": {
    "/v1/serp": {
      "post": {
        "operationId": "searchGoogle",
        "summary": "Run a Google search",
        "description": "Accepts a complete https://www.google.com/search URL and returns visible result blocks in Google's response order. Requests use prepaid funding while balance is available, otherwise they automatically use monthly Free allowance. A response can include organic listings, news, carousels, sitelinks, and nested result blocks, so pagination uses Google's organic-result offset rather than results.length.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              },
              "example": {
                "url": "https://www.google.com/search?q=hotels+tokyo&gl=jp&hl=ja&start=10"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search completed successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                },
                "example": {
                  "ok": true,
                  "url": "https://www.google.com/search?q=hotels+tokyo&gl=jp&hl=ja&start=10",
                  "finalUrl": "https://www.google.com/search?q=hotels+tokyo&gl=jp&hl=ja&start=10",
                  "results": [
                    {
                      "text": "Tokyo hotels",
                      "url": "https://example.com/tokyo-hotels"
                    }
                  ],
                  "pagination": {
                    "start": 10,
                    "nextStart": 20,
                    "nextUrl": "https://www.google.com/search?q=hotels+tokyo&gl=jp&hl=ja&start=20"
                  },
                  "billed": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/AuthenticationFailed"
          },
          "402": {
            "$ref": "#/components/responses/FreeAllowanceExhausted"
          },
          "403": {
            "$ref": "#/components/responses/RequestNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/SearchFailed"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key",
        "description": "Reserp API key. Send it as Authorization: Bearer $API_KEY."
      }
    },
    "schemas": {
      "SearchRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 10000,
            "description": "A complete https://www.google.com/search URL with a non-empty q parameter. The num parameter is unsupported. If present, start must be 0 or a non-negative multiple of 10. Other Google URL parameters pass through unchanged."
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ok",
          "url",
          "finalUrl",
          "results",
          "pagination",
          "billed"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The normalized submitted Google Search URL."
          },
          "finalUrl": {
            "type": "string",
            "format": "uri",
            "description": "The final URL after Google redirects."
          },
          "results": {
            "type": "array",
            "description": "Visible result blocks in Google's response order.",
            "items": {
              "$ref": "#/components/schemas/Result"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          },
          "billed": {
            "type": "boolean",
            "description": "Whether billing settled by charging prepaid balance or consuming Free allowance."
          }
        }
      },
      "Pagination": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "start",
          "nextStart",
          "nextUrl"
        ],
        "properties": {
          "start": {
            "type": "integer",
            "minimum": 0,
            "multipleOf": 10,
            "description": "The current Google organic-result offset; defaults to 0."
          },
          "nextStart": {
            "type": "integer",
            "minimum": 10,
            "multipleOf": 10,
            "description": "The next Google offset, always start plus 10."
          },
          "nextUrl": {
            "type": "string",
            "format": "uri",
            "description": "The submitted search URL with start set to nextStart. Follow this URL instead of calculating from results.length."
          }
        }
      },
      "Result": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "description": "Visible text for the result block, joined with newlines. Omitted when the block has no visible text."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Normalized external URL when present."
          },
          "children": {
            "type": "array",
            "description": "Nested result blocks retained when flattening would lose a URL boundary or descendant content.",
            "items": {
              "$ref": "#/components/schemas/Result"
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "ok",
          "error",
          "retryable",
          "billed"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "authentication_failed",
              "free_allowance_exhausted",
              "request_not_allowed",
              "rate_limited",
              "internal_error",
              "search_failed",
              "service_unavailable"
            ],
            "description": "Stable public error code."
          },
          "retryable": {
            "type": "boolean",
            "description": "Retry only when true."
          },
          "billed": {
            "type": "boolean",
            "description": "Authoritative indication of whether billing settled before the error response."
          }
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "The JSON body or Google Search URL is invalid. This includes any num parameter or a start value that is not 0 or a non-negative multiple of 10; these failures are not billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "invalid_request",
              "retryable": false,
              "billed": false
            }
          }
        }
      },
      "AuthenticationFailed": {
        "description": "The bearer API key is missing or invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "authentication_failed",
              "retryable": false,
              "billed": false
            }
          }
        }
      },
      "FreeAllowanceExhausted": {
        "description": "The monthly Free allowance is exhausted and no prepaid balance is available.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "free_allowance_exhausted",
              "retryable": false,
              "billed": false
            }
          }
        }
      },
      "RequestNotAllowed": {
        "description": "The account or request is not permitted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "request_not_allowed",
              "retryable": false,
              "billed": true
            }
          }
        }
      },
      "RateLimited": {
        "description": "The account rate limit was exceeded.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "rate_limited",
              "retryable": true,
              "billed": false
            }
          }
        }
      },
      "InternalError": {
        "description": "An unexpected service error occurred.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "internal_error",
              "retryable": true,
              "billed": false
            }
          }
        }
      },
      "SearchFailed": {
        "description": "The search could not be completed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "search_failed",
              "retryable": true,
              "billed": true
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "The service is temporarily unavailable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": "service_unavailable",
              "retryable": true,
              "billed": false
            }
          }
        }
      }
    }
  }
}
