Dominio verificado: hashcodcodespace.dev (las URL de esta guía usan este dominio)

Integra las constancias en tus sistemas

Verifica obras y archivos tokenizados —también los creados con IA— desde tu código: consulta por lotes, guarda la prueba firmada y recibe avisos firmados cuando una constancia se emite o se revoca. Sin claves de API para leer: todo lo público es verificable por cualquiera.

1 · API de verificación por lotes

POST /api/constancia/public/verify-batch · hasta 50 elementos · 10 consultas por minuto e IP (o la cuota de tu clave de API) · CORS abierto, sin cookies. Cada elemento es un número o {number, sha512} con el SHA-512 del archivo (calcúlalo tú: el archivo nunca se envía).

Petición
curl -s https://hashcodcodespace.dev/api/constancia/public/verify-batch \
  -H 'Content-Type: application/json' \
  -d '{"items":["HC-2026-000001",{"number":"HC-2026-000002","sha512":"<sha512 del archivo>"}]}'
Con clave de API (integradores)
# Con clave de integrador (cuota propia; pídela al administrador). Nunca la pongas en código del navegador.
curl -s https://hashcodcodespace.dev/api/constancia/public/verify-batch \
  -H "Authorization: Bearer $HASHCOD_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"items":["HC-2026-000001"]}'
# Cabecera X-RateLimit-Limit = tu cuota por minuto; 401 si la clave no es válida o está revocada; 429 si la superas.
Respuesta (resumida)
{
  "ok": true, "count": 2,
  "results": [
    { "number": "HC-2026-000001", "found": true, "status": "vigente", "valid": true,
      "checks": [{ "id": "signature", "ok": true }, { "id": "jcs_hash", "ok": true }, "…"],
      "ledgerIntact": true, "genTimeUtc": "2026-11-05T02:00:00Z", "fingerprint": "…",
      "verifyUrl": "https://hashcodcodespace.dev/verify/HC-2026-000001" },
    { "number": "HC-2026-000002", "found": true, "valid": true, "fileMatch": true, "aiProvenance": { "generator": "…" } }
  ]
}

Sin programar: Verificar lote en el navegador, con exportación a CSV.

2 · Prueba portable (JSON-LD firmado)

GET /api/constancia/public/{N}.jsonld entrega la credencial W3C tal como se firmó (eddsa-jcs-2022, Ed25519) con la procedencia de IA. Guárdala: se verifica sin esta plataforma con la clave de /.well-known/did.json.

Descargar y comprobar la huella
N=HC-2026-000001
curl -s -o $N.jsonld https://hashcodcodespace.dev/api/constancia/public/$N.jsonld
# La huella de la constancia es el SHA-512 de esos bytes exactos:
sha512sum $N.jsonld
curl -s https://hashcodcodespace.dev/api/constancia/public/$N | grep -o '"fingerprint":"[a-f0-9]*"'
# Clave pública del emisor (guárdala junto a la credencial):
curl -s https://hashcodcodespace.dev/.well-known/did.json

3 · Webhooks firmados

El administrador registra tu URL (https pública, puerto 443) y te entrega un secreto whsec_… una sola vez. Reintentos a 1 min, 5 min, 30 min, 2 h y 6 h si no respondes 2xx. Los avisos no contienen datos del titular.

Formato del aviso
POST /tu/webhook
Content-Type: application/json
X-Hashcod-Event: constancia.issued          (o constancia.revoked, ping)
X-Hashcod-Delivery: 6f1c…                   (id único: úsalo para ignorar duplicados)
X-Hashcod-Timestamp: 1762320000             (segundos Unix)
X-Hashcod-Signature: sha256=<hex>           (HMAC-SHA256 del secreto sobre "timestamp.cuerpo")

{ "id": "6f1c…", "event": "constancia.issued", "createdAt": "2026-11-05T02:00:00Z",
  "data": { "number": "HC-2026-000001", "status": "vigente", "fingerprint": "…", "genTimeUtc": "…",
            "visibility": "public", "revokedAt": null, "verifyUrl": "https://hashcodcodespace.dev/verify/HC-2026-000001" } }

