PDFPipe

Guide

HTML to PDF in Angular

Angular has no native PDF export. The common instinct is to reach for jsPDF or pdfmake, but both fall short the moment your layout gets complex: custom fonts render wrong, tables split unpredictably across pages, and CSS grid or flexbox is either ignored or approximated. The correct approach for anything beyond a receipt-sized printout is to send your HTML to a rendering endpoint and receive a real PDF back.

Why client-side PDF libraries fall short

jsPDF converts your HTML by walking the DOM and translating elements to PDF drawing commands. That translation loses CSS variables, web fonts loaded via @font-face, positioned overlays, and most modern layout primitives. pdfmake avoids the DOM entirely: you describe the document in its own JSON format, which means rewriting your templates from scratch and manually implementing every rule your CSS already expresses.

An API-based approach skips all of that. You pass your HTML and CSS exactly as it appears in the browser. Fonts, images, and page-break hints all work the same way.

Store the API key in environment.ts

Angular's environment file system lets you swap values between development and production builds. Keep your API key there so it is not scattered across multiple services:

TypeScript
// src/environments/environment.ts
export const environment = {
  production: false,
  pdfpipeApiKey: "your_api_key_here",
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  pdfpipeApiKey: "your_api_key_here",
};

The build system replaces environment.ts with environment.prod.ts when you run ng build --configuration production. For server-rendered Angular apps (Angular Universal, SSR), read the key from a server-side environment variable instead of shipping it in the client bundle.

The PDF service

Create an injectable service that wraps the two most common workflows: streaming a PDF directly to a browser download, and storing the PDF on the CDN to get back a URL.

TypeScript
// src/app/services/pdf.service.ts
import { Injectable } from "@angular/core";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Observable } from "rxjs";
import { environment } from "../../environments/environment";

const API_URL = "https://api.pdfpipe.xyz/v1/pdf";

export interface PdfOptions {
  format?: "A4" | "Letter" | "Legal";
  landscape?: boolean;
  margin?: { top?: string; right?: string; bottom?: string; left?: string };
}

@Injectable({ providedIn: "root" })
export class PdfService {
  constructor(private http: HttpClient) {}

  /** Stream a PDF and trigger a browser download. */
  downloadPdf(html: string, filename: string, options: PdfOptions = {}): void {
    const headers = new HttpHeaders({
      Authorization: `Bearer ${environment.pdfpipeApiKey}`,
      "Content-Type": "application/json",
    });

    this.http
      .post(API_URL, { html, options: { format: "A4", ...options } }, {
        headers,
        responseType: "blob",
      })
      .subscribe({
        next: (blob) => triggerDownload(blob, filename),
        error: (err) => console.error("PDF generation failed", err),
      });
  }

  /** Store the PDF on the CDN and return a download URL. */
  storePdf(
    html: string,
    filename: string,
    options: PdfOptions = {}
  ): Observable<{ document_url: string; document_expires: string }> {
    const headers = new HttpHeaders({
      Authorization: `Bearer ${environment.pdfpipeApiKey}`,
      "Content-Type": "application/json",
    });

    // store: true tells the API to persist the file and return a URL
    // rather than streaming bytes.
    return this.http.post<{ document_url: string; document_expires: string }>(
      API_URL,
      { html, options: { format: "A4", ...options }, store: true, filename },
      { headers }
    );
  }
}

function triggerDownload(blob: Blob, filename: string): void {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  anchor.click();
  URL.revokeObjectURL(url);
}

The downloadPdf method sends your HTML to POST https://api.pdfpipe.xyz/v1/pdf with responseType: "blob". Angular's HttpClient delivers the binary response as a Blob, and triggerDownload creates an object URL, clicks a synthetic anchor, then immediately revokes the URL to free memory.

Component template

Wire the service into a component. The template handles both the download path and the store-and-link path:

TypeScript
// src/app/invoice/invoice.component.ts
import { Component } from "@angular/core";
import { PdfService } from "../services/pdf.service";

@Component({
  selector: "app-invoice",
  templateUrl: "./invoice.component.html",
  styleUrls: ["./invoice.component.css"],
})
export class InvoiceComponent {
  isGenerating = false;
  downloadUrl: string | null = null;

  constructor(private pdfService: PdfService) {}

