> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ledgersyncappv2.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Turn a bank statement PDF into transactions

> Upload a bank statement PDF on a LedgerSync page and get its transactions in your chat, and a CSV, Excel or QuickBooks file.

export const AiStuck = () => <div className="lsai-stuck">
    Stuck? <a href="https://ledgersync.com/book-a-demo" target="_blank" rel="noopener noreferrer">Book a call with
    our team</a> and we'll help you, or email{" "}
    <a href="mailto:support@ledgersync.com">support@ledgersync.com</a> with the step and what the screen says.
    Never send passwords or card numbers.
  </div>;

export const AiNote = ({tone, children}) => <div className={"lsai-note lsai-note-" + (tone || "gray")}>{children}</div>;

export const AiStep = ({id, n, total, label, title, img, caption, imgs, stage, children}) => {
  const [open, setOpen] = useState(null);
  const closeRef = useRef(null);
  const openerRef = useRef(null);
  useEffect(() => {
    if (!open) return undefined;
    const root = document.documentElement;
    const overflow = root.style.overflow;
    root.style.overflow = "hidden";
    if (closeRef.current) closeRef.current.focus({
      preventScroll: true
    });
    return () => {
      root.style.overflow = overflow;
      if (openerRef.current) openerRef.current.focus({
        preventScroll: true
      });
    };
  }, [open]);
  const pictures = imgs || (img ? [{
    src: img,
    caption
  }] : []);
  return <div className="lsai-step" id={id} data-stage={stage}>
      <div className="lsai-step-num" aria-hidden="true">{n}</div>
      <div className="lsai-step-main">
        <div className="lsai-step-label">{label || "Step " + n + " of " + total}</div>
        <div className="lsai-step-title">{title}</div>
        <div className="lsai-step-text">{children}</div>
      </div>
      {pictures.length > 0 && <div className="lsai-thumbs">
          {pictures.map(pic => <a key={pic.src} className="lsai-thumb" href={pic.src} title="Click to see it larger" onClick={e => {
    e.preventDefault();
    openerRef.current = e.currentTarget;
    setOpen(pic);
  }}>
              <img src={pic.src} alt="" loading="lazy" />
              <span className="lsai-thumb-caption">{pic.caption ? pic.caption + " · " : ""}click to enlarge</span>
            </a>)}
        </div>}
      {open && <div className="lsai-lightbox" role="dialog" aria-modal="true" aria-label={open.caption || title} onClick={e => {
    if (e.detail < 2) setOpen(null);
  }} onKeyDown={e => {
    if (e.key === "Escape") setOpen(null);
    if (e.key === "Tab") e.preventDefault();
  }}>
          <button ref={closeRef} type="button" className="lsai-lightbox-close" onClick={() => setOpen(null)}>Close</button>
          <img src={open.src} alt={open.caption || title} />
          <span className="lsai-lightbox-note">{open.caption ? open.caption + " · " : ""}Click anywhere to close</span>
        </div>}
    </div>;
};

