{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenData API",
    "version": "1.0.0",
    "summary": "Account and store-checker API for OpenData.",
    "description": "OpenData is a global ecommerce store database (23,984,359 domains scanned, 1,504,744 verified stores in the 15 Nov 2026 warehouse snapshot). This document describes the only HTTP API the site exposes today: website-account session endpoints used by the on-site store checker. The store database itself is delivered as CSV exports (see /pricing/); there is no bulk-data API and there are no API keys. Authentication is a website session cookie obtained by signing in with Google. Errors from /api/* are returned as structured JSON in the ErrorResponse shape.",
    "contact": {
      "name": "OpenData support",
      "email": "support@openbio.io.vn",
      "url": "https://opendatas.store/contact/"
    }
  },
  "servers": [
    {
      "url": "https://opendatas.store",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "account",
      "description": "Website account session information."
    },
    {
      "name": "checker",
      "description": "Store-checker unlock and download logging."
    },
    {
      "name": "segments",
      "description": "Saved checker segments on an account."
    },
    {
      "name": "auth",
      "description": "Google sign-in flow (browser redirects, HTML)."
    }
  ],
  "paths": {
    "/api/me": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "getCurrentAccount",
        "summary": "Current account, plan and checker usage",
        "description": "Returns whether the caller is signed in and, when signed in, the account's plan and checker unlock usage for the current calendar month (UTC). Works anonymously: signed-out callers get signed_in=false.",
        "responses": {
          "200": {
            "description": "Account state.",
            "headers": {
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Free-tier unlock quota policy, e.g. \"30;w=2592000\"."
              },
              "RateLimit": {
                "schema": {
                  "type": "string"
                },
                "description": "Rate-limit summary for the free-tier unlock quota."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/unlock": {
      "post": {
        "tags": [
          "checker"
        ],
        "operationId": "unlockCheckerRecord",
        "summary": "Record a checker lookup and unlock a found record",
        "description": "Used by the on-site store checker. Requires a signed-in account and a same-origin request. When found is false the lookup is only logged to history. When found is true, one monthly unlock is consumed unless the account's plan is unlimited; free accounts get 30 unlocks per calendar month (UTC). A lookup over quota is logged but not unlocked and returns ok=false with error \"quota_exceeded\" (HTTP 200).",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UnlockRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lookup processed; see ok/remaining in the body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON body or invalid domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/download": {
      "post": {
        "tags": [
          "checker"
        ],
        "operationId": "recordDownload",
        "summary": "Record an account download",
        "description": "Logs that the signed-in account downloaded an asset (for example a sample file), so it appears in the account's download history. Requires a signed-in account and a same-origin request.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DownloadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Download recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON body or invalid asset name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/segments": {
      "get": {
        "tags": [
          "segments"
        ],
        "operationId": "listSegments",
        "summary": "List the account's saved segments",
        "description": "Returns up to 100 saved segments for the signed-in account, newest first.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Saved segments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "segments"
        ],
        "operationId": "saveOrDeleteSegment",
        "summary": "Save a segment, or delete one with action=delete",
        "description": "With a label (and optional payload) saves a new segment and returns its id. With {\"action\": \"delete\", \"id\": N} deletes one of the account's own segments. Requires a signed-in account and a same-origin request.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SegmentSaveRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Segment saved (id returned) or deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentSaveResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (LegacyError body with the reason as the error code).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "segments"
        ],
        "operationId": "deleteSegment",
        "summary": "Delete a saved segment by id query parameter",
        "description": "Deletes one of the signed-in account's own segments. The id is passed as the ?id= query parameter. Requires a signed-in account and a same-origin request.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Numeric id of the segment to delete.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Segment deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/auth/google/start": {
      "get": {
        "tags": [
          "auth"
        ],
        "operationId": "startGoogleSignIn",
        "summary": "Start Google sign-in (browser redirect)",
        "description": "Browser endpoint, not a JSON API. Redirects (302) to Google's OAuth 2.0 authorization page with PKCE S256, setting short-lived state/nonce/verifier cookies scoped to /auth. When sign-in is not yet enabled on the server it returns an HTML 'opening soon' page instead.",
        "responses": {
          "200": {
            "description": "Sign-in is not yet enabled on the server: an HTML 'opening soon' page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "Redirect to accounts.google.com for sign-in.",
            "headers": {
              "Location": {
                "description": "Google OAuth 2.0 authorization URL (PKCE S256).",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "Set-Cookie": {
                "description": "Short-lived od_oauth_state, od_oauth_nonce and od_oauth_verifier cookies scoped to /auth (10 minutes, HttpOnly).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Too many sign-in attempts from this IP (HTML page, with Retry-After).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before trying again.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/auth/google/callback": {
      "get": {
        "tags": [
          "auth"
        ],
        "operationId": "googleSignInCallback",
        "summary": "Google sign-in callback",
        "description": "Browser endpoint, not a JSON API. Completes the OAuth flow: verifies the Google id_token, creates or updates the account, sets the od_session cookie (30 days, HttpOnly) and redirects (302) to /account/. Invalid or expired flows get an HTML error page (400).",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "required": true,
            "description": "Authorization code returned by Google.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": true,
            "description": "State value that must match the od_oauth_state cookie.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Sign-in succeeded; redirect to /account/ with the session cookie set.",
            "headers": {
              "Location": {
                "description": "The account page (/account/).",
                "schema": {
                  "type": "string",
                  "format": "uri-reference"
                }
              },
              "Set-Cookie": {
                "description": "The od_session session cookie (30 days, HttpOnly, Secure, SameSite=Lax); the short-lived OAuth cookies are cleared.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Sign-in did not complete (HTML error page).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/auth/signout": {
      "post": {
        "tags": [
          "auth"
        ],
        "operationId": "signOut",
        "summary": "Sign out",
        "description": "Deletes the current session and clears the od_session cookie. Requires a same-origin request. Returns JSON {\"ok\": true}.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (structured JSON).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/browse": {
      "get": {
        "summary": "Browse the store database with filters (session required)",
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tech",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "app",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "gateway",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "has_email",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of 25 stores plus total_matching and next_cursor. Free plans receive masked email fields."
          },
          "401": {
            "description": "Not signed in."
          }
        }
      }
    },
    "/api/browse/export": {
      "post": {
        "summary": "Export the current filter as CSV (session required, logged)",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "CSV file (12 columns). Caps: 100 rows free (emails masked), 1,000 rows paid."
          },
          "401": {
            "description": "Not signed in."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "od_session",
        "description": "Website session cookie set by the Google sign-in flow (/auth/google/start). 32 random bytes; only its SHA-256 hash is stored server-side. HttpOnly, Secure, SameSite=Lax, 30-day lifetime. There are no API keys and no OAuth scopes: the cookie is the only credential."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Structured error detail returned by /api/* for routing-level failures (unknown endpoint, uncaught errors).",
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code, e.g. not_found or internal_error."
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what went wrong."
          },
          "hint": {
            "type": "string",
            "description": "What to do next."
          }
        },
        "required": [
          "code",
          "message",
          "hint"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/Error"
          }
        },
        "required": [
          "error"
        ]
      },
      "Usage": {
        "type": "object",
        "properties": {
          "used": {
            "type": "integer",
            "description": "Unlocks used in the current calendar month (UTC)."
          },
          "limit": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Monthly unlock limit; null means unlimited (paid plans)."
          },
          "remaining": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unlocks remaining this month; null means unlimited."
          }
        },
        "required": [
          "used",
          "limit",
          "remaining"
        ]
      },
      "MeResponse": {
        "type": "object",
        "properties": {
          "signed_in": {
            "type": "boolean"
          },
          "auth_enabled": {
            "type": "boolean",
            "description": "Whether Google sign-in is configured on the server."
          },
          "user": {
            "type": "object",
            "description": "Present only when signed_in is true.",
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "avatar": {
                "type": "string"
              }
            },
            "required": [
              "name",
              "email",
              "avatar"
            ]
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "pro"
            ],
            "description": "Present only when signed in. Billing is handled by Lemon Squeezy; the plan is matched by email."
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        },
        "required": [
          "signed_in",
          "auth_enabled"
        ]
      },
      "UnlockRequest": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "Store domain that was checked; URLs are normalized to the bare domain."
          },
          "found": {
            "type": "boolean",
            "description": "Whether the checker found the domain in the current snapshot."
          }
        },
        "required": [
          "domain",
          "found"
        ]
      },
      "UnlockResponse": {
        "type": "object",
        "description": "On success ok=true. remaining is null for unlimited plans. When the free quota is exhausted the response is still HTTP 200 with ok=false and error=\"quota_exceeded\". A not-found lookup returns logged=true and consumes no quota.",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "logged": {
            "type": "boolean",
            "description": "Present on not-found lookups: recorded in history only."
          },
          "remaining": {
            "type": [
              "integer",
              "null"
            ]
          },
          "error": {
            "type": "string",
            "description": "Present when ok=false, e.g. quota_exceeded."
          }
        },
        "required": [
          "ok"
        ]
      },
      "DownloadRequest": {
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "description": "Asset identifier: letters, digits, dot, underscore or hyphen, up to 80 characters."
          }
        },
        "required": [
          "asset"
        ]
      },
      "OkResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ]
      },
      "Segment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "label": {
            "type": "string"
          },
          "payload": {
            "type": "string",
            "description": "JSON text with optional domain and/or filter keys; empty string when absent."
          },
          "ts": {
            "type": "string",
            "description": "ISO 8601 timestamp (UTC) when the segment was saved."
          }
        },
        "required": [
          "id",
          "label",
          "payload",
          "ts"
        ]
      },
      "SegmentListResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "segments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Segment"
            }
          }
        },
        "required": [
          "ok",
          "segments"
        ]
      },
      "SegmentSaveRequest": {
        "type": "object",
        "description": "Either a save ({label, payload?}) or a delete ({action: \"delete\", id}).",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 120,
            "description": "Segment label, 1-120 characters (save only)."
          },
          "payload": {
            "type": "object",
            "description": "Optional filter payload; only the domain and filter keys are kept.",
            "properties": {
              "domain": {
                "type": "string"
              },
              "filter": {
                "type": "string",
                "maxLength": 200
              }
            }
          },
          "action": {
            "type": "string",
            "enum": [
              "delete"
            ],
            "description": "Set to delete (with id) instead of saving."
          },
          "id": {
            "type": "integer",
            "description": "Segment id (delete only)."
          }
        }
      },
      "SegmentSaveResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id of the newly saved segment; null on delete."
          }
        },
        "required": [
          "ok"
        ]
      },
      "LegacyError": {
        "type": "object",
        "description": "Handler-level error body used by the account endpoints (authentication, origin and validation failures). Routing-level failures (unknown /api/* endpoints, uncaught errors) use the ErrorResponse shape instead.",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string",
            "description": "Machine-readable error code, e.g. not_signed_in, bad_origin, bad_domain, bad_json."
          }
        },
        "required": [
          "ok",
          "error"
        ]
      }
    }
  }
}