Documento público · planes Agency y Business · estado del servicio
API pública
Versión del contrato: 1.0.0
Autenticación con Authorization: Bearer smbt_live_…. Activos, escaneos, hallazgos, reportes y cuentas de cliente. Disponible en los planes Agency y Business. Política de cambios: aviso de 90 días antes de cualquier cambio que rompa un cliente existente — ver https://securitymbt.com/docs/api.
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Flujo CI trigger → poll → gate:
```bash
KEY=smbt_live_xxx
# Base pública del Fastify (API_PUBLIC_URL), NO el panel (APP_URL).
BASE="$SECURITYMBT_API_URL/api/v1/public-api"
SCAN=$(curl -sS -X POST "$BASE/ci/scans" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $CI_PIPELINE_ID" \
-H 'content-type: application/json' \
-d '{"assetId":"'"$ASSET_ID"'","level":"passive"}' | jq -r .data.scanId)
until [ "$(curl -sS "$BASE/ci/scans/$SCAN" -H "Authorization: Bearer $KEY" \
| jq -r .data.status)" = "COMPLETED" ]; do sleep 10; done
# El endpoint YA filtra por defecto: solo exposiciones ABIERTAS y NUEVAS
# de este escaneo (firstSeenAt >= scan.startedAt). No cuenta históricos
# ni exposiciones ya verificadas/aceptadas. El gate es honesto sin más.
CRIT=$(curl -sS "$BASE/ci/scans/$SCAN/exposures?severity=CRITICAL" \
-H "Authorization: Bearer $KEY" | jq '.data | length')
[ "$CRIT" -gt 0 ] && { echo "Exposición CRÍTICA nueva — build rojo"; exit 1; }
```
Los escaneos de CI cuentan contra el límite `scansPerMonth` del plan.
Un 402 `PLAN_LIMIT_REACHED` es esperado: el pipeline debe manejarlo
(no disparar en silencio hasta agotar la cuota del cliente).
Referencia
Generada del código en cada despliegue — nunca escrita a mano. También disponible como JSON en /api/v1/openapi.json.
get/assetsListar activos
Activos de la organización dueña de la clave, paginados por cursor.
Alcance requerido: assets:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| cursor | query | No | string |
| limit | query | No | integer |
| clientAccountId | query | No | string |
| kind | query | No | string |
| status | query | No | string |
Respuestas
200 — Página de activos.
{
"data": [
{
"id": "ast_3f9k2x8h7g4m",
"kind": "DOMAIN",
"label": "ejemplo-cliente.com",
"status": "VERIFIED",
"fqdn": "ejemplo-cliente.com",
"verifiedAt": "2026-01-15T10:00:00.000Z",
"lastScanAt": "2026-02-01T04:00:00.000Z",
"clientAccountId": "cli_8h2k9x3n7g4m",
"createdAt": "2026-01-10T09:00:00.000Z"
}
],
"meta": {
"nextCursor": null
}
}post/assetsCrear un activo
Crea un activo DOMAIN, SUBDOMAIN, IP o API_ENDPOINT. Queda en estado PENDING_VERIFICATION — un escaneo activo exige verificarlo primero (ver la guía de primeros pasos).
Alcance requerido: assets:write
Cuerpo de la petición
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"DOMAIN",
"SUBDOMAIN",
"IP",
"API_ENDPOINT"
]
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"fqdn": {
"type": "string",
"minLength": 1
},
"clientAccountId": {
"type": "string",
"minLength": 1,
"nullable": true
}
},
"required": [
"kind",
"label"
],
"additionalProperties": false
}Respuestas
201 — Activo creado.
{
"data": {
"id": "ast_3f9k2x8h7g4m",
"kind": "DOMAIN",
"label": "ejemplo-cliente.com",
"status": "VERIFIED",
"fqdn": "ejemplo-cliente.com",
"verifiedAt": "2026-01-15T10:00:00.000Z",
"lastScanAt": "2026-02-01T04:00:00.000Z",
"clientAccountId": "cli_8h2k9x3n7g4m",
"createdAt": "2026-01-10T09:00:00.000Z"
},
"meta": {}
}get/assets/{id}Obtener un activo
Detalle de un activo por id.
Alcance requerido: assets:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
Respuestas
200 — El activo.
{
"data": {
"id": "ast_3f9k2x8h7g4m",
"kind": "DOMAIN",
"label": "ejemplo-cliente.com",
"status": "VERIFIED",
"fqdn": "ejemplo-cliente.com",
"verifiedAt": "2026-01-15T10:00:00.000Z",
"lastScanAt": "2026-02-01T04:00:00.000Z",
"clientAccountId": "cli_8h2k9x3n7g4m",
"createdAt": "2026-01-10T09:00:00.000Z"
},
"meta": {}
}404 — No existe o pertenece a otra organización.
post/assets/{id}/scansLanzar un escaneo
`level: "passive"` no exige verificación. `level: "full"` sí — sin ella, 403 ASSET_NOT_VERIFIED.
Alcance requerido: scans:write
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
Cuerpo de la petición
{
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": [
"passive",
"full"
]
}
},
"additionalProperties": false
}Respuestas
202 — Escaneo encolado.
{
"data": {
"scanId": "scn_7g4m3f9k2x8h",
"status": "QUEUED",
"estimatedSeconds": 30
},
"meta": {}
}403 — El activo no está verificado y se pidió level: "full".
get/scans/{id}Obtener el estado de un escaneo
Sondea este endpoint tras crear un escaneo hasta que `status` sea COMPLETED.
Alcance requerido: scans:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
Respuestas
200 — El escaneo.
{
"data": {
"id": "scn_7g4m3f9k2x8h",
"assetId": "ast_3f9k2x8h7g4m",
"status": "COMPLETED",
"trigger": "API",
"analyzersRun": [
"dns",
"tls",
"http_headers",
"tech_fingerprint",
"reputation"
],
"startedAt": "2026-02-01T04:00:02.000Z",
"finishedAt": "2026-02-01T04:00:19.000Z",
"createdAt": "2026-02-01T04:00:00.000Z"
},
"meta": {}
}post/ci/scansDisparar un escaneo de CI
Scope `scan:trigger`. Idempotente por cabecera `Idempotency-Key` o `pipelineId` del cuerpo. No crea activos. Cuenta contra `scansPerMonth` del plan: un 402 `PLAN_LIMIT_REACHED` es esperado y el pipeline debe manejarlo.
Alcance requerido: scan:trigger
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| Idempotency-Key | header | No | string |
Cuerpo de la petición
{
"type": "object",
"required": [
"assetId"
],
"properties": {
"assetId": {
"type": "string"
},
"level": {
"type": "string",
"enum": [
"passive",
"full"
]
},
"pipelineId": {
"type": "string",
"maxLength": 128
}
}
}Respuestas
202 — Escaneo encolado.
{
"data": {
"scanId": "scn_7g4m3f9k2x8h",
"status": "QUEUED",
"statusUrl": "/api/v1/public-api/ci/scans/scn_7g4m3f9k2x8h",
"exposuresUrl": "/api/v1/public-api/ci/scans/scn_7g4m3f9k2x8h/exposures",
"estimatedSeconds": 30,
"idempotentReplay": false
},
"meta": {}
}402 — Límite de escaneos del plan (`scansPerMonth`). Los disparos de CI cuentan en la misma cuota que el panel. Código `PLAN_LIMIT_REACHED`. El pipeline debe fallar de forma visible, no reintentar en silencio.
{
"error": {
"code": "PLAN_LIMIT_REACHED",
"message": "Alcanzaste el límite de tu plan actual.",
"details": {
"limit": "scansPerMonth"
}
}
}403 — La clave no tiene el ámbito `scan:trigger`. Código `INSUFFICIENT_SCOPE`.
get/ci/scans/{id}Estado de un escaneo de CI
Scope `scans:read`. Sondea hasta `COMPLETED` o `FAILED`.
Alcance requerido: scans:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
Respuestas
200 — El escaneo.
{
"data": {
"id": "scn_7g4m3f9k2x8h",
"assetId": "ast_3f9k2x8h7g4m",
"status": "COMPLETED",
"trigger": "CI",
"startedAt": "2026-02-01T04:00:02.000Z",
"finishedAt": "2026-02-01T04:00:19.000Z"
},
"meta": {}
}get/ci/scans/{id}/exposuresExposiciones del escaneo (gate)
Scope `exposure:read`. Resumen compacto: sin `justification` ni evidencia cruda. POR DEFECTO devuelve solo exposiciones ABIERTAS y NUEVAS de este escaneo (`firstSeenAt >= scan.startedAt`) — el gate honesto es "críticos nuevos de este push", no "históricos del activo". `status` explícito sustituye el filtro de estado; `newSince` (ISO 8601) sustituye la ventana de fecha.
Alcance requerido: exposure:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
| severity | query | No | string |
| status | query | No | string |
| newSince | query | No | string |
Respuestas
200 — Página de exposiciones.
{
"data": [
{
"id": "exp_9k2x8h7g4m3f",
"title": "Cabecera HSTS ausente",
"category": "http_headers",
"severity": "CRITICAL",
"status": "OPEN",
"priority": "URGENT",
"inKev": false,
"cve": null,
"findingCount": 1,
"firstSeenAt": "2026-02-01T04:00:19.000Z"
}
],
"meta": {
"nextCursor": null
}
}get/findingsListar hallazgos
Hallazgos de la organización, filtrables por activo, severidad y estado.
Alcance requerido: findings:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| cursor | query | No | string |
| limit | query | No | integer |
| assetId | query | No | string |
| severity | query | No | string |
| status | query | No | string |
| category | query | No | string |
| clientAccountId | query | No | string |
Respuestas
200 — Página de hallazgos.
{
"data": [
{
"id": "fnd_2x8h7g4m3f9k",
"assetId": "ast_3f9k2x8h7g4m",
"ruleId": "tls-cert-expiring-soon",
"title": "El certificado TLS vence en menos de 15 días",
"category": "tls",
"severity": "HIGH",
"status": "OPEN",
"firstSeenAt": "2026-02-01T04:00:19.000Z",
"lastSeenAt": "2026-02-08T04:00:19.000Z",
"resolvedAt": null
}
],
"meta": {
"nextCursor": null
}
}get/findings/{id}Obtener un hallazgo
Detalle de un hallazgo por id.
Alcance requerido: findings:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
Respuestas
200 — El hallazgo.
{
"data": {
"id": "fnd_2x8h7g4m3f9k",
"assetId": "ast_3f9k2x8h7g4m",
"ruleId": "tls-cert-expiring-soon",
"title": "El certificado TLS vence en menos de 15 días",
"category": "tls",
"severity": "HIGH",
"status": "OPEN",
"firstSeenAt": "2026-02-01T04:00:19.000Z",
"lastSeenAt": "2026-02-08T04:00:19.000Z",
"resolvedAt": null
},
"meta": {}
}get/reportsListar reportes
Reportes generados, paginados por cursor.
Alcance requerido: reports:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| cursor | query | No | string |
| limit | query | No | integer |
Respuestas
200 — Página de reportes.
{
"data": [
{
"id": "rpt_9k2x8h7g4m3f",
"type": "CLIENT_SUMMARY",
"status": "READY",
"format": "pdf",
"createdAt": "2026-02-01T08:00:00.000Z",
"expiresAt": "2026-03-03T08:00:00.000Z"
}
],
"meta": {
"nextCursor": null
}
}get/reports/{id}/downloadDescargar el PDF de un reporte
Devuelve el binario del PDF, no un JSON. 409 si el reporte todavía no está listo.
Alcance requerido: reports:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| id | path | Sí | string |
Respuestas
200 — El archivo PDF.
409 — El PDF todavía no está generado (REPORT_NOT_READY).
get/clientsListar cuentas de cliente
Cuentas de cliente de la organización, paginadas por cursor.
Alcance requerido: assets:read
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| cursor | query | No | string |
| limit | query | No | integer |
| search | query | No | string |
Respuestas
200 — Página de cuentas de cliente.
{
"data": [
{
"id": "cli_8h2k9x3n7g4m",
"name": "Cliente de ejemplo S.A.",
"contactEmail": "contacto@ejemplo-cliente.com",
"assetCount": 4,
"createdAt": "2025-11-02T09:00:00.000Z"
}
],
"meta": {
"nextCursor": null
}
}get/api/v1/assets/{assetId}/rules-of-engagementLeer Reglas de Compromiso
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
200 — La hoja de RoE, o null.
put/api/v1/assets/{assetId}/rules-of-engagementFirmar o actualizar las Reglas de Compromiso
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
200 — RoE actualizada.
201 — RoE creada.
delete/api/v1/assets/{assetId}/rules-of-engagementBorrar las Reglas de Compromiso
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
204 — Eliminada.
post/api/v1/assets/{assetId}/dast/resolve-selectionResolver la selección de pruebas
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
200 — Lista de DastTestId filtrada por catálogo ∩ RoE.
post/api/v1/assets/{assetId}/dast/estimateEstimar duración de una corrida (dry-run)
No encola nada.
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
200 — Estimación de pruebas, ritmo y duración.
get/api/v1/assets/{assetId}/dast/runsListar corridas DAST del activo
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
200 — Lista de corridas.
post/api/v1/assets/{assetId}/dast/runsLanzar una corrida DAST
Única puerta: `assertDastAllowed` (verificación + atestación + RoE + plan).
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
202 — Corrida encolada.
get/api/v1/dast/runs/{runId}Estado de una corrida DAST
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| runId | path | Sí | string |
Respuestas
200 — La corrida.
post/api/v1/dast/runs/{runId}/stopParar una corrida DAST
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| runId | path | Sí | string |
Respuestas
200 — Parada solicitada.
{
"data": {
"stopped": true
},
"meta": {}
}get/api/v1/assets/{assetId}/dast/identitiesListar identidades de prueba
Nunca devuelve el secreto cifrado.
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
Respuestas
200 — Identidades (sin secreto).
put/api/v1/assets/{assetId}/dast/identities/{role}Crear o rotar una identidad de prueba
El `secret` es de escritura. No se re-muestra.
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
| role | path | Sí | string |
Respuestas
200 — Identidad actualizada.
201 — Identidad creada.
delete/api/v1/assets/{assetId}/dast/identities/{role}Borrar una identidad de prueba
Parámetros
| Nombre | En | Obligatorio | Tipo |
|---|---|---|---|
| assetId | path | Sí | string |
| role | path | Sí | string |
Respuestas
204 — Eliminada.
Autenticación
El problema: necesitas autenticar tus peticiones a la API pública y no sabes cómo funcionan las claves.
1. Crea una clave en Ajustes → API (solo disponible en los planes Agency y Business). Elige los alcances (scopes) que necesita: assets:read, assets:write, scans:read, scans:write, findings:read, reports:read. 2. La clave completa (smbt_live_...) se muestra una sola vez, en el momento de crearla. Guárdala en un gestor de secretos — no en el código, no en un chat, no en un archivo de texto en tu escritorio. 3. Envíala en cada petición con la cabecera Authorization: Bearer smbt_live_.... 4. El prefijo smbt_live_ es detectable por escáneres de secretos (gitleaks y similares) a propósito: si una clave se filtra a un repositorio público, tú o quien la detecte lo va a notar rápido. 5. Cada clave tiene solo los alcances que le diste — una clave con assets:read no puede crear activos ni lanzar escaneos, aunque la use quien la creó.
Rotación
1. Crea una clave nueva con los mismos alcances antes de revocar la vieja — así no hay una ventana sin acceso. 2. Actualiza tu integración para usar la clave nueva. 3. Revoca la clave vieja en Ajustes → API una vez confirmes que la nueva funciona.
Buenas prácticas
- Una clave por integración, no una clave compartida entre varios sistemas — así, si una se compromete, revocas solo esa sin afectar al resto.
- El mínimo de alcances necesario. Si tu integración solo lee hallazgos, no le des
assets:write. - Pon fecha de caducidad si la integración es temporal.
Si esto no funciona
401 UNAUTHORIZEDcon la clave bien copiada: revisa que no tenga espacios al principio o al final, y que no esté revocada.403 API_ACCESS_REQUIRED: tu plan actual no incluye la API pública — hace falta Agency o Business, ver [/precios](/precios).403 INSUFFICIENT_SCOPE: la clave no tiene el alcance que esa operación necesita — créala de nuevo con el alcance correcto, no se puede editar una clave existente.
Primeros pasos con la API
El problema: quieres integrar la API pública y no sabes por dónde empezar. Esta guía llega, con curl, hasta listar hallazgos.
1. Crea una cuenta y una organización en [securitymbt](/register) si todavía no tienes una (necesitas un plan Agency o Business para la API — ver [/precios](/precios)). 2. Crea una clave de API en Ajustes → API, con los alcances assets:write, scans:write y findings:read. Copia la clave completa — solo se muestra una vez. Guárdala en una variable de entorno:
``bash export SMBT_API_KEY="smbt_live_..." ``
3. Crea un activo de tipo dominio:
``bash curl -sS -X POST https://securitymbt.com/api/v1/public-api/assets \ -H "Authorization: Bearer $SMBT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"kind": "DOMAIN", "label": "ejemplo.com", "fqdn": "ejemplo.com"}' ``
Guarda el id que devuelve la respuesta (data.id) — lo vas a necesitar en los siguientes pasos.
4. Lanza un escaneo pasivo sobre ese activo. No hace falta verificar el dominio para esto — la verificación solo es obligatoria para el escaneo completo (puertos, rutas administrativas):
``bash curl -sS -X POST https://securitymbt.com/api/v1/public-api/assets/ASSET_ID/scans \ -H "Authorization: Bearer $SMBT_API_KEY" \ -H "Content-Type: application/json" \ -d '{"level": "passive"}' ``
La respuesta trae data.scanId con estado 202 (encolado).
5. Sondea el estado del escaneo hasta que status sea COMPLETED (un escaneo pasivo tarda segundos, no minutos):
``bash curl -sS https://securitymbt.com/api/v1/public-api/scans/SCAN_ID \ -H "Authorization: Bearer $SMBT_API_KEY" ``
6. Lista los hallazgos de ese activo:
``bash curl -sS "https://securitymbt.com/api/v1/public-api/findings?assetId=ASSET_ID" \ -H "Authorization: Bearer $SMBT_API_KEY" ``
La respuesta trae data (la lista de hallazgos) y meta.nextCursor — si no es null, hay más páginas: repite la petición añadiendo ?cursor=ese_valor.
Si esto no funciona
401/403en cualquier paso: ver [autenticación](/ayuda/api/autenticacion).- El escaneo se queda en
RUNNINGmucho tiempo: espera unos segundos más y vuelve a consultar — un escaneo pasivo normal termina en menos de un minuto. - La lista de hallazgos llega vacía: es un resultado válido, no un error — significa que el escaneo pasivo no encontró nada de alta prioridad en ese dominio.
- Para recibir avisos sin tener que sondear, configura un webhook de alertas — ver [webhooks](/ayuda/api/webhooks).
Webhooks
El problema: no quieres sondear la API cada minuto para saber si algo cambió — quieres que te avisen.
Los webhooks se configuran en el panel (Ajustes → Alertas → Canales, no con la API), pero lo que reciba tu servidor es lo importante aquí: cómo verificar que la petición viene de verdad de securitymbt, y cómo manejar reintentos sin procesar el mismo evento dos veces.
Eventos
surface.change— un cambio detectado en la superficie de un activo (puerto, certificado, subdominio, cabecera, DNS…).uptime.incident— el activo cayó o se recuperó.alert.test— el envío manual de prueba desde el panel, para confirmar que tu endpoint responde antes de depender de él.
Forma del payload
``json { "event": "surface.change", "timestamp": "2026-02-01T04:00:19.000Z", "organization": { "id": "org_...", "name": "Tu organización" }, "asset": { "id": "ast_...", "label": "ejemplo.com", "fqdn": "ejemplo.com" }, "change": { "id": "chg_...", "type": "CERT_EXPIRING", "severity": "HIGH", "summary": "El certificado TLS vence en menos de 15 días", "detectedAt": "2026-02-01T04:00:19.000Z" } } ``
change es null en alert.test — no hay un cambio real detrás de un envío de prueba.
Verificar la firma
Cabecera X-SecurityMBT-Signature: sha256=<hex>. El HMAC-SHA256 se calcula sobre los BYTES EXACTOS del cuerpo recibido — nunca sobre el JSON reformateado por tu framework, porque un espacio de más ya no coincide. El secreto es específico de tu organización y de la URL del webhook; lo ves en Ajustes → Alertas → Canales al configurar el webhook.
Node.js:
```js import { createHmac, timingSafeEqual } from 'node:crypto';
function isValidSignature(rawBody, signatureHeader, secret) { const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); const provided = (signatureHeader || '').trim(); if (provided.length !== expected.length) return false; return timingSafeEqual(Buffer.from(provided), Buffer.from(expected)); }
// En Express, captura el body crudo ANTES de que algo lo parsee a JSON: // app.use('/webhooks/securitymbt', express.raw({ type: 'application/json' })); app.post('/webhooks/securitymbt', (req, res) => { const ok = isValidSignature(req.body, req.headers['x-securitymbt-signature'], WEBHOOK_SECRET); if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString('utf8')); // ... procesar event.event / event.change ... res.status(200).end(); }); ```
Python:
```python import hashlib import hmac
def is_valid_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() provided = (signature_header or "").strip() return hmac.compare_digest(provided, expected)
Flask: usa request.get_data() para el body crudo, nunca request.json
(que ya lo parseó y perdió el byte a byte original).
@app.route("/webhooks/securitymbt", methods=["POST"]) def securitymbt_webhook(): raw_body = request.get_data() signature = request.headers.get("X-SecurityMBT-Signature", "") if not is_valid_signature(raw_body, signature, WEBHOOK_SECRET): return "", 401
event = request.get_json() # ... procesar event["event"] / event["change"] ... return "", 200 ```
Sin esta verificación, cualquiera que conozca tu URL puede mandarte alertas falsas — no confíes en el payload hasta que la firma valide.
Reintentos
Hasta 3 intentos por entrega, con espera creciente entre cada uno (backoff exponencial desde 5 segundos). Tu endpoint tiene 10 segundos para responder 2xx antes de que se cuente como fallo. Tras 10 fallos consecutivos, el webhook se desactiva automáticamente y recibes un correo — vuelve a verificarlo desde el panel para reactivarlo.
Idempotencia
Hoy el payload no trae un identificador de entrega dedicado — usa change.id (presente en surface.change y uptime.incident) como clave para no procesar el mismo cambio dos veces si tu endpoint recibe un reintento después de haber respondido correctamente pero antes de que la respuesta nos llegara. alert.test no tiene change.id: son envíos manuales, no hace falta deduplicar.
Si esto no funciona
- Tu endpoint nunca recibe nada: confirma que la URL sea alcanzable públicamente (un webhook a
localhosto a una IP privada se rechaza). - La firma nunca valida: el sospechoso número uno es que tu framework ya parseó el body a JSON antes de que puedas verificarlo — captura el body crudo primero.
- El webhook se desactivó solo: revisa que tu endpoint responda
2xxen menos de 10 segundos, no después de procesar todo de forma síncrona.
Changelog de la API
Cambios que afectan a integraciones existentes. Separado del changelog interno del producto: esto es solo lo que le importa a quien integra con la API pública.
Política de cambios: aviso de 90 días antes de cualquier cambio que rompa una integración existente (un campo que desaparece, un endpoint que cambia de forma, un código de error que cambia de significado). Añadir un campo nuevo opcional o un endpoint nuevo no cuenta como cambio disruptivo y no lleva aviso previo.
1.0.0 — 2026
- Primera versión pública: activos, escaneos, hallazgos, reportes y cuentas de cliente.
- Autenticación por clave (
smbt_live_...), alcances por clave, límite de peticiones por hora según plan. - Documento OpenAPI 3.1 generado en
/api/v1/openapi.json.