Saltar al contenido

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

NombreEnObligatorioTipo
cursorqueryNostring
limitqueryNointeger
clientAccountIdqueryNostring
kindqueryNostring
statusqueryNostring

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

NombreEnObligatorioTipo
idpathstring

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

NombreEnObligatorioTipo
idpathstring

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

NombreEnObligatorioTipo
idpathstring

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

NombreEnObligatorioTipo
Idempotency-KeyheaderNostring

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

NombreEnObligatorioTipo
idpathstring

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

NombreEnObligatorioTipo
idpathstring
severityqueryNostring
statusqueryNostring
newSincequeryNostring

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

NombreEnObligatorioTipo
cursorqueryNostring
limitqueryNointeger
assetIdqueryNostring
severityqueryNostring
statusqueryNostring
categoryqueryNostring
clientAccountIdqueryNostring

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

NombreEnObligatorioTipo
idpathstring

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

NombreEnObligatorioTipo
cursorqueryNostring
limitqueryNointeger

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

NombreEnObligatorioTipo
idpathstring

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

NombreEnObligatorioTipo
cursorqueryNostring
limitqueryNointeger
searchqueryNostring

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

NombreEnObligatorioTipo
assetIdpathstring

Respuestas

200 — La hoja de RoE, o null.

put/api/v1/assets/{assetId}/rules-of-engagementFirmar o actualizar las Reglas de Compromiso

Parámetros

NombreEnObligatorioTipo
assetIdpathstring

Respuestas

200 — RoE actualizada.

201 — RoE creada.

delete/api/v1/assets/{assetId}/rules-of-engagementBorrar las Reglas de Compromiso

Parámetros

NombreEnObligatorioTipo
assetIdpathstring

Respuestas

204 — Eliminada.

post/api/v1/assets/{assetId}/dast/resolve-selectionResolver la selección de pruebas

Parámetros

NombreEnObligatorioTipo
assetIdpathstring

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

NombreEnObligatorioTipo
assetIdpathstring

Respuestas

200 — Estimación de pruebas, ritmo y duración.

get/api/v1/assets/{assetId}/dast/runsListar corridas DAST del activo

Parámetros

NombreEnObligatorioTipo
assetIdpathstring

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

NombreEnObligatorioTipo
assetIdpathstring

Respuestas

202 — Corrida encolada.

get/api/v1/dast/runs/{runId}Estado de una corrida DAST

Parámetros

NombreEnObligatorioTipo
runIdpathstring

Respuestas

200 — La corrida.

post/api/v1/dast/runs/{runId}/stopParar una corrida DAST

Parámetros

NombreEnObligatorioTipo
runIdpathstring

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

NombreEnObligatorioTipo
assetIdpathstring

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

NombreEnObligatorioTipo
assetIdpathstring
rolepathstring

Respuestas

200 — Identidad actualizada.

201 — Identidad creada.

delete/api/v1/assets/{assetId}/dast/identities/{role}Borrar una identidad de prueba

Parámetros

NombreEnObligatorioTipo
assetIdpathstring
rolepathstring

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 UNAUTHORIZED con 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/403 en cualquier paso: ver [autenticación](/ayuda/api/autenticacion).
  • El escaneo se queda en RUNNING mucho 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 localhost o 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 2xx en 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.