PDF output

Account Statement

Create a multi-page business account statement from JSON data using vanilla JavaScript, reusable HTML templates, and print-ready CSS.

This example creates a multi-page business account statement from a JSON response. It fetches a generated 60-day ledger from the sample account statement endpoint, then uses vanilla JavaScript and an HTML <template> to populate the account summary, balances, and transaction rows before rendering the PDF.

Keeping the sample data outside the HTML makes the document easier to understand and closer to a real integration. You can replace the example endpoint with your own API while keeping most of the statement layout and rendering request unchanged.

The finished statement includes account details, the statement period, opening and closing balances, credit and debit totals, categorized transactions, and account notices. Print-specific styles keep the transaction table readable as it continues across multiple pages.

Preview of Account Statement

Interactive example

Edit and render this PDF

Change the HTML, CSS, or API options. The browser preview updates as you edit; select PDF to render the final output through the API.

Example code

Use this request in your app

Choose a language and copy a complete request using this example's HTML, CSS, and API options.

const apiId = process.env.HCTI_API_ID;
const apiKey = process.env.HCTI_API_KEY;
if (!apiId || !apiKey) {
    throw new Error("Missing HCTI_API_ID or HCTI_API_KEY");
}
const authorization = `Basic ${Buffer.from(`${apiId}:${apiKey}`).toString("base64")}`;

