# API reference ## Authentication All endpoints except `POST /auth/register` and `GET /health` require an API key in the `Authorization` header: ``` Authorization: Bearer pp_a1b2c3d4e5f6g7h8i9j0... ``` API keys use a `pp_` prefix (proxy pool) followed by 48 random characters. The prefix aids visual identification and log filtering. Unauthenticated or invalid requests return `401 Unauthorized`. Requests with valid keys but insufficient credits return `402 Payment Required`. ## Common response patterns ### Pagination List endpoints support cursor-based pagination: ``` GET /proxies?limit=50&cursor=eyJpZCI6Ii4uLiJ9 ``` Response includes a `next_cursor` field when more results exist: ```json { "items": [...], "next_cursor": "eyJpZCI6Ii4uLiJ9", "total_count": 1234 } ``` ### Error responses All errors follow a consistent shape: ```json { "error": { "code": "INSUFFICIENT_CREDITS", "message": "You need at least 1 credit to acquire a proxy. Current balance: 0.", "details": {} } } ``` ## Proxy domain endpoints ### Sources #### `GET /sources` List all configured proxy sources. **Query parameters**: `is_active` (bool, optional), `limit` (int, default 50), `cursor` (string, optional). **Response** `200`: ```json { "items": [ { "id": "uuid", "url": "https://example.com/proxies.txt", "parser_name": "plaintext", "cron_schedule": "*/30 * * * *", "default_protocol": "http", "is_active": true, "last_scraped_at": "2025-01-15T10:30:00Z", "created_at": "2025-01-01T00:00:00Z" } ], "next_cursor": null, "total_count": 5 } ``` #### `POST /sources` Add a new proxy source. **Request body**: ```json { "url": "https://example.com/proxies.txt", "parser_name": "plaintext", "cron_schedule": "*/30 * * * *", "default_protocol": "http" } ``` **Validation**: The `parser_name` must match a registered plugin. If omitted, the registry attempts auto-detection via `supports()`. Returns `422` if no matching parser is found. **Response** `201`: The created source object. #### `PATCH /sources/{source_id}` Update a source. All fields are optional — only provided fields are changed. #### `DELETE /sources/{source_id}` Delete a source. Associated proxies are NOT deleted (they may have been discovered by multiple sources). #### `POST /sources/{source_id}/scrape` Trigger an immediate scrape of the source, bypassing the cron schedule. Returns the scrape result. **Response** `200`: ```json { "source_id": "uuid", "proxies_discovered": 142, "proxies_new": 23, "proxies_updated": 119, "duration_ms": 1540 } ``` ### Proxies #### `GET /proxies` Query the proxy pool with filtering and sorting. **Query parameters**: | Parameter | Type | Description | |-----------|------|-------------| | `status` | string | Filter by status: `active`, `dead`, `unchecked` | | `protocol` | string | Filter by protocol: `http`, `https`, `socks4`, `socks5` | | `anonymity` | string | Filter by anonymity: `transparent`, `anonymous`, `elite` | | `country` | string | ISO 3166-1 alpha-2 country code | | `min_score` | float | Minimum composite score (0.0–1.0) | | `max_latency_ms` | float | Maximum average latency | | `min_uptime_pct` | float | Minimum uptime percentage | | `verified_within_minutes` | int | Only proxies checked within the last N minutes | | `sort` | string | Sort field: `score`, `latency`, `uptime`, `last_checked` | | `order` | string | Sort order: `asc`, `desc` (default: `desc` for score) | | `limit` | int | Results per page (default: 50, max: 200) | | `cursor` | string | Pagination cursor | **Response** `200`: ```json { "items": [ { "id": "uuid", "ip": "203.0.113.42", "port": 8080, "protocol": "http", "status": "active", "anonymity": "elite", "exit_ip": "203.0.113.42", "country": "US", "score": 0.87, "avg_latency_ms": 245.3, "uptime_pct": 94.2, "last_checked_at": "2025-01-15T10:25:00Z", "first_seen_at": "2025-01-10T08:00:00Z", "tags": {"provider": "datacenter"} } ], "next_cursor": "...", "total_count": 892 } ``` #### `GET /proxies/{proxy_id}` Get detailed info for a single proxy, including recent check history. **Response** `200`: ```json { "proxy": { ... }, "recent_checks": [ { "checker_name": "tcp_connect", "stage": 1, "passed": true, "latency_ms": 120.5, "detail": "TCP connect OK", "created_at": "2025-01-15T10:25:00Z" } ] } ``` #### `POST /proxies/acquire` Acquire a proxy with an exclusive lease. Costs 1 credit. **Request body**: ```json { "protocol": "http", "country": "US", "anonymity": "elite", "min_score": 0.7, "lease_duration_seconds": 300 } ``` All filter fields are optional. `lease_duration_seconds` defaults to 300 (5 minutes), max 3600 (1 hour). **Response** `200`: ```json { "lease_id": "uuid", "proxy": { "ip": "203.0.113.42", "port": 8080, "protocol": "http", "country": "US", "anonymity": "elite" }, "expires_at": "2025-01-15T10:30:00Z", "credits_remaining": 42 } ``` **Error responses**: `402` if insufficient credits, `404` if no proxy matches the filters, `409` if all matching proxies are currently leased. #### `POST /proxies/acquire/{lease_id}/release` Release a lease early. The proxy becomes available immediately. The credit is NOT refunded (credits are consumed on acquisition). #### `POST /proxies/test` Test whether good proxies can reach a specific URL. **Request body**: ```json { "url": "https://example.com", "count": 5, "timeout_seconds": 10, "protocol": "http", "country": "US" } ``` **Response** `200`: ```json { "url": "https://example.com", "results": [ { "proxy_id": "uuid", "ip": "203.0.113.42", "port": 8080, "reachable": true, "status_code": 200, "latency_ms": 340, "error": null }, { "proxy_id": "uuid", "ip": "198.51.100.10", "port": 3128, "reachable": false, "status_code": null, "latency_ms": null, "error": "Connection refused by target" } ], "success_rate": 0.8 } ``` ### Stats #### `GET /stats/pool` Pool health overview. **Response** `200`: ```json { "total_proxies": 15420, "by_status": {"active": 3200, "dead": 11800, "unchecked": 420}, "by_protocol": {"http": 8000, "https": 4000, "socks4": 1200, "socks5": 2220}, "by_anonymity": {"transparent": 1500, "anonymous": 1000, "elite": 700}, "avg_score": 0.62, "avg_latency_ms": 380.5, "sources_active": 12, "sources_total": 15, "last_scrape_at": "2025-01-15T10:30:00Z", "last_validation_at": "2025-01-15T10:25:00Z" } ``` #### `GET /stats/plugins` Plugin registry status. **Response** `200`: ```json { "parsers": [{"name": "plaintext", "type": "SourceParser"}], "checkers": [ {"name": "tcp_connect", "stage": 1, "priority": 0}, {"name": "http_anonymity", "stage": 2, "priority": 0} ], "notifiers": [ {"name": "smtp", "healthy": true, "subscribes_to": ["proxy.pool_low", "credits.*"]}, {"name": "webhook", "healthy": false, "subscribes_to": ["*"]} ] } ``` ## Accounts domain endpoints ### Auth #### `POST /auth/register` Create a new user account and initial API key. No authentication required. **Request body**: ```json { "email": "user@example.com", "display_name": "Alice" } ``` **Response** `201`: ```json { "user": {"id": "uuid", "email": "user@example.com", "display_name": "Alice"}, "api_key": { "id": "uuid", "key": "pp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4", "prefix": "pp_a1b2c", "label": "default" } } ``` **Important**: The `key` field is the raw API key. It is returned ONLY in this response. Store it securely — it cannot be retrieved again. #### `POST /auth/keys` Create an additional API key for the authenticated user. #### `GET /auth/keys` List all API keys for the authenticated user (returns prefix and metadata, never the full key). #### `DELETE /auth/keys/{key_id}` Revoke an API key. ### Account #### `GET /account` Get the authenticated user's account info. #### `GET /account/credits` Get current credit balance and recent transaction history. **Response** `200`: ```json { "balance": 42, "recent_transactions": [ { "amount": -1, "tx_type": "acquire", "description": "Proxy acquired: 203.0.113.42:8080", "created_at": "2025-01-15T10:25:00Z" }, { "amount": 100, "tx_type": "purchase", "description": "Credit purchase", "created_at": "2025-01-14T00:00:00Z" } ] } ``` #### `GET /account/leases` List the authenticated user's active proxy leases. ## System endpoints #### `GET /health` Basic health check. No authentication required. **Response** `200`: ```json { "status": "healthy", "postgres": "connected", "redis": "connected", "version": "0.1.0" } ```