Run the screenshot CLI in CI
This guide puts the rs CLI into a GitHub Actions workflow and a GitLab CI job. Both install a pinned release, check its SHA-256 checksum, read the API key from a CI secret and keep the screenshots as build artifacts. For other guides, go back to the guides hub.
Tested on 2026-10-15 with rs v0.1.2 (rs_linux_amd64.tar.gz) in alpine:3.20 and ubuntu:24.04 containers. The workflow file passed actionlint 1.7.7.
What you need
- An API key that starts with
rs_live_. Create one after you sign up. - The key stored as a CI secret named
RS_API_KEY. The CLI reads this variable before its config file, as the CLI authentication page explains.
Install a pinned release, not latest
The release assets are archives named rs_<os>_<arch>.tar.gz, with a checksums.txt file beside them. Pin the version in one variable and check the archive before you run it. A pinned version keeps a new CLI release from changing a build that passed yesterday.
RS_VERSION=v0.1.2 base="https://github.com/Render-Screenshot/rs-cli/releases/download/${RS_VERSION}" curl -fsSLO "${base}/rs_linux_amd64.tar.gz" curl -fsSLO "${base}/checksums.txt" grep ' rs_linux_amd64.tar.gz$' checksums.txt | sha256sum -c - tar -xzf rs_linux_amd64.tar.gz rs sudo install -m 0755 rs /usr/local/bin/rs rs --version
The binary is statically linked, so the same archive runs on Ubuntu runners and on Alpine images. On an ARM runner, use rs_linux_arm64.tar.gz.
GitHub Actions
Save this as .github/workflows/screenshots.yml:
name: Screenshots on: push: branches: [main] pull_request: workflow_dispatch: permissions: contents: read jobs: capture: runs-on: ubuntu-latest timeout-minutes: 10 env: RS_VERSION: v0.1.2 steps: - name: Install the rs CLI (pinned, checksum checked) run: | set -euo pipefail base="https://github.com/Render-Screenshot/rs-cli/releases/download/${RS_VERSION}" curl -fsSLO "${base}/rs_linux_amd64.tar.gz" curl -fsSLO "${base}/checksums.txt" grep ' rs_linux_amd64.tar.gz$' checksums.txt | sha256sum -c - tar -xzf rs_linux_amd64.tar.gz rs sudo install -m 0755 rs /usr/local/bin/rs rs --version - name: Capture screenshots env: RS_API_KEY: ${{ secrets.RS_API_KEY }} run: | set -euo pipefail if [ -z "${RS_API_KEY:-}" ]; then echo "::notice::RS_API_KEY is not available to this run (for example a pull request from a fork). Skipping." exit 0 fi mkdir -p screenshots rs take https://example.com --preset og_card -o screenshots/home-og.png --quiet rs take https://example.com --device iphone_14_pro -o screenshots/home-mobile.png --quiet test -s screenshots/home-og.png test -s screenshots/home-mobile.png - name: Upload screenshots if: ${{ hashFiles('screenshots/*.png') != '' }} uses: actions/upload-artifact@v4 with: name: screenshots path: screenshots/ retention-days: 7
Three choices in this file matter for secrets:
- The key is set as
envon the one step that needs it. The install step never sees it. permissions: contents: readlimits what the job token can do.- GitHub does not pass repository secrets to runs for pull requests from forks. The
-zcheck turns that case into a notice instead of a failed build.
GitLab CI
Add a CI/CD variable named RS_API_KEY in Settings > CI/CD > Variables. Mark it Masked so the value is hidden in job logs. Mark it Protected if only protected branches may spend credits. A protected variable is empty on other branches, so the job below runs only on the default branch.
screenshots: image: alpine:3.20 variables: RS_VERSION: v0.1.2 rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH before_script: - apk add --no-cache curl - base="https://github.com/Render-Screenshot/rs-cli/releases/download/${RS_VERSION}" - curl -fsSLO "${base}/rs_linux_amd64.tar.gz" - curl -fsSLO "${base}/checksums.txt" - grep ' rs_linux_amd64.tar.gz$' checksums.txt | sha256sum -c - - tar -xzf rs_linux_amd64.tar.gz rs - install -m 0755 rs /usr/local/bin/rs script: - mkdir -p screenshots - rs take https://example.com --preset og_card -o screenshots/home-og.png --quiet - test -s screenshots/home-og.png artifacts: paths: - screenshots/ expire_in: 1 week
Exit codes we measured
We ran rs v0.1.2 in an Alpine container with three key states:
| Key state | Exit code | Output |
|---|---|---|
RS_API_KEY not set and no config file |
2 | Error: no API key configured. Run 'rs login' or set RS_API_KEY |
| Key set, API rejects it (HTTP 401) | 1 | Nothing, even with --verbose |
| Valid key, page captured | 0 | The file you named with -o |
A non-zero exit fails the step, so a bad key cannot pass the build. Because a rejected key is silent in this version, the troubleshooting table starts with the key.
The same request without the CLI
If your CI image cannot download binaries, send the request with curl. The GET form:
curl -sS --fail \ -H "Authorization: Bearer $RS_API_KEY" \ "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&preset=og_card" \ -o home-og.png
The POST form with a JSON body:
curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "preset": "og_card"}' \ -o home-og.png
--fail makes curl exit 22 on an HTTP error and keeps the error body out of your PNG file. Remove it while you debug to see the JSON error.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Step exits 1 with no output | The API rejected the key | Check that the secret name is RS_API_KEY, that the key starts with rs_live_ and that it was not revoked. |
| Exit 2, "no API key configured" | The variable is empty in this job | On GitHub, check fork pull requests and the step env. On GitLab, check protected-variable rules. |
sha256sum: WARNING: 1 computed checksum did NOT match |
Corrupt or changed download | Do not install it. Run the job again; if it still fails, check the release page. |
429 errors in a matrix build |
Too many parallel jobs for your plan | Lower the matrix max-parallel value. See rate limits. |
Limits to plan for
- Each capture renders fresh and uses one credit, unless you opt in to cached results (caching). A scheduled workflow that runs every hour on 10 URLs uses 240 credits a day.
- A page has at most 60 seconds to load (
wait.timeout). - Requests per minute and concurrent requests depend on your plan, listed on the rate limits page.
Related
- CLI command reference
- Raw HTTP API guide if you call the API from CI without the CLI.
- Batch endpoint for many URLs in one request