const request = {
    html: `<p id="loading" class="loading">Loading account statement…</p>

<main id="statement" class="statement" hidden>
  <header class="statement-header">
    <div class="brand">
      <div class="brand-mark">M</div>
      <div>
        <strong data-institution-name></strong>
        <span>Business banking</span>
      </div>
    </div>

    <div class="statement-heading">
      <span>Account statement</span>
      <strong data-statement-period></strong>
      <small data-statement-id></small>
    </div>
  </header>

  <section class="account-details">
    <div>
      <span>Prepared for</span>
      <strong data-account-name></strong>
    </div>
    <div>
      <span>Account</span>
      <strong data-account-type></strong>
    </div>
    <div>
      <span>Account number</span>
      <strong data-account-number></strong>
    </div>
  </section>

  <section class="summary">
    <article class="summary-primary">
      <span>Closing balance</span>
      <strong data-closing-balance></strong>
    </article>
    <article>
      <span>Opening balance</span>
      <strong data-opening-balance></strong>
    </article>
    <article>
      <span>Credits</span>
      <strong class="credit" data-total-credits></strong>
    </article>
    <article>
      <span>Debits</span>
      <strong data-total-debits></strong>
    </article>
  </section>

  <section class="activity">
    <div class="section-heading">
      <div>
        <span>Account activity</span>
        <h1>Transactions</h1>
      </div>
      <p data-transaction-count></p>
    </div>

    <table>
      <thead>
        <tr>
          <th>Date</th>
          <th>Description</th>
          <th class="amount">Amount</th>
        </tr>
      </thead>
      <tbody id="transactions"></tbody>
    </table>
  </section>

  <footer class="statement-footer">
    <div>
      <strong>Questions about this statement?</strong>
      <span data-support></span>
    </div>
    <ul id="notices"></ul>
  </footer>
</main>

<template id="transaction-template">
  <tr>
    <td data-date></td>
    <td class="transaction">
      <strong data-description></strong>
      <span data-category></span>
    </td>
    <td class="amount" data-amount></td>
  </tr>
</template>

<template id="notice-template">
  <li data-notice></li>
</template>

<script>
  const endpoint =
    "https://example-data.htmlcsstoimage.com/account-statement";

  async function renderStatement() {
    const response = await fetch(endpoint);
    const data = await response.json();

    const money = cents =>
      new Intl.NumberFormat(data.account.locale, {
        style: "currency",
        currency: data.account.currency
      }).format(cents / 100);

    const date = value =>
      new Intl.DateTimeFormat(data.account.locale, {
        month: "short",
        day: "numeric",
        year: "numeric",
        timeZone: "UTC"
      }).format(new Date(\`\${value}T00:00:00Z\`));

    document.querySelector("[data-institution-name]").textContent =
      data.institution.name;
    document.querySelector("[data-statement-period]").textContent =
      \`\${date(data.statement.period_start)}–\${date(data.statement.period_end)}\`;
    document.querySelector("[data-statement-id]").textContent =
      data.statement.statement_id;

    document.querySelector("[data-account-name]").textContent =
      data.account.name;
    document.querySelector("[data-account-type]").textContent =
      data.account.account_type;
    document.querySelector("[data-account-number]").textContent =
      data.account.account_number_masked;

    document.querySelector("[data-closing-balance]").textContent =
      money(data.summary.closing_balance_cents);
    document.querySelector("[data-opening-balance]").textContent =
      money(data.summary.opening_balance_cents);
    document.querySelector("[data-total-credits]").textContent =
      \`+\${money(data.summary.total_credits_cents)}\`;
    document.querySelector("[data-total-debits]").textContent =
      money(data.summary.total_debits_cents);
    document.querySelector("[data-transaction-count]").textContent =
      \`\${data.meta.transaction_count} transactions\`;

    document.querySelector("[data-support]").textContent =
      \`\${data.institution.support_email} · \${data.institution.support_phone}\`;

    const transactionTemplate =
      document.querySelector("#transaction-template");
    const transactions = document.createDocumentFragment();

    for (const transaction of data.transactions) {
      const row =
        transactionTemplate.content.firstElementChild.cloneNode(true);

      row.querySelector("[data-date]").textContent =
        date(transaction.posted_on);
      row.querySelector("[data-description]").textContent =
        transaction.description;
      row.querySelector("[data-category]").textContent =
        transaction.category;

      const amount = row.querySelector("[data-amount]");
      amount.textContent = money(transaction.amount_cents);
      amount.classList.add(transaction.direction);

      transactions.append(row);
    }

    document.querySelector("#transactions").append(transactions);

    const noticeTemplate = document.querySelector("#notice-template");
    const notices = document.createDocumentFragment();

    for (const message of data.notices) {
      const notice =
        noticeTemplate.content.firstElementChild.cloneNode(true);

      notice.textContent = message;
      notices.append(notice);
    }

    document.querySelector("#notices").append(notices);
    document.querySelector("#loading").remove();
    document.querySelector("#statement").hidden = false;

    requestAnimationFrame(() => ScreenshotReady());
  }

  renderStatement();
</script>`,
    css: `* {
  box-sizing: border-box;
}

html {
  color: #172033;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 14px;
}

body {
  margin: 0;
  padding: 32px;
  background: #eef2f6;
}

.statement {
  width: 100%;
  max-width: 7.5in;
  margin: 0 auto;
  padding: 36px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 12px 36px rgba(23, 32, 51, 0.12);
}

.loading {
  margin: 0;
  color: #64748b;
}

.statement-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding-bottom: 24px;
  border-bottom: 2px solid #172033;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
}

.brand-mark {
  display: grid;
  width: 40px;
  height: 40px;
  place-items: center;
  color: white;
  background: #3157d5;
  border-radius: 10px;
  font-size: 20px;
  font-weight: 800;
}

.brand strong,
.brand span,
.statement-heading span,
.statement-heading strong,
.statement-heading small {
  display: block;
}

.brand strong {
  font-size: 15px;
}

.brand span,
.statement-heading span,
.statement-heading small {
  margin-top: 3px;
  color: #64748b;
  font-size: 11px;
}

.statement-heading {
  text-align: right;
}

.statement-heading span {
  margin: 0 0 5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.statement-heading strong {
  font-size: 15px;
}

.account-details {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr;
  gap: 24px;
  padding: 22px 0;
}

.account-details span,
.summary span {
  display: block;
  margin-bottom: 6px;
  color: #64748b;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.account-details strong {
  font-size: 13px;
}

.summary {
  display: grid;
  grid-template-columns: 1.4fr repeat(3, 1fr);
  overflow: hidden;
  margin-bottom: 32px;
  border: 1px solid #dbe2ea;
  border-radius: 10px;
}

.summary article {
  min-height: 82px;
  padding: 17px;
  border-left: 1px solid #dbe2ea;
}

.summary article:first-child {
  border-left: 0;
}

.summary strong {
  font-size: 15px;
}

.summary-primary {
  color: white;
  background: #172033;
}

.summary-primary span {
  color: #b8c3d4;
}

.summary-primary strong {
  font-size: 21px;
}

.credit {
  color: #16805b;
}

.section-heading {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 13px;
}

.section-heading span {
  color: #3157d5;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.section-heading h1 {
  margin: 3px 0 0;
  font-size: 20px;
}

.section-heading p {
  margin: 0;
  color: #64748b;
  font-size: 11px;
}

table {
  width: 100%;
  border-collapse: collapse;
}

thead {
  display: table-header-group;
}

th {
  padding: 10px 8px;
  color: #64748b;
  background: #f5f7fa;
  border-bottom: 1px solid #dbe2ea;
  font-size: 10px;
  letter-spacing: 0.07em;
  text-align: left;
  text-transform: uppercase;
}

td {
  padding: 10px 8px;
  border-bottom: 1px solid #e7ebf0;
  font-size: 11px;
  vertical-align: top;
}

tr {
  break-inside: avoid;
}

th:first-child,
td:first-child {
  width: 106px;
}

.amount {
  width: 105px;
  text-align: right;
  white-space: nowrap;
}

.transaction strong,
.transaction span {
  display: block;
}

.transaction span {
  margin-top: 3px;
  color: #718096;
  font-size: 9px;
}

td.credit {
  color: #16805b;
  font-weight: 700;
}

.statement-footer {
  display: grid;
  grid-template-columns: 1fr 1.5fr;
  gap: 30px;
  margin-top: 28px;
  padding-top: 20px;
  border-top: 2px solid #172033;
  break-inside: avoid;
}

.statement-footer strong,
.statement-footer span {
  display: block;
}

.statement-footer span,
.statement-footer li {
  color: #64748b;
  font-size: 9px;
  line-height: 1.55;
}

.statement-footer ul {
  margin: 0;
  padding-left: 16px;
}

@media print {
  body {
    padding: 0;
    background: white;
  }

  .statement {
    max-width: none;
    padding: 0;
    border-radius: 0;
    box-shadow: none;
  }
}`,
    render_when_ready: true,
    pdf_options: {
        page_height: "11in",
        page_width: "8.5in",
        scale: 1,
        margins: [
            "0.4in",
            "0.4in",
            "0.4in",
            "0.4in",
        ],
        print_background: true,
    },
    media_type: "print",
};

