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

# Test in CI

> A script that waits for your test fax in the Faxbeep API, in curl, Node and Python.

A test of a fax API or a fax server can end with a check on Faxbeep: send a page to a test number, then wait until the page appears in the API. The scripts on this page do the waiting. They do not send the fax: send it with your own fax provider, fax server or machine.

## How it works

<Steps>
  <Step title="Note the time">
    Just before you send, note the time in UTC, with `Z`:

    ```bash theme={null}
    export SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    ```
  </Step>

  <Step title="Send the test">
    Send your test fax to a [test number](/tests/fax) with your provider or machine. A [scan to email test](/tests/scan-to-email) works the same way.
  </Step>

  <Step title="Wait for it">
    Run one of the scripts below. It asks `GET /api/faxes?since=...` every 15 seconds, and keeps the same `since` on each call.
  </Step>

  <Step title="Check the result">
    The script prints the oldest test after `since` that matches, as JSON, and exits with 0. Check its `page_count`, open its `view_url`, or download its `pdf_url`. If no test matches in time, the script exits with 1.
  </Step>
</Steps>

## The script

Set these environment variables, then run the script.

| Variable | Required | Meaning |
| - | - | - |
| `SINCE` | Yes | The UTC time just before you sent the test, for example `2026-10-01T14:25:00Z` |
| `FROM` | No | Your sender: the full number, the [station ID](/developers/station-id) or the email address |
| `PAGES` | No | The number of pages you sent |
| `TIMEOUT` | No | Seconds to wait before the script gives up. Default: 600 |

Set `FROM` only if your sender ID reaches Faxbeep and only you use it. Most faxes by phone arrive with no sender ID. Without `FROM`, set `PAGES`: other people send tests at the same time.

<CodeGroup>
  ```bash curl theme={null}
  #!/usr/bin/env bash
  # Needs curl and jq.
  set -u
  API="https://faxbeep.com/api/faxes"
  SINCE="${SINCE:?Set SINCE to the UTC time just before you sent the fax}"
  FROM="${FROM:-}"          # your sender ID or address, if you know it
  PAGES="${PAGES:-}"        # the page count you sent, if you know it
  TIMEOUT="${TIMEOUT:-600}" # seconds
  deadline=$(( $(date +%s) + TIMEOUT ))
  tmp=$(mktemp -d)

  while [ "$(date +%s)" -lt "$deadline" ]; do
    args=(--get --silent --show-error --data-urlencode "since=$SINCE" --data-urlencode "limit=100")
    if [ -n "$FROM" ]; then args+=(--data-urlencode "from=$FROM"); fi
    status=$(curl "${args[@]}" -D "$tmp/headers" -o "$tmp/body" -w '%{http_code}' "$API")

    if [ "$status" = "429" ]; then
      wait=$(grep -i '^retry-after:' "$tmp/headers" | tr -dc '0-9')
      sleep "${wait:-60}"
      continue
    fi
    if [ "$status" != "200" ]; then
      echo "HTTP $status: $(cat "$tmp/body")" >&2
      exit 1
    fi

    # The list is newest first, so "last" is the oldest fax after SINCE.
    fax=$(jq -c --arg pages "$PAGES" \
      '[.[] | select($pages == "" or .page_count == ($pages | tonumber))] | last // empty' "$tmp/body")
    if [ -n "$fax" ]; then
      echo "$fax"
      exit 0
    fi
    sleep 15
  done

  echo "No fax after $TIMEOUT seconds" >&2
  exit 1
  ```

  ```javascript Node theme={null}
  // Node 18 or later. No package needed.
  const API = "https://faxbeep.com/api/faxes";
  const since = process.env.SINCE; // the UTC time just before you sent the fax
  const from = process.env.FROM || ""; // your sender ID or address, if you know it
  const pages = process.env.PAGES ? Number(process.env.PAGES) : null;
  const timeout = Number(process.env.TIMEOUT || 600); // seconds

  const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

  async function waitForFax() {
    if (!since) throw new Error("Set SINCE to the UTC time just before you sent the fax");
    const deadline = Date.now() + timeout * 1000;

    while (Date.now() < deadline) {
      const params = new URLSearchParams({ since, limit: "100" });
      if (from) params.set("from", from);
      const res = await fetch(`${API}?${params}`);

      if (res.status === 429) {
        await sleep(Number(res.headers.get("retry-after")) || 60);
        continue;
      }
      if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

      // The list is newest first, so the last match is the oldest fax after SINCE.
      const faxes = await res.json();
      const matches = faxes.filter((fax) => pages === null || fax.page_count === pages);
      if (matches.length > 0) return matches[matches.length - 1];
      await sleep(15);
    }
    throw new Error(`No fax after ${timeout} seconds`);
  }

  waitForFax().then(
    (fax) => console.log(JSON.stringify(fax)),
    (err) => {
      console.error(err.message);
      process.exit(1);
    },
  );
  ```

  ```python Python theme={null}
  # Python 3.8 or later. Standard library only.
  import json
  import os
  import sys
  import time
  import urllib.error
  import urllib.parse
  import urllib.request

  API = "https://faxbeep.com/api/faxes"
  since = os.environ["SINCE"]  # the UTC time just before you sent the fax
  sender = os.environ.get("FROM", "")  # your sender ID or address, if you know it
  pages = int(os.environ["PAGES"]) if os.environ.get("PAGES") else None
  timeout = int(os.environ.get("TIMEOUT", "600"))  # seconds
  deadline = time.time() + timeout

  while time.time() < deadline:
      params = {"since": since, "limit": 100}
      if sender:
          params["from"] = sender
      try:
          # Set a User-Agent: the default one of urllib is refused with 403.
          req = urllib.request.Request(
              f"{API}?{urllib.parse.urlencode(params)}", headers={"User-Agent": "my-fax-test/1.0"}
          )
          with urllib.request.urlopen(req, timeout=30) as res:
              faxes = json.load(res)
      except urllib.error.HTTPError as err:
          if err.code == 429:
              time.sleep(int(err.headers.get("Retry-After", "60")))
              continue
          sys.exit(f"HTTP {err.code}: {err.read().decode()}")

      # The list is newest first, so the last match is the oldest fax after SINCE.
      matches = [fax for fax in faxes if pages is None or fax["page_count"] == pages]
      if matches:
          print(json.dumps(matches[-1]))
          sys.exit(0)
      time.sleep(15)

  sys.exit(f"No fax after {timeout} seconds")
  ```
</CodeGroup>

Run it, for example: `SINCE=2026-10-01T14:25:00Z PAGES=2 node wait-for-fax.mjs`.

## Rules for your own client

* **Keep `since` at the time you noted.** Do not move it to the newest `received_at` that you saw. `received_at` is the time a test arrived. A test appears when its processing is done, so a long fax can appear after a newer short one.
* **Wait between calls.** 15 seconds is enough: a fax by phone usually appears 1 to 2 minutes after the call ends. The rate limit is 60 requests each minute for one IP address, and parallel jobs from one runner share it.
* **Stop after a time limit.** Do not poll forever. 10 minutes is a good limit for one test.
* **On 429, wait.** Wait the number of seconds in the `Retry-After` header, then call again.
* **Ask for up to 100 tests.** The scripts set `limit=100`, so a busy minute does not push your test out of the list.

<Tip>
  Your test is public for 30 days, like every test. Use a test page with no personal or confidential data.
</Tip>

## Next

<Columns cols={2}>
  <Card title="Find your fax" icon="magnifying-glass" href="/developers/find-your-fax">
    One call by time, by sender or by email.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    The rate limit, the errors and the fields of a test.
  </Card>
</Columns>


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