Verifica siempre la firma con el cuerpo crudo, compara en tiempo constante, rechaza marcas de tiempo de más de 5 minutos y descarta X-Hashcod-Delivery repetidos.

Node.js
// Node.js + Express. Usa el cuerpo CRUDO: re-serializar el JSON cambia los bytes y la firma no coincide.
const crypto = require('node:crypto');
const express = require('express');
const app = express();

app.post('/hashcod/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-Hashcod-Timestamp') || '';
  const sig = req.get('X-Hashcod-Signature') || '';
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.HASHCOD_WEBHOOK_SECRET)
    .update(ts + '.').update(req.body).digest('hex');
  const valid = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!valid || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
  const event = JSON.parse(req.body);   // event.event, event.data.number…
  res.sendStatus(204);                  // responde 2xx rápido; procesa en segundo plano
});
app.listen(3000);
Python
# Python + Flask
import hashlib, hmac, os, time
from flask import Flask, abort, request

app = Flask(__name__)

@app.post('/hashcod/webhook')
def hashcod_webhook():
    ts = request.headers.get('X-Hashcod-Timestamp', '')
    body = request.get_data()  # bytes crudos
    expected = 'sha256=' + hmac.new(os.environ['HASHCOD_WEBHOOK_SECRET'].encode(),
                                    ts.encode() + b'.' + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get('X-Hashcod-Signature', '')):
        abort(401)
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(401)
    event = request.get_json()  # event['event'], event['data']['number']…
    return '', 204
PHP
<?php
// PHP puro
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_HASHCOD_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('HASHCOD_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_HASHCOD_SIGNATURE'] ?? '') || abs(time() - (int)$ts) > 300) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);  // $event['event'], $event['data']['number']…
http_response_code(204);

4 · SDK para Node.js y Python

Dos paquetes mínimos sin dependencias, servidos desde este dominio: verifican la firma de la credencial con la clave de did.json, que los bytes descargados sean exactamente los firmados, y la firma de los webhooks. No envían nada a la plataforma salvo las dos descargas públicas.

SDK · Node.js
npm install https://hashcodcodespace.dev/sdk/hashcod-constancia-1.0.1.tgz

// Node.js ≥ 18, sin dependencias (dentro de una función async)
const { verifyConstancia, verifyWebhook } = require('hashcod-constancia');

const r = await verifyConstancia('HC-2026-000001', { base: 'https://hashcodcodespace.dev' });
console.log(r.ok, r.fingerprint);          // firma eddsa-jcs-2022 + bytes canónicos

// En tu receptor de webhooks (cuerpo crudo):
verifyWebhook({ secret: process.env.HASHCOD_WEBHOOK_SECRET, body: req.body,
  timestamp: req.get('X-Hashcod-Timestamp'), signature: req.get('X-Hashcod-Signature') });
SDK · Python
pip install https://hashcodcodespace.dev/sdk/hashcod_constancia-1.0.1-py3-none-any.whl

# Python ≥ 3.8, solo biblioteca estándar (Ed25519 incluido)
import os
from hashcod_constancia import verify_constancia, verify_webhook

r = verify_constancia('HC-2026-000001', base='https://hashcodcodespace.dev')
print(r['ok'], r['fingerprint'])

ok = verify_webhook(os.environ['HASHCOD_WEBHOOK_SECRET'], request.get_data(),
                    request.headers.get('X-Hashcod-Timestamp'), request.headers.get('X-Hashcod-Signature'))

# Terminal: hashcod-constancia HC-2026-000001   (código de salida 0 si es válida)

Descarga directa: npm (.tgz) · Python (.whl) · módulo de un archivo (.js).

5 · hcod verify

Terminal
# Todas las comprobaciones (firma, huella, compromiso, sello de tiempo, roles y ancla externa):
php bin/hcod verify HC-2026-000001 --base https://hashcodcodespace.dev
# Sin depender de la red para el paquete, con el archivo y el prompt:
php bin/hcod verify HC-2026-000001 --cod HC-2026-000001.cod --asset contrato.pdf --prompt prompt.txt
# Código de salida 0 solo si todo pasa: úsalo en scripts y pipelines de CI.

Verificar lote · did.json del emisor