export const AiPage = ({title, subtitle, video, poster, stage, children}) => {
  const [playing, setPlaying] = useState(false);
  const [current, setCurrent] = useState(stage);
  const pageRef = useRef(null);
  const markRef = useRef(null);
  const topRef = useRef(null);
  const watchRef = useRef(null);
  const closeRef = useRef(null);
  useEffect(() => {
    if (!stage || !topRef.current || !pageRef.current) return undefined;
    const page = pageRef.current;
    const top = topRef.current;
    let frame = 0;
    const update = () => {
      frame = 0;
      const css = getComputedStyle(top);
      const stuck = css.position === "sticky" && markRef.current.getBoundingClientRect().top < (parseFloat(css.top) || 0);
      if (stuck !== top.classList.contains("lsai-top-stuck")) {
        const root = document.documentElement;
        root.style.overflowAnchor = "none";
        requestAnimationFrame(() => requestAnimationFrame(() => {
          root.style.overflowAnchor = "";
        }));
        if (stuck) {
          const before = top.offsetHeight;
          const margin = parseFloat(css.marginBottom) || 0;
          top.style.marginBottom = margin + before + "px";
          top.classList.add("lsai-top-stuck");
          top.style.marginBottom = margin + before - top.offsetHeight + "px";
        } else {
          top.classList.remove("lsai-top-stuck");
          top.style.marginBottom = "";
        }
      }
      const atEnd = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 8;
      const line = atEnd ? Infinity : top.getBoundingClientRect().bottom + 120;
      if (stuck) {
        const height = top.offsetHeight + "px";
        if (document.documentElement.style.getPropertyValue("--lsai-pinned-h") !== height) {
          document.documentElement.style.setProperty("--lsai-pinned-h", height);
        }
      }
      let next = stage;
      page.querySelectorAll("[data-stage]").forEach(el => {
        if (el.getBoundingClientRect().top < line) next = Number(el.getAttribute("data-stage"));
      });
      setCurrent(next);
    };
    const schedule = () => {
      if (!frame) frame = requestAnimationFrame(update);
    };
    const reset = () => {
      top.classList.remove("lsai-top-stuck");
      top.style.marginBottom = "";
      schedule();
    };
    const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
    if (observer) {
      observer.observe(top, {
        box: "border-box"
      });
      observer.observe(page);
    }
    window.addEventListener("scroll", schedule, {
      passive: true
    });
    window.addEventListener("resize", reset);
    update();
    return () => {
      document.documentElement.style.removeProperty("--lsai-pinned-h");
      if (observer) observer.disconnect();
      window.removeEventListener("scroll", schedule);
      window.removeEventListener("resize", reset);
      if (frame) cancelAnimationFrame(frame);
    };
  }, [stage, title]);
  useEffect(() => {
    if (!playing) return undefined;
    const root = document.documentElement;
    const overflow = root.style.overflow;
    root.style.overflow = "hidden";
    if (closeRef.current) closeRef.current.focus({
      preventScroll: true
    });
    return () => {
      root.style.overflow = overflow;
      if (watchRef.current) watchRef.current.focus({
        preventScroll: true
      });
    };
  }, [playing]);
  const stages = ["Get ready", "Add it to your AI", "Sign in", "Click Allow", "Ask a question"];
  return <div ref={pageRef} className={stage ? "lsai-page lsai-page-pinned" : "lsai-page"}>
      <div ref={markRef} className="lsai-top-mark" aria-hidden="true" />
      <div ref={topRef} className="lsai-top">
        <div className="lsai-head">
          <div className="lsai-head-text">
            <h1 className="lsai-title">{title}</h1>
            {subtitle && <p className="lsai-subtitle">{subtitle}</p>}
          </div>
          {video && <button ref={watchRef} type="button" className="lsai-video-button" onClick={() => setPlaying(true)}>
              <span className="lsai-video-play" aria-hidden="true">▶</span>
              <span className="lsai-video-label"><strong>Watch it first</strong><span>Short setup video</span></span>
            </button>}
        </div>
        {stage && <div className="lsai-progress-wrap">
            <ol className="lsai-progress" aria-label="Setup progress">
              {stages.map((label, i) => {
    const n = i + 1;
    const state = n < current ? "lsai-done" : n === current ? "lsai-current" : "lsai-todo";
    return <li key={label} className={"lsai-progress-item " + state} aria-current={n === current ? "step" : undefined}>
                    <span className="lsai-progress-bar" />
                    <span className="lsai-progress-label">{label}</span>
                  </li>;
  })}
            </ol>
            <div className="lsai-progress-now">{stages[current - 1]}</div>
          </div>}
      </div>
      {playing && <div className="lsai-lightbox lsai-video-box" role="dialog" aria-modal="true" aria-label={title + ": setup video"} tabIndex={-1} onClick={e => {
    if (e.target === e.currentTarget) setPlaying(false);
  }} onKeyDown={e => {
    if (e.key === "Escape") setPlaying(false);
    if (e.key === "Tab") {
      e.preventDefault();
      const close = e.currentTarget.querySelector("button");
      (document.activeElement === close ? e.currentTarget.querySelector("video") : close).focus({
        preventScroll: true
      });
    }
  }}>
          <button ref={closeRef} type="button" className="lsai-lightbox-close" onClick={() => setPlaying(false)}>Close</button>
          <video className="lsai-video-player" controls autoPlay playsInline tabIndex={0} poster={poster} src={video} />
          <span className="lsai-lightbox-note">Click Close when you are done</span>
        </div>}
      {children}
    </div>;
};

