{"openapi":"3.1.0","info":{"title":"tasa-bcv-api","version":"1.0.0","description":"API REST pública del histórico oficial de tasas de cambio del Banco Central de Venezuela (BCV) para USD/VES y EUR/VES. Las tasas se actualizan diariamente a las 23:00 America/Caracas (lun–vie). Las fechas de fin de semana y feriados devuelven tasas propagadas con `is_propagated: true`.","contact":{"name":"Repositorio","url":"https://github.com/Gisus07/tasa-bcv-api"},"license":{"name":"AGPL-3.0-or-later","url":"https://www.gnu.org/licenses/agpl-3.0.html"}},"servers":[{"url":"https://tasa-bcv-api-production.up.railway.app","description":"Producción"}],"tags":[{"name":"rates","description":"Consultas de tasas de cambio"},{"name":"parallel","description":"Tasa paralela (Binance P2P, USDT/VES)"},{"name":"intervention","description":"Intervención cambiaria del BCV (Bs./EUR)"},{"name":"keys","description":"Gestión de API keys del tier Free"},{"name":"system","description":"Estado y metadatos"},{"name":"admin","description":"Endpoints administrativos (requieren bearer token)"}],"externalDocs":{"description":"Código fuente y documentación extendida","url":"https://github.com/Gisus07/tasa-bcv-api"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"HealthResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"db":{"type":"string","enum":["reachable"]},"uptimeSeconds":{"type":"number","minimum":0}},"required":["status","db","uptimeSeconds"]},"RatesPair":{"type":"object","properties":{"date":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"usd":{"type":"number","minimum":0,"exclusiveMinimum":true,"example":510.7873},"eur":{"type":"number","minimum":0,"exclusiveMinimum":true,"example":598.12171255},"propagated_currencies":{"type":"array","items":{"type":"string","enum":["USD","EUR"],"description":"Código ISO 4217 de la moneda"},"minItems":1,"description":"Solo aparece cuando alguna de las tasas fue propagada (heredada del último día hábil). Lista los códigos ISO de las monedas propagadas."}},"required":["date","usd","eur"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string","example":"La fecha 2030-01-01 está en el futuro."},"code":{"type":"string","example":"DATE_OUT_OF_RANGE"},"details":{"type":"object","additionalProperties":{"nullable":true}}},"required":["error","code"]},"SingleRate":{"type":"object","properties":{"date":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"currency":{"type":"string","enum":["USD","EUR"],"description":"Código ISO 4217 de la moneda"},"rate":{"type":"number","minimum":0,"exclusiveMinimum":true,"description":"Tasa de venta oficial publicada por el BCV","example":510.7873},"is_propagated":{"type":"boolean","enum":[true],"description":"Solo aparece cuando el valor fue heredado (fin de semana o feriado). En días con publicación real este campo no se incluye."},"propagated_from":{"type":"string","description":"Fecha origen de la propagación. Solo presente cuando `is_propagated` lo está.","example":"2026-05-14"}},"required":["date","currency","rate"]},"RangeResponse":{"type":"object","properties":{"from":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"to":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"count":{"type":"integer","minimum":0},"rates":{"type":"array","items":{"$ref":"#/components/schemas/SingleRate"}}},"required":["from","to","count","rates"]},"LastUpdatedResponse":{"type":"object","properties":{"has_data":{"type":"boolean","description":"false cuando aún no hay ningún ingest exitoso (base recién creada)."},"last_successful_run_at":{"type":"string","nullable":true,"format":"date-time"},"last_successful_job_type":{"type":"string","nullable":true},"rows_upserted":{"type":"integer","nullable":true,"minimum":0}},"required":["has_data","last_successful_run_at","last_successful_job_type","rows_upserted"]},"ParallelLatest":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time","example":"2026-05-23T17:00:00.000Z"},"currency_pair":{"type":"string","example":"USDT/VES"},"buy":{"type":"number","minimum":0,"exclusiveMinimum":true,"description":"Bs para comprar 1 USDT (mediana top-10)","example":722.5},"sell":{"type":"number","minimum":0,"exclusiveMinimum":true,"description":"Bs al vender 1 USDT (mediana top-10)","example":721.1},"average":{"type":"number","minimum":0,"exclusiveMinimum":true,"example":721.8},"source":{"type":"string","example":"binance_p2p"}},"required":["timestamp","currency_pair","buy","sell","average","source"]},"ParallelSnapshot":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"buy":{"type":"number","minimum":0,"exclusiveMinimum":true},"sell":{"type":"number","minimum":0,"exclusiveMinimum":true},"average":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["timestamp","buy","sell","average"]},"ParallelHistoryResponse":{"type":"object","properties":{"from":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"to":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"count":{"type":"integer","minimum":0},"snapshots":{"type":"array","items":{"$ref":"#/components/schemas/ParallelSnapshot"}}},"required":["from","to","count","snapshots"]},"ParallelDailyItem":{"type":"object","properties":{"date":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"open":{"type":"number","minimum":0,"exclusiveMinimum":true},"high":{"type":"number","minimum":0,"exclusiveMinimum":true},"low":{"type":"number","minimum":0,"exclusiveMinimum":true},"close":{"type":"number","minimum":0,"exclusiveMinimum":true},"average":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["date","open","high","low","close","average"]},"ParallelDailyResponse":{"type":"object","properties":{"from":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"to":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"count":{"type":"integer","minimum":0},"days":{"type":"array","items":{"$ref":"#/components/schemas/ParallelDailyItem"}}},"required":["from","to","count","days"]},"InterventionLatest":{"type":"object","properties":{"date":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-21"},"intervention_number":{"type":"string","example":"011-26"},"currency_pair":{"type":"string","example":"EUR/VES"},"rate":{"type":"number","minimum":0,"exclusiveMinimum":true,"description":"Tipo de cambio de la intervención, Bs. por EUR","example":710.95},"source":{"type":"string","example":"bcv"}},"required":["date","intervention_number","currency_pair","rate","source"]},"InterventionItem":{"type":"object","properties":{"date":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"intervention_number":{"type":"string","example":"011-26"},"rate":{"type":"number","minimum":0,"exclusiveMinimum":true,"example":710.95}},"required":["date","intervention_number","rate"]},"InterventionHistoryResponse":{"type":"object","properties":{"from":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"to":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"currency_pair":{"type":"string","example":"EUR/VES"},"count":{"type":"integer","minimum":0},"interventions":{"type":"array","items":{"$ref":"#/components/schemas/InterventionItem"}}},"required":["from","to","currency_pair","count","interventions"]},"RegisterKeyResponse":{"type":"object","properties":{"key":{"type":"string","description":"La API key en texto plano. Guárdala AHORA — solo se muestra esta vez.","example":"tbk_a1b2c3d4e5f6789012345678abcdefabcdefabcdefabcdef"},"key_prefix":{"type":"string","description":"Prefijo visible que identifica la key (sin exponer el secreto).","example":"tbk_a1b2c3d4"},"tier":{"type":"string","example":"free"},"rate_limit_per_minute":{"type":"integer","minimum":0,"exclusiveMinimum":true,"example":300},"created_at":{"type":"string","format":"date-time"},"usage":{"type":"object","properties":{"tip":{"type":"string"}},"required":["tip"]}},"required":["key","key_prefix","tier","rate_limit_per_minute","created_at","usage"]},"RegisterKeyRequest":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","example":"dev@example.com"},"name":{"type":"string","minLength":1,"maxLength":120,"example":"Ana Pérez"},"purpose":{"type":"string","maxLength":500,"description":"Descripción opcional del uso planeado","example":"Calculadora de remesas en mi e-commerce"}},"required":["email","name"]},"KeyUsageDay":{"type":"object","properties":{"date":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"count":{"type":"integer","minimum":0,"example":87}},"required":["date","count"]},"KeyUsageResponse":{"type":"object","properties":{"key_prefix":{"type":"string"},"days":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Ventana solicitada","example":30},"total_requests":{"type":"integer","minimum":0,"description":"Suma de requests en la ventana (no incluye días sin uso, esos no se almacenan)."},"usage":{"type":"array","items":{"$ref":"#/components/schemas/KeyUsageDay"},"description":"Días con uso, ordenados de más reciente a más antiguo. Días sin uso no aparecen."}},"required":["key_prefix","days","total_requests","usage"]},"ApiKeyInfo":{"type":"object","properties":{"key_prefix":{"type":"string","example":"tbk_a1b2c3d4"},"name":{"type":"string"},"email":{"type":"string"},"purpose":{"type":"string","nullable":true},"tier":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","nullable":true,"format":"date-time"},"request_count":{"type":"integer","minimum":0}},"required":["key_prefix","name","email","purpose","tier","created_at","last_used_at","request_count"]},"RevokeKeyResponse":{"type":"object","properties":{"revoked":{"type":"boolean"},"id":{"type":"integer","minimum":0,"exclusiveMinimum":true},"message":{"type":"string"}},"required":["revoked","id","message"]},"TriggerIngestResponse":{"type":"object","properties":{"job_type":{"type":"string"},"started":{"type":"boolean"},"message":{"type":"string"}},"required":["job_type","started","message"]},"TriggerIngestRequest":{"type":"object","properties":{"await":{"type":"boolean","default":false,"description":"Cuando es true, espera a que la ingesta termine antes de responder."}}},"ReingestRequest":{"type":"object","properties":{"await":{"type":"boolean","default":false,"description":"Cuando es true, espera a que la re-ingesta termine antes de responder."}}},"AdminKeyEntry":{"type":"object","properties":{"id":{"type":"integer","minimum":0,"exclusiveMinimum":true},"key_prefix":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"purpose":{"type":"string","nullable":true},"tier":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","nullable":true,"format":"date-time"},"request_count":{"type":"integer","minimum":0},"revoked_at":{"type":"string","nullable":true,"format":"date-time"}},"required":["id","key_prefix","name","email","purpose","tier","created_at","last_used_at","request_count","revoked_at"]},"AdminKeyList":{"type":"object","properties":{"count":{"type":"integer","minimum":0},"keys":{"type":"array","items":{"$ref":"#/components/schemas/AdminKeyEntry"}}},"required":["count","keys"]}},"parameters":{}},"paths":{"/health":{"get":{"tags":["system"],"summary":"Comprobación de disponibilidad de la API y conectividad con la base de datos","responses":{"200":{"description":"La API y la base de datos están disponibles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"La base de datos no está disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/v1/rates/latest":{"get":{"tags":["rates"],"summary":"Últimas tasas USD y EUR","description":"Devuelve la tasa publicada más reciente para USD y EUR. Cada moneda puede tener su propia fecha si una se actualizó después que la otra.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/rates/latest\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/latest', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/latest', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/rates/latest\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/rates/latest');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/rates/latest\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Últimas tasas USD y EUR","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatesPair"},"example":{"date":"2026-05-19","usd":510.7873,"eur":602.18768455,"propagated_currencies":["USD"]}}}},"404":{"description":"Aún no hay tasas disponibles (ejecutar backfill o daily)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/rates/range":{"get":{"tags":["rates"],"summary":"Tasas históricas dentro de un rango (máx 365 días)","description":"Devuelve cada registro disponible dentro de `[from, to]`, opcionalmente filtrado por moneda. Los días propagados se incluyen con `is_propagated: true`.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/rates/range?from=2026-05-01&to=2026-05-19&currency=USD\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/range?from=2026-05-01&to=2026-05-19&currency=USD', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/range?from=2026-05-01&to=2026-05-19&currency=USD', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/rates/range?from=2026-05-01&to=2026-05-19&currency=USD\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/rates/range?from=2026-05-01&to=2026-05-19&currency=USD');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/rates/range?from=2026-05-01&to=2026-05-19&currency=USD\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"from","in":"query"},{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"to","in":"query"},{"schema":{"type":"string","enum":["USD","EUR","all"],"default":"all","description":"Filtro de moneda; \"all\" devuelve ambas"},"required":false,"name":"currency","in":"query"}],"responses":{"200":{"description":"Tasas dentro del rango solicitado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RangeResponse"},"example":{"from":"2026-05-14","to":"2026-05-17","count":4,"rates":[{"date":"2026-05-14","currency":"USD","rate":510.7873},{"date":"2026-05-15","currency":"USD","rate":510.7873,"is_propagated":true,"propagated_from":"2026-05-14"},{"date":"2026-05-16","currency":"USD","rate":510.7873,"is_propagated":true,"propagated_from":"2026-05-14"},{"date":"2026-05-17","currency":"USD","rate":510.7873,"is_propagated":true,"propagated_from":"2026-05-14"}]}}}},"400":{"description":"Rango inválido, demasiado grande, o fecha fuera de límites","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/rates/usd":{"get":{"tags":["rates"],"summary":"Tasa USD (la más reciente por defecto, o para una fecha dada)","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/rates/usd\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/usd', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/usd', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/rates/usd\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/rates/usd');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/rates/usd\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Si se omite, devuelve la tasa más reciente disponible.","example":"2026-05-14"},"required":false,"name":"date","in":"query"}],"responses":{"200":{"description":"Tasa USD","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleRate"},"examples":{"normal":{"summary":"Día con publicación","value":{"date":"2026-05-14","currency":"USD","rate":510.7873}},"propagated":{"summary":"Día propagado (sábado)","value":{"date":"2026-05-16","currency":"USD","rate":510.7873,"is_propagated":true,"propagated_from":"2026-05-14"}}}}}},"400":{"description":"Fecha fuera de rango o anterior al histórico disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No se encontró tasa USD","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/rates/eur":{"get":{"tags":["rates"],"summary":"Tasa EUR (la más reciente por defecto, o para una fecha dada)","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/rates/eur\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/eur', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/eur', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/rates/eur\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/rates/eur');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/rates/eur\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Si se omite, devuelve la tasa más reciente disponible.","example":"2026-05-14"},"required":false,"name":"date","in":"query"}],"responses":{"200":{"description":"Tasa EUR","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleRate"},"examples":{"normal":{"summary":"Día con publicación","value":{"date":"2026-05-14","currency":"EUR","rate":598.12171255}},"propagated":{"summary":"Día propagado (sábado)","value":{"date":"2026-05-16","currency":"EUR","rate":601.4520428,"is_propagated":true,"propagated_from":"2026-05-15"}}}}}},"400":{"description":"Fecha fuera de rango o anterior al histórico disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No se encontró tasa EUR","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/last-updated":{"get":{"tags":["system"],"summary":"Timestamp de la última ingesta exitosa","description":"Útil para monitoreo. Si este valor no avanza durante >36h, el job diario probablemente está fallando.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/last-updated\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/last-updated', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/last-updated', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/last-updated\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/last-updated');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/last-updated\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Detalles del último run exitoso","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LastUpdatedResponse"},"example":{"has_data":true,"last_successful_run_at":"2026-05-19T04:00:00.000Z","last_successful_job_type":"daily","rows_upserted":12}}}}}}},"/v1/parallel/latest":{"get":{"tags":["parallel"],"summary":"Tasa paralela en vivo (Binance P2P, USDT/VES)","description":"Tasa paralela actual (\"dólar Binance\", USDT/VES) consultada en vivo desde Binance P2P (cache de 30s). Mediana del top-10 de ofertas. `buy` = Bs para comprar 1 USDT, `sell` = al vender, `average` = referencia. Si Binance no responde, devuelve el último snapshot horario almacenado.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/parallel/latest\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/parallel/latest', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/parallel/latest', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/parallel/latest\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/parallel/latest');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/parallel/latest\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Tasa paralela actual","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParallelLatest"}}}},"404":{"description":"Sin datos (Binance no responde y no hay snapshots aún)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/parallel/history":{"get":{"tags":["parallel"],"summary":"Histórico horario de la tasa paralela","description":"Snapshots horarios de la tasa paralela (Binance P2P) en un rango de fechas (YYYY-MM-DD). Máximo 31 días por request; para rangos mayores usa /v1/parallel/daily.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/parallel/history?from=2026-05-22&to=2026-05-23\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/parallel/history?from=2026-05-22&to=2026-05-23', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/parallel/history?from=2026-05-22&to=2026-05-23', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/parallel/history?from=2026-05-22&to=2026-05-23\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/parallel/history?from=2026-05-22&to=2026-05-23');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/parallel/history?from=2026-05-22&to=2026-05-23\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"from","in":"query"},{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"to","in":"query"}],"responses":{"200":{"description":"Snapshots horarios en el rango","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParallelHistoryResponse"}}}},"400":{"description":"Rango inválido o mayor a 31 días","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/parallel/daily":{"get":{"tags":["parallel"],"summary":"Agregación diaria (OHLC) de la tasa paralela","description":"Velas diarias (open/high/low/close + promedio) de la tasa paralela, agregadas por día del calendario de Caracas. Ideal para gráficas largas. Máximo 365 días por request.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/parallel/daily?from=2026-04-01&to=2026-05-23\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/parallel/daily?from=2026-04-01&to=2026-05-23', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/parallel/daily?from=2026-04-01&to=2026-05-23', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/parallel/daily?from=2026-04-01&to=2026-05-23\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/parallel/daily?from=2026-04-01&to=2026-05-23');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/parallel/daily?from=2026-04-01&to=2026-05-23\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"from","in":"query"},{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"to","in":"query"}],"responses":{"200":{"description":"Velas diarias en el rango","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParallelDailyResponse"}}}},"400":{"description":"Rango inválido o mayor a 365 días","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/intervention/latest":{"get":{"tags":["intervention"],"summary":"Última intervención cambiaria del BCV","description":"Última intervención cambiaria publicada por el BCV, con su tipo de cambio en Bs./EUR. Es una serie INDEPENDIENTE de la tasa oficial USD/EUR. Para saber si hubo intervención hoy, compara el campo `date` con la fecha actual. Solo ocurre en días hábiles; no se propaga.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/intervention/latest\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/intervention/latest', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/intervention/latest', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/intervention/latest\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/intervention/latest');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/intervention/latest\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Última intervención registrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterventionLatest"}}}},"404":{"description":"Aún no hay intervenciones registradas","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/intervention/history":{"get":{"tags":["intervention"],"summary":"Histórico de intervenciones cambiarias","description":"Intervenciones cambiarias del BCV (tipo de cambio Bs./EUR) en un rango de fechas (YYYY-MM-DD). Máximo 366 días por request. Solo aparecen los días en que el BCV efectivamente intervino.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/intervention/history?from=2026-05-01&to=2026-05-21\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/intervention/history?from=2026-05-01&to=2026-05-21', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/intervention/history?from=2026-05-01&to=2026-05-21', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/intervention/history?from=2026-05-01&to=2026-05-21\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/intervention/history?from=2026-05-01&to=2026-05-21');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/intervention/history?from=2026-05-01&to=2026-05-21\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"from","in":"query"},{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"to","in":"query"}],"responses":{"200":{"description":"Intervenciones en el rango","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterventionHistoryResponse"}}}},"400":{"description":"Rango inválido o mayor a 366 días","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/register":{"post":{"tags":["keys"],"summary":"Crear una API key del tier Free (gratis)","description":"Auto-registro público. Devuelve la API key en texto plano UNA SOLA VEZ. Guárdala — no la podemos mostrar de nuevo, solo el prefijo visible. La key da derecho a 300 req/min (vs 30 req/min sin key). Sin verificación de email por ahora — si abusan, se revoca.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST \"https://tasa-bcv-api-production.up.railway.app/v1/keys/register\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\":\"dev@example.com\",\"name\":\"Ana Pérez\",\"purpose\":\"Mi proyecto\"}'"},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/register', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"email\":\"dev@example.com\",\"name\":\"Ana Pérez\",\"purpose\":\"Mi proyecto\"}),\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/register', { method: 'POST' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.post(\"https://tasa-bcv-api-production.up.railway.app/v1/keys/register\", json={\"email\":\"dev@example.com\",\"name\":\"Ana Pérez\",\"purpose\":\"Mi proyecto\"})\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/keys/register');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);\ncurl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['email' => 'dev@example.com', ''name' => 'Ana Pérez', ''purpose' => 'Mi proyecto']));\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n  \"bytes\"\n)\n\nfunc main() {\n  body := bytes.NewBufferString(\"{\\\"email\\\":\\\"dev@example.com\\\",\\\"name\\\":\\\"Ana Pérez\\\",\\\"purpose\\\":\\\"Mi proyecto\\\"}\")\n  req, _ := http.NewRequest(\"POST\", \"https://tasa-bcv-api-production.up.railway.app/v1/keys/register\", body)\n  req.Header.Set(\"Content-Type\", \"application/json\")\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterKeyRequest"}}}},"responses":{"201":{"description":"API key creada. Guarda `key` ahora mismo — solo se muestra una vez.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterKeyResponse"},"example":{"key":"tbk_a1b2c3d4e5f6789012345678abcdef1234567890abcdef12","key_prefix":"tbk_a1b2c3d4","tier":"free","rate_limit_per_minute":300,"created_at":"2026-05-19T18:30:00.000Z","usage":{"tip":"Envía el header `Authorization: Bearer <tu_key>` en cada request."}}}}},"400":{"description":"Entrada inválida (email mal formado, campos faltantes, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/me/usage":{"get":{"tags":["keys"],"summary":"Histograma diario de uso de mi API key","description":"Devuelve el conteo de requests por día en la ventana solicitada (default 30 días). Los días sin requests no aparecen — el cliente debe asumir 0 para los faltantes si necesita una serie completa. Ideal para gráficas de uso semanales o mensuales.","security":[{"bearerAuth":[]}],"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/keys/me/usage?days=30\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/me/usage?days=30', {\n  method: 'GET',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN },\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/me/usage?days=30', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/keys/me/usage?days=30\", headers=headers)\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/keys/me/usage?days=30');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN]);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/keys/me/usage?days=30\", nil)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":365,"default":30,"description":"Días hacia atrás a incluir (1-365). Default 30.","example":30},"required":false,"name":"days","in":"query"}],"responses":{"200":{"description":"Histograma diario","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyUsageResponse"},"example":{"key_prefix":"tbk_a1b2c3d4","days":30,"total_requests":412,"usage":[{"date":"2026-05-19","count":87},{"date":"2026-05-18","count":64},{"date":"2026-05-15","count":122},{"date":"2026-05-14","count":139}]}}}},"401":{"description":"Falta API key o es inválida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/keys/me":{"get":{"tags":["keys"],"summary":"Info de mi API key","description":"Devuelve metadata de la key que se está usando para autenticar la llamada: prefijo visible, nombre, email, tier, fecha de creación, última vez usada y request count.","security":[{"bearerAuth":[]}],"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/keys/me\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/me', {\n  method: 'GET',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN },\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/me', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/keys/me\", headers=headers)\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/keys/me');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN]);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/keys/me\", nil)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Información de la API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyInfo"},"example":{"key_prefix":"tbk_a1b2c3d4","name":"Ana Pérez","email":"dev@example.com","purpose":"Calculadora de remesas","tier":"free","created_at":"2026-05-19T18:30:00.000Z","last_used_at":"2026-05-19T19:15:42.123Z","request_count":142}}}},"401":{"description":"Falta API key o es inválida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["keys"],"summary":"Revocar mi propia API key","description":"El dueño de la key se auto-revoca. La key deja de funcionar de inmediato — siguientes requests con ella devuelven 401. Operación idempotente y final: una vez revocada, no se puede reactivar; hay que registrar una nueva.","security":[{"bearerAuth":[]}],"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X DELETE \"https://tasa-bcv-api-production.up.railway.app/v1/keys/me\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/me', {\n  method: 'DELETE',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN },\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/keys/me', { method: 'DELETE' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.post(\"https://tasa-bcv-api-production.up.railway.app/v1/keys/me\", headers=headers)\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/keys/me');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN]);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"DELETE\", \"https://tasa-bcv-api-production.up.railway.app/v1/keys/me\", nil)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Key revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeKeyResponse"},"example":{"revoked":true,"id":1,"message":"Tu API key fue revocada. Registra una nueva si necesitas seguir consumiendo la API."}}}},"401":{"description":"Falta API key o es inválida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/trigger-ingest":{"post":{"tags":["admin"],"summary":"Dispara una ingesta diaria manualmente","description":"Endpoint protegido con bearer token que fuerza un daily-update sin esperar al cron. Útil cuando el run programado falla o para refrescar tras un deploy. Con await=false (default) responde 202 y el job corre en background; con await=true espera a que termine y responde 200.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST \"https://tasa-bcv-api-production.up.railway.app/v1/admin/trigger-ingest\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"await\":false}'"},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/trigger-ingest', {\n  method: 'POST',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN, 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"await\":false}),\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/trigger-ingest', { method: 'POST' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.post(\"https://tasa-bcv-api-production.up.railway.app/v1/admin/trigger-ingest\", headers=headers, json={\"await\":false})\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/admin/trigger-ingest');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN, 'Content-Type: application/json']);\ncurl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['await' => false]));\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n  \"bytes\"\n)\n\nfunc main() {\n  body := bytes.NewBufferString(\"{\\\"await\\\":false}\")\n  req, _ := http.NewRequest(\"POST\", \"https://tasa-bcv-api-production.up.railway.app/v1/admin/trigger-ingest\", body)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  req.Header.Set(\"Content-Type\", \"application/json\")\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerIngestRequest"}}}},"responses":{"200":{"description":"Ingesta completada (cuando `await=true`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerIngestResponse"}}}},"202":{"description":"Job iniciado en background (cuando `await=false`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerIngestResponse"}}}},"401":{"description":"Token admin faltante o inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/reingest":{"post":{"tags":["admin"],"summary":"Re-ingesta total del histórico (corrige datos mal cargados)","description":"Endpoint protegido con bearer token que re-ejecuta el backfill completo: re-lee los XLS de USD y EUR del BCV y re-propaga los huecos. Idempotente (sobrescribe solo lo que cambió) y con lock de 30 min. Úsalo para corregir fechas que entraron mal en una carga previa (p. ej. un XLS publicado con lag). Con await=false (default) responde 202 y corre en background; con await=true espera a que termine y responde 200.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST \"https://tasa-bcv-api-production.up.railway.app/v1/admin/reingest\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"await\":false}'"},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/reingest', {\n  method: 'POST',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN, 'Content-Type': 'application/json' },\n  body: JSON.stringify({\"await\":false}),\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/reingest', { method: 'POST' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.post(\"https://tasa-bcv-api-production.up.railway.app/v1/admin/reingest\", headers=headers, json={\"await\":false})\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/admin/reingest');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN, 'Content-Type: application/json']);\ncurl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['await' => false]));\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n  \"bytes\"\n)\n\nfunc main() {\n  body := bytes.NewBufferString(\"{\\\"await\\\":false}\")\n  req, _ := http.NewRequest(\"POST\", \"https://tasa-bcv-api-production.up.railway.app/v1/admin/reingest\", body)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  req.Header.Set(\"Content-Type\", \"application/json\")\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReingestRequest"}}}},"responses":{"200":{"description":"Re-ingesta completada (cuando await=true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerIngestResponse"}}}},"202":{"description":"Re-ingesta iniciada en background (cuando await=false)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerIngestResponse"}}}},"401":{"description":"Token admin faltante o inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/keys":{"get":{"tags":["admin"],"summary":"Listar todas las API keys (activas y revocadas)","description":"Devuelve metadata de todas las keys ordenadas por fecha de creación descendente. Limitado a 100 por respuesta. No incluye la key en texto plano — solo el prefijo visible y el resto de campos.","security":[{"bearerAuth":[]}],"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/admin/keys\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/keys', {\n  method: 'GET',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN },\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/keys', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/admin/keys\", headers=headers)\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/admin/keys');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN]);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/admin/keys\", nil)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"responses":{"200":{"description":"Listado de keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminKeyList"},"example":{"count":2,"keys":[{"id":2,"key_prefix":"tbk_b3c4d5e6","name":"Acme Calculator","email":"dev@acme.com","purpose":"Conversión de remesas","tier":"free","created_at":"2026-05-19T22:30:00.000Z","last_used_at":"2026-05-19T22:45:12.123Z","request_count":87,"revoked_at":null},{"id":1,"key_prefix":"tbk_a1b2c3d4","name":"Test","email":"test@example.com","purpose":null,"tier":"free","created_at":"2026-05-18T10:00:00.000Z","last_used_at":"2026-05-18T11:00:00.000Z","request_count":12,"revoked_at":"2026-05-19T20:00:00.000Z"}]}}}},"401":{"description":"Token admin faltante o inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/keys/{id}/revoke":{"post":{"tags":["admin"],"summary":"Revocar una API key por ID","description":"Soft-delete: marca la key como revocada (`revoked_at = now()`). La key revocada deja de autenticar de inmediato — siguientes requests con ella devuelven 401. Operación idempotente: revocar una key ya revocada responde 404 NOT_FOUND.","security":[{"bearerAuth":[]}],"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST \"https://tasa-bcv-api-production.up.railway.app/v1/admin/keys/1/revoke\" \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/keys/1/revoke', {\n  method: 'POST',\n  headers: { 'Authorization': 'Bearer ' + ADMIN_TOKEN },\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/admin/keys/1/revoke', { method: 'POST' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nheaders = {\"Authorization\": f\"Bearer {ADMIN_TOKEN}\"}\nr = requests.post(\"https://tasa-bcv-api-production.up.railway.app/v1/admin/keys/1/revoke\", headers=headers)\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/admin/keys/1/revoke');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $ADMIN_TOKEN]);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"POST\", \"https://tasa-bcv-api-production.up.railway.app/v1/admin/keys/1/revoke\", nil)\n  req.Header.Set(\"Authorization\", \"Bearer \" + adminToken)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"integer","minimum":0,"exclusiveMinimum":true,"example":1},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Key revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeKeyResponse"},"example":{"revoked":true,"id":1,"message":"API key 1 revocada. Futuras requests con esta key devolverán 401."}}}},"401":{"description":"Token admin faltante o inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Key no encontrada o ya estaba revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/rates/{date}":{"get":{"tags":["rates"],"summary":"Tasas USD y EUR para una fecha específica","description":"Devuelve ambas monedas para la fecha indicada. Fines de semana y feriados se devuelven con `propagated_currencies` indicando qué monedas se heredaron del último día hábil.","x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X GET \"https://tasa-bcv-api-production.up.railway.app/v1/rates/2026-05-14\""},{"lang":"javascript","label":"JavaScript (fetch)","source":"const res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/2026-05-14', {\n  method: 'GET',\n});\nconst data = await res.json();\nconsole.log(data);"},{"lang":"typescript","label":"TypeScript","source":"// Tipos sugeridos (válidos con Zod o tu validador favorito)\ninterface RateRecord { date: string; currency: 'USD' | 'EUR'; rate: number; is_propagated?: true; propagated_from?: string }\n\nconst res = await fetch('https://tasa-bcv-api-production.up.railway.app/v1/rates/2026-05-14', { method: 'GET' });\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst data = await res.json();"},{"lang":"python","label":"Python (requests)","source":"import requests\n\nr = requests.get(\"https://tasa-bcv-api-production.up.railway.app/v1/rates/2026-05-14\")\nr.raise_for_status()\nprint(r.json())"},{"lang":"php","label":"PHP (cURL)","source":"<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://tasa-bcv-api-production.up.railway.app/v1/rates/2026-05-14');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n$response = curl_exec($ch);\ncurl_close($ch);\n$data = json_decode($response, true);"},{"lang":"go","label":"Go (net/http)","source":"package main\n\nimport (\n  \"encoding/json\"\n  \"fmt\"\n  \"net/http\"\n)\n\nfunc main() {\n  req, _ := http.NewRequest(\"GET\", \"https://tasa-bcv-api-production.up.railway.app/v1/rates/2026-05-14\", nil)\n  res, _ := http.DefaultClient.Do(req)\n  defer res.Body.Close()\n  var data map[string]any\n  json.NewDecoder(res.Body).Decode(&data)\n  fmt.Println(data)\n}"}],"parameters":[{"schema":{"type":"string","description":"Fecha ISO YYYY-MM-DD","example":"2026-05-14"},"required":true,"name":"date","in":"path"}],"responses":{"200":{"description":"Tasas para la fecha solicitada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatesPair"},"examples":{"normal":{"summary":"Día hábil con publicación real","value":{"date":"2026-05-14","usd":510.7873,"eur":598.12171255}},"weekend":{"summary":"Fin de semana (ambas tasas propagadas)","value":{"date":"2026-05-16","usd":510.7873,"eur":601.4520428,"propagated_currencies":["USD","EUR"]}},"partial_propagation":{"summary":"Solo USD propagado (EUR sí publicó)","value":{"date":"2026-05-19","usd":510.7873,"eur":602.18768455,"propagated_currencies":["USD"]}},"pre_redenomination":{"summary":"Pre-redenominación 2021 (Bs.S, magnitudes ~10^5)","value":{"date":"2020-03-27","usd":73830.1638,"eur":81349.02768139}}}}}},"400":{"description":"Fecha fuera de rango o anterior al histórico disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"out_of_range":{"summary":"Fecha futura","value":{"error":"La fecha 2027-01-01 está en el futuro. Máxima permitida: 2026-05-20.","code":"DATE_OUT_OF_RANGE","details":{"date":"2027-01-01","maxDate":"2026-05-20"}}},"before_history":{"summary":"Fecha anterior al histórico","value":{"error":"No hay datos históricos para USD antes de 2016-01-04. Solicitado: 2015-12-31.","code":"DATE_BEFORE_HISTORY","details":{"date":"2015-12-31","minDate":"2016-01-04","currency":"USD"}}},"invalid_format":{"summary":"Formato inválido","value":{"error":"Entrada inválida","code":"VALIDATION_ERROR","details":{"issues":[{"path":"date","message":"La fecha debe estar en formato YYYY-MM-DD y ser un día válido del calendario gregoriano","code":"custom"}]}}}}}}},"404":{"description":"No hay tasas para esa fecha (datos aún no ingresados)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}