Primeros pasos con la API de subastas del BOE
Guía de la API de subastas del BOE: pedir una ficha, decodificar sus bloques, leer las pujas y consultar solo lo que ha cambiado.
Esta guía recorre lo mínimo para sacar partido a la API: leer una subasta, entender la forma de los datos y montar un seguimiento que no descargue todo cada vez. Los ejemplos usan curl y Python, pero sirve cualquier cliente HTTP.
Necesitas la URL base y un token. Ambos se entregan con el acceso; aquí aparecen como $BASE y $TOKEN.
1. Pedir una subasta
El token va en la cabecera Authorization, tal cual, sin Bearer delante.
curl "$BASE/api/auctions/SUB-JA-2026-000000" \
-H "Authorization: $TOKEN" \
-H "Accept: application/json"
El identificador es el mismo que usa el portal. Si no existe en la base, la respuesta es un 404; si el token falla, un 401.
2. Decodificar los bloques de la ficha
Aquí está la única sorpresa de la API. Los bloques grandes de la ficha llegan como cadenas que contienen JSON, así que hay que decodificarlos una segunda vez:
import json
import os
import requests
BASE = os.environ["BASE"]
CABECERAS = {"Authorization": os.environ["TOKEN"], "Accept": "application/json"}
def ficha(identificador):
r = requests.get(f"{BASE}/api/auctions/{identificador}", headers=CABECERAS, timeout=30)
r.raise_for_status()
subasta = r.json()
for bloque in ("general_information", "managing_authorities",
"properties", "lots", "related", "bids"):
if isinstance(subasta.get(bloque), str):
subasta[bloque] = json.loads(subasta[bloque])
return subasta
general_information es una lista de pares con una sola clave cada uno. Para consultarla con comodidad, conviértela en un diccionario:
subasta = ficha("SUB-JA-2026-000000")
general = {k: v for par in subasta["general_information"] for k, v in par.items()}
print(general["Tipo de subasta"]) # JUDICIAL EN VÍA DE APREMIO
print(general["Valor subasta"]) # 199.407,52 €
3. Convertir importes y fechas
Los importes son texto con formato español. Una función pequeña los convierte en números:
def euros(texto):
"""'199.407,52 €' -> 199407.52; devuelve None si no hay importe."""
if not texto or not texto[0].isdigit():
return None
return float(texto.split(" ")[0].replace(".", "").replace(",", "."))
Campos como «Puja mínima» pueden traer un texto en lugar de un importe («Sin puja mínima»). La función devuelve None en ese caso.
Las fechas de la ficha incluyen su versión ISO entre paréntesis. Esa es la referencia exacta de la hora de cierre:
import re
from datetime import datetime
def iso(texto):
m = re.search(r"ISO: ([0-9T:+\-]+)", texto or "")
return datetime.fromisoformat(m.group(1)) if m else None
cierre = iso(general.get("Fecha de conclusión"))
4. Leer los bienes
Cada elemento de properties es un bien. Además de la descripción y de los pares que publica el portal, lleva superficie en metros cuadrados cuando el texto la indica.
for bien in subasta["properties"] or []:
datos = {k: v for par in bien["property"] for k, v in par.items()}
print(bien["title"], bien["superficie"], datos.get("Localidad"), datos.get("Provincia"))
La provincia y la localidad están ahí, dentro de cada bien. En las subastas por lotes, properties viene vacío y los bienes están en lots.
5. Seguir las pujas
Para vigilar una subasta abierta no hace falta pedir la ficha entera. Hay una ruta más ligera:
curl "$BASE/api/auctions/SUB-JA-2026-000000/bids" -H "Authorization: $TOKEN"
{
"auction_number": "SUB-JA-2026-000000",
"bids": { "tipo": "oculta", "importe": null, "mensaje": "La subasta ha recibido alguna puja. …", "lotes": [] },
"bids_refreshed_at": "2026-09-30T12:56:03.000000Z"
}
Conviene tener claro qué se puede saber y qué no. Mientras la subasta está abierta, el portal no publica importes: solo dice si ha habido puja. Lo normal es ver sin_pujas, después oculta y, al concluir, puja_maxima_final con el importe. bids_refreshed_at indica cuándo se comprobó por última vez.
6. Pedir solo lo que ha cambiado
Consultar cien subastas cada hora para ver si alguna se ha movido es un desperdicio. El histórico de cambios responde a esa pregunta en una sola petición:
def cambios_desde(momento, campos=("fecha_conclusion", "bids.tipo", "bids.importe")):
params = {"since": momento, "fields": ",".join(campos), "limit": 500}
while True:
r = requests.get(f"{BASE}/api/changes", headers=CABECERAS, params=params, timeout=30)
r.raise_for_status()
pagina = r.json()
yield from pagina["changes"]
if not pagina["has_more"]:
break
params["before_id"] = pagina["before_id_next"]
for c in cambios_desde("2026-09-30T00:00:00Z"):
print(c["changed_at"], c["auction_number"], c["field"], c["old_value"], "->", c["new_value"])
Guarda el changed_at más reciente que hayas procesado y úsalo como since en la siguiente ejecución. Como el límite inferior es inclusivo, el último cambio puede repetirse: descártalo por su id.
Si solo te interesan unas subastas concretas, añade auction_numbers con hasta 50 identificadores separados por comas.
Un ciclo de trabajo razonable
- Una vez al día, recorre
/api/auctionspágina a página para incorporar las subastas nuevas. - Cada hora, llama a
/api/changesconsincepara enterarte de primeras pujas, prórrogas y cierres. - Cuando una subasta concluya, pide su ficha una última vez para guardar el resultado.
Con eso tienes los mismos datos que quien revisa el portal a mano, sin revisarlo. La referencia completa de parámetros y respuestas está en la documentación, y si quieres entender por qué cambia la hora de cierre, en este artículo sobre las prórrogas.