const response = await fetch("https://hcti.io/v1/image", {
    method: "POST",
    headers: {
        Authorization: authorization,
        "Content-Type": "application/json",
    },
    body: JSON.stringify(request),
});
if (!response.ok) {
    throw new Error(`HCTI request failed: ${response.status} ${await response.text()}`);
}
const result = await response.json() as { url: string };
console.log(result.url);

Request configuration

Options used in this example

These values are sent with the HTML and CSS to control how HCTI loads the page and captures the final PDF. You can change each one in the editor above.

Render When Ready

render_when_ready
True
Waits until the page signals that the screenshot is ready. The image fails if the readiness signal is never sent.
Learn about this option

Media Type

media_type
print
Sets the CSS media type used while rendering the page.

PDF Options

pdf_options
HCTI.PDFOptionsV2
Options used when generating a PDF instead of an image.
Learn about this option

From code to screenshot

How HCTI examples work

HTML/CSS to Image renders HTML and CSS in hosted Chrome, then captures the result as an image or PDF. Each example combines real source code with the API options used for the render, so it is both a working demo and a starting point for your own request.

Edit the source and options above to test changes in the browser, then render through the API to see the production result.

Common questions

Can I use the HTML and CSS from these examples?

Yes. Open an example to inspect and edit its complete HTML, CSS, and render options. Use it as a starting point, then send the resulting request from your application.

What is the difference between an example and a template?

Examples are complete API requests built from HTML and CSS. Templates are saved, reusable designs created in the visual editor and rendered repeatedly with different variable values.

Does HCTI create screenshots and PDFs?

Yes. HCTI can render HTML and CSS or capture a public webpage as an image, and it can generate PDFs when you need document output instead of a screenshot.

Keep exploring

Related examples

View all examples