  downloadPdf(): void {
    this.isGenerating = true;
    const html = this.buildInvoiceHtml("INV-2026-001", "Acme Corp", 1250.0);

    this.pdfService.downloadPdf(html, "invoice-INV-2026-001.pdf", {
      format: "A4",
    });

    // downloadPdf fires the browser download synchronously once the blob
    // arrives; mark generating as done after a short delay for UX.
    setTimeout(() => { this.isGenerating = false; }, 2000);
  }

  storeAndGetUrl(): void {
    this.isGenerating = true;
    const html = this.buildInvoiceHtml("INV-2026-001", "Acme Corp", 1250.0);

    this.pdfService.storePdf(html, "invoice-INV-2026-001.pdf").subscribe({
      next: ({ document_url }) => {
        this.downloadUrl = document_url;
        this.isGenerating = false;
      },
      error: () => { this.isGenerating = false; },
    });
  }

  private buildInvoiceHtml(id: string, client: string, amount: number): string {
    return `
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8" />
        <style>
          body { font-family: system-ui, sans-serif; padding: 40px; color: #1d1812; }
          h1   { font-size: 28px; margin-bottom: 4px; }
          table { width: 100%; border-collapse: collapse; margin-top: 24px; }
          th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e0d9cf; }
          .total { font-weight: 700; }
        </style>
      </head>
      <body>
        <h1>Invoice ${id}</h1>
        <p>Client: ${client}</p>
        <table>
          <thead>
            <tr><th>Description</th><th>Amount</th></tr>
          </thead>
          <tbody>
            <tr><td>Professional services</td><td>$${amount.toFixed(2)}</td></tr>
            <tr class="total"><td>Total</td><td>$${amount.toFixed(2)}</td></tr>
          </tbody>
        </table>
      </body>
      </html>
    `;
  }
}
HTML
<!-- src/app/invoice/invoice.component.html -->
<div class="invoice-actions">
  <button (click)="downloadPdf()" [disabled]="isGenerating">
    {{ isGenerating ? "Generating..." : "Download PDF" }}
  </button>

  <button (click)="storeAndGetUrl()" [disabled]="isGenerating">
    {{ isGenerating ? "Generating..." : "Get download URL" }}
  </button>

  <a *ngIf="downloadUrl" [href]="downloadUrl" target="_blank" rel="noopener noreferrer">
    Open stored PDF
  </a>
</div>

Register HttpClientModule

HttpClient is provided through HttpClientModule. Add it once at the root module, or import it directly into a standalone component if you are on Angular 17 or later:

TypeScript
// src/app/app.module.ts — add HttpClientModule once at the root
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { HttpClientModule } from "@angular/common/http";
import { AppComponent } from "./app.component";
import { InvoiceComponent } from "./invoice/invoice.component";

@NgModule({
  declarations: [AppComponent, InvoiceComponent],
  imports: [BrowserModule, HttpClientModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

// Standalone Angular (v17+): import HttpClientModule in the component instead.
// @Component({ imports: [HttpClientModule], ... })

Store the PDF and get a download URL

Streaming bytes back to the browser works well for on-demand downloads, but sometimes you need a persistent URL: to email to a customer, embed in a dashboard, or pass to another service. Pass store: true in the request body and the API stores the file on the CDN instead of streaming it. The response headers contain the URL and an expiry timestamp:

JSON
// What the API returns when store: true
{
  // Response headers carry the document metadata:
  "X-PDFPipe-Document-Id":      "doc_01HXYZ...",
  "X-PDFPipe-Document-Url":     "https://cdn.pdfpipe.xyz/docs/doc_01HXYZ....pdf",
  "X-PDFPipe-Document-Expires": "2026-07-13T10:00:00Z"
}

// The Angular service maps these into the observable payload:
// { url: "https://cdn.pdfpipe.xyz/...", expiresAt: "2026-07-13T10:00:00Z" }

The storePdf method in the service above reads those headers and returns them as a typed observable. The component subscribes and stores the URL in downloadUrl for the template to render as a link.

Pricing

The Hobby plan includes 500 free documents per month. No credit card is required to start.

PlanPriceDocuments / moNotes
Hobby$0500No card required
Starter$19 / mo5,000Document archive, email support
Growth$49 / mo20,000Priority rendering, longer archive
Scale$149 / mo50,000Webhook delivery, bulk endpoint
Business$499 / moUnlimitedSLA, dedicated support

PDFPipe is the API used in this guide. Try it without signing up in the live playground, or read the full API reference.

Get an API key →