Ir al contenido

Quickstart · Firma electrónica con bseal

bseal expone dos tracks de firma:

  • Track 1 — Template + Signature — sellado simple async con KMS RSA-2048 (lo que vas a usar en este quickstart).
  • Track 2 — Circuit + Envelope — multi-firmante con routing, OTP, access codes y certificado de auditoría. Roadmap a PAdES embedido.

EndpointAcción
POST /v1/templatesSubí PDF base + define X-Placeholders
POST /v1/signaturesSolicitá seal (202, async) — débito 40 Açaí
GET /v1/signatures/{id}Poll hasta status=signed
GET /v1/signatures/{id}/documentDescargá bytes firmados + headers de integridad
LocalmenteVerificá con aws kms verify

Setup:

Ventana de terminal
export DJ_BASE=https://bseal.qa.bjungle.net
export DJ_API_KEY=dj_test_...

Opción recomendada · @bjungle/sdk (TypeScript)

Sección titulada «Opción recomendada · @bjungle/sdk (TypeScript)»
Ventana de terminal
npm install @bjungle/sdk
import { BjungleClient } from '@bjungle/sdk';
const dj = new BjungleClient({
apiKey: process.env.DJ_API_KEY!,
baseUrl: 'https://api.qa.bjungle.net',
moduleUrls: {
bseal: 'https://bseal.qa.bjungle.net',
},
});

El body es el PDF crudo (Content-Type: application/pdf, máximo 25 MB). La metadata viaja en query params + header X-Placeholders.

X-Placeholders es JSON con la lista de campos a rellenar:

[
{ "key": "nombre", "page": 1, "x": 100, "y": 700, "size": 12 },
{ "key": "fecha", "page": 1, "x": 400, "y": 700, "size": 12 },
{ "key": "id", "page": 1, "x": 100, "y": 680, "size": 10 }
]
import fs from 'node:fs/promises';
const pdfBytes = await fs.readFile('contrato.pdf');
const template = await dj.bseal.templates.upload(pdfBytes, {
code: 'contrato-v1',
name: 'Contrato de servicio',
placeholders: [
{ key: 'nombre', page: 1, x: 100, y: 700, size: 12 },
{ key: 'fecha', page: 1, x: 400, y: 700, size: 12 },
],
});
// { id, code, name, version: 1, ... }

El SDK arma el query string + headers (Content-Type: application/pdf, X-Placeholders JSON-encoded) — vos sólo pasás bytes + metadata.

Respuesta 201:

{
"id": "tpl_01J8...",
"code": "contrato-v1",
"name": "Contrato de servicio",
"version": 1,
"created_at": "2026-06-05T14:00:00Z"
}

code debe matchear ^[a-z0-9_-]{1,64}$ y es único por tenant. Cambios al PDF base = subí un template nuevo con code distinto o v2.


const request = await dj.bseal.signatures.create({
template_id: template.id,
subject_ref: 'user-quickstart-001',
merge_data: {
nombre: 'Camila Rodríguez',
fecha: '2026-06-05',
},
});
// { id, status: 'pending', acai_debited: 40, ... }

Respuesta 202:

{
"id": "req_01J8...",
"template_id": "tpl_01J8...",
"subject_ref": "user-quickstart-001",
"status": "pending",
"created_at": "2026-06-05T14:00:00Z",
"acai_debited": 40
}

El worker corre cada 3 segundos y procesa hasta 10 requests en batch:

  1. Descarga el PDF base de S3.
  2. Estampa cada merge_data[key] como text watermark en la posición del placeholder con pdfcpu.
  3. Calcula SHA-256 del PDF renderizado.
  4. Llama KMS.Sign(digest, MessageType=DIGEST, RSASSA_PKCS1_V1_5_SHA_256).
  5. Sube el PDF sellado a S3 y persiste signed_documents.
  6. Flip request → signed y enqueue evento bseal.signature.completed.

Latencia típica sub-5s. Polleá cada 1-2 segundos hasta status ∈ failed.

async function waitSigned(requestId: string, timeoutMs = 30000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const r = await dj.bseal.signatures.get(requestId);
if (r.status === 'signed' || r.status === 'failed') return r;
await new Promise((res) => setTimeout(res, 1500));
}
throw new Error('sealing timeout');
}
const result = await waitSigned(request.id);
if (result.status !== 'signed') throw new Error(result.failure_reason);

El endpoint proxea los bytes server-side (las URLs presignadas de LocalStack no son accesibles desde el browser en dev). Headers de integridad clave:

HeaderValor
X-Document-SHA256SHA-256 del PDF (hex)
X-KMS-Key-ARNARN del KMS key
X-Signature-AlgorithmSiempre RSASSA_PKCS1_V1_5_SHA_256
X-SignatureFirma KMS del digest (hex)
import { writeFile } from 'node:fs/promises';
const { pdf, sha256, kmsKeyArn, signature } = await dj.bseal.signatures.download(request.id);
await writeFile('contrato-firmado.pdf', new Uint8Array(pdf));
console.log({ sha256, kmsKeyArn, signature });

pdf es un ArrayBuffer; los tres metadata strings vienen ya de las headers X-Document-Sha256 / X-Kms-Key-Arn / X-Kms-Signature.


Paso 5 · Verificar la firma con aws kms verify

Sección titulada «Paso 5 · Verificar la firma con aws kms verify»

Cualquier tercero con acceso al KMS key puede verificar offline:

Ventana de terminal
aws kms verify \
--key-id "$KMS" \
--message-type DIGEST \
--signing-algorithm RSASSA_PKCS1_V1_5_SHA_256 \
--message fileb://<(printf '%s' "$SHA" | xxd -r -p) \
--signature fileb://<(printf '%s' "$SIG" | xxd -r -p)

