De la credencial al documento firmado
Esta guía recorre el flujo completo de una integración: subir un PDF, crear el sobre, agregar un
segundo firmante, enviarlo, recibir por webhook la confirmación de que todos firmaron y descargar
el PDF sellado. Continúa donde termina el inicio rápido. Cada paso
muestra la misma llamada en curl, JavaScript y Java; la pestaña elegida se mantiene en toda la
página.
Requisitos: las cuatro variables de entorno del inicio rápido (BBSIGN_BASE_URL,
BBSIGN_TOKEN_URL, BBSIGN_CLIENT_ID, BBSIGN_CLIENT_SECRET), un PDF llamado contract.pdf y,
para las pestañas de JavaScript y Java, un client construido como en el inicio rápido.
1. Obtener un token
Sección titulada «1. Obtener un token»Ambos SDKs obtienen y renuevan los tokens automáticamente. El token explícito se necesita para
curl y para la subida del paso 2, que se hace con HTTP directo. Un token es válido durante
300 s.
TOKEN=$(curl -s -X POST "$BBSIGN_TOKEN_URL" \ -d grant_type=client_credentials \ -d client_id="$BBSIGN_CLIENT_ID" \ -d client_secret="$BBSIGN_CLIENT_SECRET" | jq -r .access_token)const tokenResponse = await fetch(process.env.BBSIGN_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.BBSIGN_CLIENT_ID, client_secret: process.env.BBSIGN_CLIENT_SECRET, }),})const { access_token: token } = await tokenResponse.json()HttpClient http = HttpClient.newHttpClient();String form = "grant_type=client_credentials" + "&client_id=" + URLEncoder.encode(System.getenv("BBSIGN_CLIENT_ID"), StandardCharsets.UTF_8) + "&client_secret=" + URLEncoder.encode(System.getenv("BBSIGN_CLIENT_SECRET"), StandardCharsets.UTF_8);HttpRequest tokenRequest = HttpRequest.newBuilder(URI.create(System.getenv("BBSIGN_TOKEN_URL"))) .header("Content-Type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(form)) .build();String tokenJson = http.send(tokenRequest, HttpResponse.BodyHandlers.ofString()).body();String token = new ObjectMapper().readTree(tokenJson).get("access_token").asText();2. Subir el documento
Sección titulada «2. Subir el documento»POST /api/v1/documents recibe multipart con una sola parte, file. Acepta
application/pdf de hasta 50 MB.
La respuesta es un UploadedDocumentResponse; conserve su id. Un documento que no se adjunta a
un sobre en 24 horas se elimina.
DOCUMENT_ID=$(curl -s -X POST "$BBSIGN_BASE_URL/api/v1/documents" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@contract.pdf;type=application/pdf" | jq -r .id)import { openAsBlob } from 'node:fs'
const form = new FormData()form.set('file', await openAsBlob('contract.pdf', { type: 'application/pdf' }), 'contract.pdf')
const upload = await fetch(`${process.env.BBSIGN_BASE_URL}/api/v1/documents`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form,})const { id: documentId } = await upload.json()OkHttpClient okHttp = new OkHttpClient();RequestBody multipart = new MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("file", "contract.pdf", RequestBody.create(new File("contract.pdf"), MediaType.get("application/pdf"))) .build();Request upload = new Request.Builder() .url(System.getenv("BBSIGN_BASE_URL") + "/api/v1/documents") .header("Authorization", "Bearer " + token) .post(multipart) .build();UUID documentId;try (Response response = okHttp.newCall(upload).execute()) { documentId = UUID.fromString(new ObjectMapper().readTree(response.body().string()).get("id").asText());}Los métodos generados del SDK envían JSON, por lo que las subidas multipart de las pestañas de
JavaScript y Java usan el cliente HTTP de la plataforma (okhttp ya es una dependencia del SDK de
Java). También puede adjuntar un archivo a un borrador existente con
POST /api/v1/envelopes/{id}/documents, con el mismo nombre de parte.
3. Crear el sobre
Sección titulada «3. Crear el sobre»Envíe el id del documento en documentIds. title es el único campo obligatorio; para enviar el
sobre se requieren al menos un firmante y un documento.
ENVELOPE_ID=$(curl -s -X POST "$BBSIGN_BASE_URL/api/v1/envelopes" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"title\": \"Service agreement\", \"documentIds\": [\"$DOCUMENT_ID\"], \"otpRequired\": true, \"signers\": [{\"email\": \"ana@example.com\", \"name\": \"Ana Perez\", \"signOrder\": 1}] }" | jq -r .id)const envelope = await client.envelopes.createEnvelope({ createEnvelopeRequest: { title: 'Service agreement', documentIds: [documentId], otpRequired: true, signers: [{ email: 'ana@example.com', name: 'Ana Perez', signOrder: 1 }], },})EnvelopeResponse envelope = client.execute(client.envelopes().createEnvelope( new CreateEnvelopeRequest() .title("Service agreement") .documentIds(List.of(documentId)) .otpRequired(true) .signers(List.of(new CreateSignerSpec() .email("ana@example.com") .name("Ana Perez") .signOrder(1)))));4. Agregar un firmante
Sección titulada «4. Agregar un firmante»Puede agregar firmantes mientras el sobre está en DRAFT. signOrder aplica cuando
sequentialSigning es true; en caso contrario, todos los firmantes reciben su enlace al mismo
tiempo.
curl -s -X POST "$BBSIGN_BASE_URL/api/v1/envelopes/$ENVELOPE_ID/signers" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "luis@example.com", "name": "Luis Gomez", "signOrder": 2}'await client.envelopes.addSigner({ id: envelope.id, addSignerRequest: { email: 'luis@example.com', name: 'Luis Gomez', signOrder: 2 },})client.execute(client.envelopes().addSigner(envelope.getId(), new AddSignerRequest().email("luis@example.com").name("Luis Gomez").signOrder(2)));5. Enviar el sobre
Sección titulada «5. Enviar el sobre»Al enviar, los documentos y los firmantes quedan fijos, cada firmante recibe por correo un enlace
válido durante 72 horas de forma predeterminada y
se emite envelope.sent. La respuesta es un SendResponse con los tokens de firma de cada
firmante.
curl -s -X POST "$BBSIGN_BASE_URL/api/v1/envelopes/$ENVELOPE_ID/send" \ -H "Authorization: Bearer $TOKEN"const sent = await client.envelopes.sendEnvelope({ id: envelope.id })console.log('Sent', sent.envelopeId)SendResponse sent = client.execute(client.envelopes().sendEnvelope(envelope.getId()));System.out.println("Sent " + sent.getEnvelopeId());6. Recibir envelope.completed
Sección titulada «6. Recibir envelope.completed»Registre un webhook en Configuración, pestaña Webhooks, suscrito a envelope.completed.
Cuando firma el último firmante, bb-sign envía un evento firmado a su URL. Verifique la firma sobre
el cuerpo original, responda 2xx y luego procese el evento. El payload contiene el id y el estado
del sobre; el documento se obtiene en el paso 7. Webhooks describe
la entrega y los reintentos.
POST /webhooks/bb-sign HTTP/1.1Content-Type: application/jsonX-BBSign-Event-Id: 3f8c1e2a-7d44-4b1a-9c0e-5a6b7c8d9e0fX-BBSign-Event-Type: envelope.completedX-BBSign-Signature: t=1760000000,v1=5f1a...
{"id":"3f8c1e2a-7d44-4b1a-9c0e-5a6b7c8d9e0f","type":"envelope.completed","apiVersion":"2026-08-09", "createdAt":"2026-10-10T14:22:31Z","data":{"envelope":{"id":"...","title":"Service agreement", "status":"COMPLETED","createdAt":"...","expiresAt":null}}}import express from 'express'import { verifyWebhookSignature } from '@binarybridges/bb-sign-sdk/webhooks'
const app = express()app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } }))
app.post('/webhooks/bb-sign', (req, res) => { if (!verifyWebhookSignature(req.get('X-BBSign-Signature'), req.rawBody, process.env.BBSIGN_WEBHOOK_SECRET)) { return res.sendStatus(401) } const event = JSON.parse(req.rawBody.toString('utf8')) res.sendStatus(200) if (event.type === 'envelope.completed') void downloadSigned(event.data.envelope.id)})@PostMapping("/webhooks/bb-sign")public ResponseEntity<Void> receive( @RequestHeader("X-BBSign-Signature") String signature, @RequestBody byte[] rawBody) throws IOException {
if (!Webhooks.verify(signature, rawBody, System.getenv("BBSIGN_WEBHOOK_SECRET"))) { return ResponseEntity.status(401).build(); } WebhookEvent event = new ObjectMapper().readValue(rawBody, WebhookEvent.class); if ("envelope.completed".equals(event.getType())) { executor.submit(() -> downloadSigned(event.getData().getEnvelope().getId())); } return ResponseEntity.ok().build();}7. Listar los documentos y descargar el PDF firmado
Sección titulada «7. Listar los documentos y descargar el PDF firmado»GET /api/v1/envelopes/{id}/documents devuelve cada documento con un downloadUrl (el original)
y, cuando el sobre está COMPLETED, un signedUrl (la copia sellada). Ambos son enlaces
prefirmados válidos durante 3.600 s, y son null si la
credencial no tiene document:download en el espacio de trabajo del sobre. Trátelos como
secretos: dan acceso al archivo sin token.
SIGNED_URL=$(curl -s "$BBSIGN_BASE_URL/api/v1/envelopes/$ENVELOPE_ID/documents" \ -H "Authorization: Bearer $TOKEN" | jq -r '.[0].signedUrl')
curl -s -o contract-signed.pdf "$SIGNED_URL"import { writeFile } from 'node:fs/promises'
async function downloadSigned(envelopeId) { const documents = await client.envelopes.listEnvelopeDocuments({ id: envelopeId }) for (const document of documents) { const pdf = await fetch(document.signedUrl) await writeFile(`${document.id}-signed.pdf`, Buffer.from(await pdf.arrayBuffer())) }}void downloadSigned(UUID envelopeId) throws Exception { List<DocumentWithUrlsResponse> documents = client.execute(client.envelopes().listEnvelopeDocuments(envelopeId)); HttpClient http = HttpClient.newHttpClient(); for (DocumentWithUrlsResponse document : documents) { HttpRequest download = HttpRequest.newBuilder(URI.create(document.getSignedUrl())).build(); http.send(download, HttpResponse.BodyHandlers.ofFile(Path.of(document.getId() + "-signed.pdf"))); }}Archive los documentos que descarga: bb-sign conserva los documentos firmados durante un plazo fijo, y su conservación posterior es responsabilidad de la organización. Consulte Alcance y conservación.
Reintentar un POST sin respuesta
Sección titulada «Reintentar un POST sin respuesta»429 y 503 se reintentan también en POST, porque indican que el servidor no procesó la
solicitud.
Espacio de trabajo del sobre
Sección titulada «Espacio de trabajo del sobre»El sobre de este recorrido se archivó en General porque no se envió workspaceId. Una
credencial accede a los espacios de trabajo que un administrador le asignó; GET /api/v1/workspaces
los lista, y cada EnvelopeResponse incluye workspaceId y workspaceName. Los sobres de
espacios que la credencial no puede leer responden 404 y no aparecen en las listas.