From credential to signed document
This guide walks through the complete integration flow: upload a PDF, create the envelope, add a
second signer, send it, receive webhook confirmation that everyone signed and download the sealed
PDF. It continues where the quickstart ends. Each step shows the
same call in curl, JavaScript and Java; the tab you choose stays selected across the page.
Requirements: the four environment variables from the quickstart (BBSIGN_BASE_URL,
BBSIGN_TOKEN_URL, BBSIGN_CLIENT_ID, BBSIGN_CLIENT_SECRET), a PDF named contract.pdf and,
for the JavaScript and Java tabs, a client built as in the quickstart.
1. Get a token
Section titled “1. Get a token”Both SDKs fetch and refresh tokens automatically. The explicit token is needed for curl and for
the upload in step 2, which uses plain HTTP. A token is valid for
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. Upload the document
Section titled “2. Upload the document”POST /api/v1/documents takes multipart with one part, file. It accepts
application/pdf up to 50 MB.
The response is an UploadedDocumentResponse; keep its id. A document that is not attached to an
envelope within 24 h is deleted.
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());}The generated SDK methods send JSON, so the multipart uploads in the JavaScript and Java tabs use
the platform HTTP client (okhttp is already a dependency of the Java SDK). You can also attach a
file to an existing draft with POST /api/v1/envelopes/{id}/documents, with the same part name.
3. Create the envelope
Section titled “3. Create the envelope”Send the document id in documentIds. title is the only required field; sending the envelope
requires at least one signer and one document.
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. Add a signer
Section titled “4. Add a signer”You can add signers while the envelope is in DRAFT. signOrder applies when
sequentialSigning is true; otherwise every signer receives a link at the same time.
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. Send the envelope
Section titled “5. Send the envelope”On send, the documents and signers become fixed, each signer is emailed a link valid for
72 h by default, and envelope.sent is emitted. The
response is a SendResponse with each signer’s signing tokens.
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. Receive envelope.completed
Section titled “6. Receive envelope.completed”Register a webhook in Settings, tab Webhooks, subscribed to envelope.completed. When the
last signer signs, bb-sign sends a signed event to your URL. Verify the signature over the raw
body, respond 2xx and then process the event. The payload contains the envelope id and status;
the document is retrieved in step 7. Webhooks describes delivery
and retries.
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. List the documents and download the signed PDF
Section titled “7. List the documents and download the signed PDF”GET /api/v1/envelopes/{id}/documents returns each document with a downloadUrl (the original)
and, once the envelope is COMPLETED, a signedUrl (the sealed copy). Both are presigned links
valid for 3,600 s, and they are null if the
credential does not hold document:download in the envelope’s workspace. Treat them as secrets:
they give access to the file without a 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 the documents you download: bb-sign keeps signed documents for a fixed period, and conservation after that is the organization’s responsibility. See Scope and retention.
Retry a POST that got no response
Section titled “Retry a POST that got no response”429 and 503 are also retried for POST, because they indicate the server did not process the
request.
Envelope workspace
Section titled “Envelope workspace”The envelope in this walkthrough was filed in General because no workspaceId was sent. A
credential accesses the workspaces an admin assigned to it; GET /api/v1/workspaces lists them,
and each EnvelopeResponse includes workspaceId and workspaceName. Envelopes in workspaces the
credential cannot read return 404 and do not appear in lists.