Salida:

{
"KeyId": "arn:aws:kms:us-east-1:...:key/...",
"SignatureValid": true,
"SigningAlgorithm": "RSASSA_PKCS1_V1_5_SHA_256"
}

Alternativa más rápida si confiás en bseal: el SDK expone una verificación server-side que recomputa el SHA-256, llama a KMS y devuelve valid: bool — útil para asserts de tests E2E.

const v = await dj.bseal.signatures.verify(request.id);
console.log(v.valid, v.sha256, v.kms_key_arn);

Para verificación offline / sin depender de bseal, exportá la public key del KMS una vez y verificá RSA-PKCS1v15(SHA-256) localmente:

import { createVerify } from 'node:crypto';
import { readFileSync } from 'node:fs';
const publicKeyPem = readFileSync('kms-public-key.pem', 'utf8');
const pdfBytes = readFileSync('contrato-firmado.pdf');
const expectedSha = process.env.X_DOCUMENT_SHA256; // hex de la header
const signatureHex = process.env.X_SIGNATURE; // hex de la header
import { createHash } from 'node:crypto';
const actualSha = createHash('sha256').update(pdfBytes).digest('hex');
if (actualSha !== expectedSha) throw new Error('digest mismatch');
const verifier = createVerify('RSA-SHA256');
verifier.update(pdfBytes);
const ok = verifier.verify(publicKeyPem, Buffer.from(signatureHex, 'hex'));
console.log({ ok });

kms-public-key.pem se obtiene una vez con:

Ventana de terminal
aws kms get-public-key --key-id "$KMS" --query PublicKey --output text | base64 -d > kms-public-key.der
openssl rsa -pubin -inform DER -in kms-public-key.der -outform PEM -out kms-public-key.pem

Se publica via outbox pattern (at-least-once — dedup en request_id):

{
"event_id": "...",
"tenant_id": "...",
"module": "bseal",
"event_type": "bseal.signature.completed",
"occurred_at":"2026-06-05T14:00:05Z",
"payload": {
"request_id": "req_01J8...",
"template_id": "tpl_01J8...",
"subject_ref": "user-quickstart-001",
"document_sha256": "5d41402abc...",
"kms_key_arn": "arn:aws:kms:us-east-1:...:key/...",
"signature": "<hex>",
"signed_at": "2026-06-05T14:00:04Z"
}
}

Verificá HMAC con el webhook_secret antes de procesar (mismo patrón que KYC — ver quickstart KYC sección Webhook).


Si necesitás múltiples firmantes con OTP/auth, usás circuits + envelopes:

Ventana de terminal
# 1. Crear circuito draft
curl -X POST "$DJ_BASE/v1/circuits" -H "X-API-Key: $DJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code":"contrato-multi","name":"Contrato multi-firmante"}'
# 2. Subir base PDF
curl -X POST "$DJ_BASE/v1/circuits/$CID/base-pdf" -H "X-API-Key: $DJ_API_KEY" \
-H "Content-Type: application/pdf" --data-binary @contrato.pdf
# 3. Definir signers + fields
curl -X PUT "$DJ_BASE/v1/circuits/$CID/definition" -H "X-API-Key: $DJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"signers": [
{ "role": "cliente", "auth": ["otp_email"], "order": 1 },
{ "role": "comercial", "auth": ["access_code"], "order": 2 }
],
"fields": [
{ "key": "firma_cliente", "page": 1, "x": 100, "y": 100, "type": "signature", "signer": "cliente" },
{ "key": "firma_comercial", "page": 1, "x": 400, "y": 100, "type": "signature", "signer": "comercial" }
]
}'
# 4. Publicar
curl -X POST "$DJ_BASE/v1/circuits/$CID/publish" -H "X-API-Key: $DJ_API_KEY"
# 5. Crear envelope (response trae sign tokens por signer — visibles UNA SOLA VEZ)
curl -X POST "$DJ_BASE/v1/envelopes" -H "X-API-Key: $DJ_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"circuit_id\":\"$CID\",\"signers\":[{\"role\":\"cliente\",\"email\":\"camila@x.co\"},{\"role\":\"comercial\",\"email\":\"venta@x.co\"}]}"

Cada signer recibe un link tipo https://sign.bjungle.com/{sign_token}, que golpea la API pública /v1/sign/{token}/* (sin X-API-Key):

  • GET /v1/sign/{token} — estado actual del firmante
  • GET /v1/sign/{token}/document — PDF para previsualizar
  • POST /v1/sign/{token}/consent — registro del consentimiento ESIGN/UETA
  • POST /v1/sign/{token}/otp/send + /otp/verify — auth OTP
  • POST /v1/sign/{token}/access-code — auth por código
  • POST /v1/sign/{token}/sign — firma final

Cuando todos los signers firman, el worker sella + emite bseal.envelope.completed.


SíntomaCausaFix
400 al crear templateHeader X-Placeholders malformadoValidá con jq -e . <<< "$JSON" antes
409 en POST templatescode ya existeSubí con code nuevo o v2
402 al crear signatureBalance < 40 AçaíCargá créditos
status=failed después de pollfailure_reason en respuestaSuele ser PDF inválido o placeholder fuera de página
SignatureValid=false en aws kms verifyEstás pasando hex texto en lugar de bytesUsá xxd -r -p para convertir hex → bytes antes de fileb://
Headers de integridad ausentesEl PDF aún no está firmadoPolleá /v1/signatures/{id} hasta signed antes de descargar

API Reference Bseal

Spec OpenAPI 3.1, schema completo de templates, signatures, circuitos y envelopes.

Abrir →

Sign-in with bjungle (OIDC)

Cómo emitir Bearer JWTs para tu app a partir del wallet del usuario.

Concepto →