<AiPage title="Turn a bank statement PDF into transactions" subtitle="LedgerSync reads the PDF and shows the transactions in your chat. It can also make a CSV (spreadsheet), Excel or QuickBooks file. Nothing is added to a client.">
  <AiNote tone="gray">
    **Don't attach the PDF to the chat.** The AI cannot pass a file to LedgerSync. It gives you a LedgerSync page
    to upload it instead.
  </AiNote>

  <AiStep id="ask" n={1} total={5} title="Ask the AI">
    Ask: *"Convert a bank statement PDF into transactions."* The AI gives you a link to a LedgerSync page.
  </AiStep>

  <AiNote tone="yellow">
    **The AI does not know how to do this?** Your AI still has an older copy of LedgerSync's tools. Do
    [After a LedgerSync update](/ai/troubleshooting#after-an-update), then start a new chat and ask again.
  </AiNote>

  <AiStep id="open" n={2} total={5} title="Open the link">
    Click the link the AI gave you. The page says **Upload your bank statement**.
  </AiStep>

  <AiStep id="choose" n={3} total={5} title="Choose the PDF" img="https://mintcdn.com/ledgersync/mfvqSld5Ek6ODF06/images/ai-convert/upload-1-choose.png?fit=max&auto=format&n=mfvqSld5Ek6ODF06&q=85&s=a111c1e2004d34a84de054c08d1abf08" caption="Choose PDF" width="760" height="330" data-path="images/ai-convert/upload-1-choose.png">
    Click **Choose PDF** and pick the statement on your computer or phone.
  </AiStep>

  <AiStep id="upload" n={4} total={5} title="Click Upload" imgs={[{ src: "/images/ai-convert/upload-2-confirm.png", caption: "Upload" }, { src: "/images/ai-convert/upload-3-done.png", caption: "Done" }]}>
    The page shows the file's name. Click **Upload**. Wait until the page says **Done**.

    Picked the wrong file? Click **Choose a different file** before you click **Upload**.
  </AiStep>

  <AiStep id="done" n={5} total={5} title="Tell the AI you are done">
    Go back to your chat and tell the AI: *"Done."* It shows the transactions. A long statement? The AI may offer a
    file instead of listing every line.

    Still reading? Wait a minute, then say *"Done."* again.
  </AiStep>

  <AiNote tone="gray">
    **Want a file?** Ask: *"Give me an Excel file."* Or a CSV, or a QuickBooks file. The AI gives you a download
    link. It works once, for 10 minutes. Link not working? Ask the AI for a new one.
  </AiNote>

  <AiNote tone="green">
    **The AI tells you when the totals can't be checked.** If the transactions don't match the statement's
    balances, or the balances could not be read, check them before you use them.
  </AiNote>

  <AiNote tone="gray">
    **One link, one statement.** Each link takes one PDF and works for 24 hours. After 3 failed uploads it stops
    working. For another statement, or a new try, ask again.
  </AiNote>

  <AiNote tone="gray">
    **It is also in the LedgerSync app.** The conversion shows on your **PDF Converter** page, like one you made
    there.
  </AiNote>

  Something went wrong? See [Troubleshooting](/ai/troubleshooting).

  <Accordion title="For developers: tools, files and limits">
    * Tools: `ledgersync_create_statement_upload_link` makes the upload page link and a `conversion_id`
      (`cnv_...`). `ledgersync_get_statement_conversion` says `waiting_for_upload`, `in_progress`, `completed`
      (with the transactions, a page at a time), `failed` or `expired`. `ledgersync_get_conversion_download_link`
      makes a one-time link to the file: `csv`, `xlsx`, `qbo` or `ofx`.
    * It is the same reader as the PDF Converter in the LedgerSync app and `POST /v3/statements/extract`.
    * Each person's conversions are their own: the owner, administrators and employees each see only the ones
      they made. A read-only connection can read conversions but cannot upload.
    * Limits: your firm can convert 3 statements at a time and 500 a day on live (2 and 50 on sandbox), shared
      with your API keys. Each AI app can make 30 upload links an hour. PDFs can be up to 30 MB.
  </Accordion>

  <AiStuck />
</AiPage>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.