API Reference Bseal
Spec OpenAPI 3.1, schema completo de templates, signatures, circuitos y envelopes.
Abrir →bseal expone dos tracks de firma:
| Endpoint | Acción |
|---|---|
POST /v1/templates | Subí PDF base + define X-Placeholders |
POST /v1/signatures | Solicitá seal (202, async) — débito 40 Açaí |
GET /v1/signatures/{id} | Poll hasta status=signed |
GET /v1/signatures/{id}/document | Descargá bytes firmados + headers de integridad |
| Localmente | Verificá con aws kms verify |
Setup:
export DJ_BASE=https://bseal.qa.bjungle.netexport DJ_API_KEY=dj_test_...@bjungle/sdk (TypeScript)npm install @bjungle/sdkimport { 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.
curl -X POST "$DJ_BASE/v1/templates?code=contrato-v1&name=Contrato%20de%20servicio" \ -H "X-API-Key: $DJ_API_KEY" \ -H "Content-Type: application/pdf" \ -H 'X-Placeholders: [{"key":"nombre","page":1,"x":100,"y":700,"size":12},{"key":"fecha","page":1,"x":400,"y":700,"size":12}]' \ --data-binary @contrato.pdfimport fs from 'node:fs/promises';
const pdfBytes = await fs.readFile('contrato.pdf');
const placeholders = [ { key: 'nombre', page: 1, x: 100, y: 700, size: 12 }, { key: 'fecha', page: 1, x: 400, y: 700, size: 12 },];
const res = await fetch( `${DJ_BASE}/v1/templates?code=contrato-v1&name=${encodeURIComponent('Contrato de servicio')}`, { method: 'POST', headers: { 'X-API-Key': DJ_API_KEY, 'Content-Type': 'application/pdf', 'X-Placeholders': JSON.stringify(placeholders), }, body: pdfBytes, },);const template = await res.json(); // {id, code, name, version, ...}pdf, _ := os.ReadFile("contrato.pdf")placeholders, _ := json.Marshal([]map[string]any{ {"key":"nombre","page":1,"x":100,"y":700,"size":12}, {"key":"fecha", "page":1,"x":400,"y":700,"size":12},})
req, _ := http.NewRequest("POST", base+"/v1/templates?code=contrato-v1&name=Contrato+de+servicio", bytes.NewReader(pdf))req.Header.Set("X-API-Key", apiKey)req.Header.Set("Content-Type", "application/pdf")req.Header.Set("X-Placeholders", string(placeholders))res, _ := http.DefaultClient.Do(req)defer res.Body.Close()var t struct{ ID string `json:"id"` }_ = json.NewDecoder(res.Body).Decode(&t)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, ... }curl -X POST "$DJ_BASE/v1/signatures" \ -H "X-API-Key: $DJ_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"template_id\": \"$TEMPLATE_ID\", \"subject_ref\": \"user-quickstart-001\", \"merge_data\": { \"nombre\": \"Camila Rodríguez\", \"fecha\": \"2026-06-05\" } }"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:
merge_data[key] como text watermark en la posición del placeholder con pdfcpu.KMS.Sign(digest, MessageType=DIGEST, RSASSA_PKCS1_V1_5_SHA_256).signed_documents.signed y enqueue evento bseal.signature.completed.signedLatencia 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);while true; do STATUS=$(curl -s "$DJ_BASE/v1/signatures/$REQUEST_ID" \ -H "X-API-Key: $DJ_API_KEY" | jq -r .status) echo "status=$STATUS" [[ "$STATUS" == "signed" || "$STATUS" == "failed" ]] && break sleep 1doneasync function waitSigned(requestId, timeoutMs = 30000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const r = await fetch(`${DJ_BASE}/v1/signatures/${requestId}`, { headers: { 'X-API-Key': DJ_API_KEY }, }).then(r => r.json()); if (r.status === 'signed' || r.status === 'failed') return r; await new Promise(r => setTimeout(r, 1500)); } throw new Error('sealing timeout');}
const result = await waitSigned(requestId);if (result.status !== 'signed') throw new Error(result.failure_reason);func waitSigned(ctx context.Context, base, key, id string) (*SignatureRequest, error) { deadline := time.Now().Add(30 * time.Second) for time.Now().Before(deadline) { req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/v1/signatures/%s", base, id), nil) req.Header.Set("X-API-Key", key) res, err := http.DefaultClient.Do(req) if err != nil { return nil, err } var r SignatureRequest _ = json.NewDecoder(res.Body).Decode(&r) res.Body.Close() if r.Status == "signed" || r.Status == "failed" { return &r, nil } time.Sleep(1500 * time.Millisecond) } return nil, errors.New("sealing timeout")}El endpoint proxea los bytes server-side (las URLs presignadas de LocalStack no son accesibles desde el browser en dev). Headers de integridad clave:
| Header | Valor |
|---|---|
X-Document-SHA256 | SHA-256 del PDF (hex) |
X-KMS-Key-ARN | ARN del KMS key |
X-Signature-Algorithm | Siempre RSASSA_PKCS1_V1_5_SHA_256 |
X-Signature | Firma 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.
curl -i "$DJ_BASE/v1/signatures/$REQUEST_ID/document" \ -H "X-API-Key: $DJ_API_KEY" \ -o contrato-firmado.pdf
# Guardar headers de integridadSHA=$(curl -sI "$DJ_BASE/v1/signatures/$REQUEST_ID/document" \ -H "X-API-Key: $DJ_API_KEY" | grep -i 'X-Document-SHA256:' | awk '{print $2}' | tr -d '\r')SIG=$(curl -sI "$DJ_BASE/v1/signatures/$REQUEST_ID/document" \ -H "X-API-Key: $DJ_API_KEY" | grep -i 'X-Signature:' | awk '{print $2}' | tr -d '\r')KMS=$(curl -sI "$DJ_BASE/v1/signatures/$REQUEST_ID/document" \ -H "X-API-Key: $DJ_API_KEY" | grep -i 'X-KMS-Key-ARN:' | awk '{print $2}' | tr -d '\r')aws kms verifyCualquier tercero con acceso al KMS key puede verificar offline:
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 headerconst 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 });pub := mustLoadRSAPublic("kms-public-key.pem")pdf, _ := os.ReadFile("contrato-firmado.pdf")
hash := sha256.Sum256(pdf)sig, _ := hex.DecodeString(os.Getenv("X_SIGNATURE"))
err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], sig)if err != nil { log.Fatal("invalid signature:", err) }log.Println("signature valid")kms-public-key.pem se obtiene una vez con:
aws kms get-public-key --key-id "$KMS" --query PublicKey --output text | base64 -d > kms-public-key.deropenssl rsa -pubin -inform DER -in kms-public-key.der -outform PEM -out kms-public-key.pembseal.signature.completedSe 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:
# 1. Crear circuito draftcurl -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 PDFcurl -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 + fieldscurl -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. Publicarcurl -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 firmanteGET /v1/sign/{token}/document — PDF para previsualizarPOST /v1/sign/{token}/consent — registro del consentimiento ESIGN/UETAPOST /v1/sign/{token}/otp/send + /otp/verify — auth OTPPOST /v1/sign/{token}/access-code — auth por códigoPOST /v1/sign/{token}/sign — firma finalCuando todos los signers firman, el worker sella + emite
bseal.envelope.completed.
| Síntoma | Causa | Fix |
|---|---|---|
400 al crear template | Header X-Placeholders malformado | Validá con jq -e . <<< "$JSON" antes |
409 en POST templates | code ya existe | Subí con code nuevo o v2 |
402 al crear signature | Balance < 40 Açaí | Cargá créditos |
status=failed después de poll | failure_reason en respuesta | Suele ser PDF inválido o placeholder fuera de página |
SignatureValid=false en aws kms verify | Estás pasando hex texto en lugar de bytes | Usá xxd -r -p para convertir hex → bytes antes de fileb:// |
| Headers de integridad ausentes | El PDF aún no está firmado | Polleá /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 →