Web SDKv1.0.0

Own the download: export, upload to S3, hand back a link

Hide or take over the SDK's own Download, export the edited document through viewer.createExporter(), upload it to your bucket through a presigned URL, then give the person a link from your backend.
Most requestedtoolbar.downloadcallbacks.onDownloadviewer.createExporter()exporter.uploadTo()exporter.getBlob()exportCompletedviewer.markSaved()
The viewer produces the finished PDF; where it goes is your decision. Two ways to start are shown, because they suit different products: keep the toolbar button and take over what it does with callbacks.onDownload, or hide it with toolbar.download: false and put a Submit action of your own in the toolbar.

What you get

  • The SDK's Download dialog and flow never run; your function does.
  • The exported bytes carry what the viewer holds: the form values and signatures the user entered, and Edit Text's pending edits, written in first.
  • A whole-document export clears viewer.hasUnsavedChanges; whether the upload succeeded is yours to track.

1. Hide the button, or take it over

toolbar.download: false hides the built-in Download. callbacks.onDownload keeps the button and runs your function instead of the SDK's dialog and flow (after forms.validateRequired, when that is on). The S3 example in Framework integrations hides Print and Download and adds a toolbar action; uploadTo() sends Content-Type: application/pdf unless headers overrides it.
viewer.tsTypeScript
const viewer = new PDFViewer(container, doc, {
  toolbar: {
    print: false,
    download: false,
    actions: [
      {
        id: 'submit',
        label: 'Submit Form',
        icon: 'done',
        variant: 'primary',
        onClick: async () => {
          const presignedUrl = await fetchPresignedUrl(); // your API
          const exporter = viewer.createExporter();
          const res = await exporter.uploadTo(presignedUrl, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/pdf' },
          });
          if (!res.ok) {
            showError(res.status); // uploadTo does not reject on an HTTP error
            return;
          }
          showSuccessNotification();
        },
      },
    ],
  },
});

// Or keep the toolbar's Download and take over what it does:
const viewer2 = new PDFViewer(container, doc, {
  callbacks: { onDownload: () => void submit() },
});
A rejected promise from an action's onClick is logged and reported through the error event. Actions are set when the viewer is built and cannot be disabled or relabeled later.

2. Export the bytes yourself when the upload needs more than a PUT

getBlob() returns the same export as a Blob, for an upload with progress of your own or a multipart form. Every export method takes flatten, invalidateSignatures, pages, onProgress(done, total) and signal; a flatten left out comes from the viewer's export option. A flattening export of a signed document is refused with INVALID_STATE unless invalidateSignatures: true is given.
save.tsTypeScript
const controller = new AbortController();

const blob = await viewer.createExporter().getBlob({
  flatten: true,
  onProgress: (done, total) => progress.set(done / total),
  signal: controller.signal, // aborting rejects with ABORTED
});

const res = await fetch(presignedUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/pdf' },
  body: blob,
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);

3. Know when the export finished, and when it is saved

exportCompleted fires on the viewer for the toolbar's Download and for every exporter from viewer.createExporter(), with sizeBytes and partial (true for a page range). An export of the whole document clears hasUnsavedChanges by itself; nothing fires when it goes back to false, so exportCompleted is that moment. Only when you save another way (doc.save(), an upload of bytes you already had) do you call viewer.markSaved() yourself.
save.tsTypeScript
viewer.on('exportCompleted', ({ sizeBytes, partial }) => {
  if (!partial) uploadStatus.set(`Exported ${sizeBytes} bytes`);
});

// Saving through doc.save() instead: the viewer cannot see it, so tell it.
const bytes = await doc.save();
await fetch('/api/documents/42', { method: 'PUT', body: bytes.slice() });
viewer.markSaved();
A signature drawn into a form field on screen is not in doc.save(): it lives in the viewer until viewer.createExporter() writes it. When a viewer is on screen and you want everything the user sees, use the exporter.

Gotchas

  • uploadTo() resolves to the fetch Response and does not reject on an HTTP error status: check response.ok. It rejects with NETWORK_ERROR when the request could not be sent at all (the address, the network, or a CORS refusal: the response must allow your origin) and ABORTED when signal aborted it.
  • forms.validateRequired holds back the toolbar's Download while a required field is empty, with or without your onDownload; viewer.createExporter() is not held back. Gate it yourself with doc.forms.getValues() and the fields.
  • A change made while an export ran is not in the bytes and stays unsaved, and an undo back to the saved state still counts.
  • Each export method rejects with FEATURE_NOT_LICENSED when the license lacks export, INVALID_ARGUMENT when pages is empty or names a page the document does not have, EXPORT_FAILED when the document cannot be written, and DOCUMENT_CLOSED after doc.close().
  • The presigned URL's origin is a request of your own: it needs to be in your page's connect-src.