# Flow: website screenshots and PDFs with core nodes only (no Chromium on the box)

**URL:** <https://discourse.nodered.org/t/flow-website-screenshots-and-pdfs-with-core-nodes-only-no-chromium-on-the-box/101926>\
**Category:** Share Your Projects\
**Created:** [17 October 2026 06:15 UTC](https://discourse.nodered.org/t/flow-website-screenshots-and-pdfs-with-core-nodes-only-no-chromium-on-the-box/101926 "2026-10-17T06:15:41Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![curlshot](https://avatars.discourse-cdn.com/v4/letter/c/ce7236/32.png) [@curlshot](https://discourse.nodered.org/u/curlshot)\
**Post date:** [17 October 2026 06:15 UTC](https://discourse.nodered.org/t/flow-website-screenshots-and-pdfs-with-core-nodes-only-no-chromium-on-the-box/101926/1 "2026-10-17T06:15:41Z")

</div>

I needed screenshots of a few web pages from Node-RED (a weekly snapshot of some status pages and a dashboard image for a Telegram bot), so here is the small flow I ended up with. It only uses core nodes: `inject`, `function`, `http request`, `switch` and `file`.

It calls a hosted screenshot API, CurlShot (disclosure: I build it; there is a free plan with 100 screenshots a month, no card), so there is no Chromium or Puppeteer to install on the Pi or in the container. The same shape works for any screenshot API that returns the image as the response body.

**How it works**

- The `inject` node sets `msg.url` to the page you want.
- The `function` node builds the query (`format`, `full_page`, `block_cookie_banners`, `block_ads`) and puts the key in an `X-Access-Key` header, read from the env var `CURLSHOT_KEY`.
- The `http request` node returns a **binary buffer**.
- The `switch` checks `statusCode`; a 200 goes to the `file` node, anything else (the API answers errors as JSON with `error_message`) goes to debug.

Change `format` to `pdf` for a PDF of the page, or add `viewport_device=iphone_15_pro` for the mobile layout. Instead of the `file` node you can send `msg.payload` straight to a Telegram or email node.

```json
[
 {
  "id": "cs_inject",
  "type": "inject",
  "name": "Take screenshot",
  "props": [
   {
    "p": "url",
    "v": "https://nodered.org",
    "vt": "str"
   }
  ],
  "repeat": "",
  "once": false,
  "topic": "",
  "x": 140,
  "y": 100,
  "wires": [
   [
    "cs_prep"
   ]
  ]
 },
 {
  "id": "cs_prep",
  "type": "function",
  "name": "Build request",
  "func": "// Put your key in an env var (Settings > Environment) named CURLSHOT_KEY\nconst q = new URLSearchParams({\n url: msg.url,\n format: 'png', // png, jpeg, webp or pdf\n full_page: 'true',\n block_cookie_banners: 'true',\n block_ads: 'true'\n});\nmsg.url = 'https://curlshot.com/api/v1/screenshot?' + q.toString();\nmsg.headers = { 'X-Access-Key': env.get('CURLSHOT_KEY') };\nmsg.filename = '/tmp/screenshot-' + Date.now() + '.png';\nreturn msg;",
  "outputs": 1,
  "x": 330,
  "y": 100,
  "wires": [
   [
    "cs_http"
   ]
  ]
 },
 {
  "id": "cs_http",
  "type": "http request",
  "name": "CurlShot API",
  "method": "GET",
  "ret": "bin",
  "paytoqs": "ignore",
  "url": "",
  "persist": false,
  "senderr": false,
  "x": 510,
  "y": 100,
  "wires": [
   [
    "cs_switch"
   ]
  ]
 },
 {
  "id": "cs_switch",
  "type": "switch",
  "name": "200?",
  "property": "statusCode",
  "propertyType": "msg",
  "rules": [
   {
    "t": "eq",
    "v": "200",
    "vt": "num"
   },
   {
    "t": "else"
   }
  ],
  "checkall": "true",
  "repair": false,
  "outputs": 2,
  "x": 670,
  "y": 100,
  "wires": [
   [
    "cs_file"
   ],
   [
    "cs_debug"
   ]
  ]
 },
 {
  "id": "cs_file",
  "type": "file",
  "name": "Save file",
  "filename": "filename",
  "filenameType": "msg",
  "appendNewline": false,
  "createDir": true,
  "overwriteFile": "true",
  "encoding": "none",
  "x": 830,
  "y": 80,
  "wires": [
   []
  ]
 },
 {
  "id": "cs_debug",
  "type": "debug",
  "name": "Error",
  "active": true,
  "tosidebar": true,
  "complete": "payload",
  "targetType": "msg",
  "x": 830,
  "y": 130,
  "wires": []
 }
]

```

Options are documented at [Documentation | CurlShot](https://curlshot.com/docs) . Feedback welcome, especially on pages that come out wrong.

---

<div class="post-metadata">

**Author:** ![gregorius](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/gregorius/32/73816_2.png) [@gregorius](https://discourse.nodered.org/u/gregorius)\
**Post date:** [17 October 2026 09:07 UTC](https://discourse.nodered.org/t/flow-website-screenshots-and-pdfs-with-core-nodes-only-no-chromium-on-the-box/101926/2 "2026-10-17T09:07:24Z")

</div>

I would think about the naming, CurlShot reminded me not to search for the words "come" and "shot" in combined form. CurlShot, I guess is taken from curl and screenshot, what curl has to do with is beyond me since I'm not taking a screenshot of curl, I'm retrieving data with curl.

Perhaps something like [site shot](https://www.site-shot.com/) or [open graph](https://www.opengraph.io/website-screenshot-tool) or even [screenshot guru](https://screenshot.guru/). Or webshot or pageshot or htmlshot (for the t3kk1es out there). I.e. from the naming it's clear of what I'm taking a screenshot of. Obviously don't be using headshot because you're not retrieving only the `<head>` of the page.

---

<div class="post-metadata">

**Author:** ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)\
**Post date:** [17 October 2026 11:07 UTC](https://discourse.nodered.org/t/flow-website-screenshots-and-pdfs-with-core-nodes-only-no-chromium-on-the-box/101926/3 "2026-10-17T11:07:07Z")

</div>

Plus-points for a clear privacy page on your website. 😃

Of course, the service only works with web pages your service can get to which, when combined with Node-RED which may very often be used on a private network only, somewhat limits its use.

Agree with gregorious (doesn't always happen 😉 ) that the naming could be a little better perhaps.

The final point is something of a bugbear of mine. Your price points jump from free to nearly $200 per year. This can be a major put-off for people who find themselves wanting to use multiple cloud services since the costs very quickly spiral out of control. Personally, I prefer to see services have some intermediate prices, perhaps $5pm to cater for people who might want just a little more than your free tier. Though the free tier is pretty good I think.

I can see that some people may find this useful and I wish you all the best in running the service.
