Zum Inhalt
RAWCaptureBooth User manual
Version 3.29.0

Status webhook

The photo booth reports its status at regular intervals to an address entered by the operator. Every report is a JSON with the same values the remote management portal shows for this photo booth. What the receiver does with it is up to the receiver.

It is switched on in the admin area "Lighting & Accessories" > "Status webhook". Remote management is not required: the webhook also works on a photo booth without the portal.

What it is good for#

  • Show on your own website whether the photo booth is in use right now and how many photos have been taken.
  • Keep your own overview of several photo booths, in a system you already use.
  • Get notified when paper runs low, the camera battery weakens or a photo booth stops reporting - via home automation or a chat.
  • Record counters, for example for billing the customer.

Status webhook or triggers?#

Both report to the outside, but differently:

Status webhookTriggers for external programs
Whenat a fixed interval, every 30 seconds by defaultthe moment a step happens
Whatthe complete status of the photo bootha single event with a few values
Where toan address on the internet or on the networkan address or a program on the photo booth
Typicaldisplay, monitoring, statisticslights, sockets, scripts during the session

Both can be used at the same time. The triggers are described in the chapter Triggers for external programs.

Setting it up#

  1. Provide a receiver: an address that accepts a POST request with JSON and answers with a status from 200 to 299. The examples further down are enough for this.
  2. In the area "Status webhook", enter the address and save. This creates the key for the signature.
  3. Take the key over with "Copy" and enter it at the receiver.
  4. Press "Send test report". The receiver's answer and the content sent appear below.
  5. Switch on "Use status webhook". The first report goes out immediately, then at the set interval.

What arrives#

Every report is a POST request with these headers:

HeaderContent
Content-Typeapplication/json; charset=utf-8
User-AgentRAWCaptureBooth-Statuswebhook/<version>
X-RAWCaptureBooth-TimestampTime of the report in seconds since 1970 (Unix time)
X-RAWCaptureBooth-Signaturesha256= followed by the signature, see below

The content looks like this (shortened to one printer and one buzzer):

{
  "schema_version": 1,
  "sent_at": "2026-10-03T21:14:05+02:00",
  "report_interval_seconds": 30,
  "computer_name": "FOTOBOX-01",
  "rawcapturebooth_version": "3.28.0",
  "rawcapturebooth_latest_version": "3.28.0",
  "cockpit_version": null,
  "cockpit_latest_version": null,
  "uptime": "5:12:03",
  "network_status": "WLAN (Ballroom)",
  "connectivity_mode": "online",
  "email_enabled": true,
  "print_enabled": true,
  "cloud_enabled": false,
  "event_name": "Wedding Miller",
  "sessions_total": 1843,
  "sessions_since_start": 112,
  "session_limit": 0,
  "total_print_jobs": 2210,
  "print_jobs_since_start": 131,
  "print_limit": 300,
  "ai_portraits_total": 0,
  "ai_portraits_since_start": 0,
  "ai_portrait_limit": 0,
  "ai_portrait_warn_threshold": 0,
  "last_session_at": "2026-10-03T19:13:40Z",
  "last_capture_at": "2026-10-03T19:13:40Z",
  "error_count": 0,
  "last_error": null,
  "last_error_at": null,
  "cpu_used_percent": 23.5,
  "memory_used_percent": 61.2,
  "disk_free_gb": 182.4,
  "disk_total_gb": 476.3,
  "outbox_pending": 0,
  "outbox_email_pending": 0,
  "outbox_cloud_pending": 0,
  "camera_battery_percent": null,
  "camera_power_source": "ac",
  "printers": [
    {
      "role": "print",
      "mode": "single",
      "name": "DP-DS620",
      "available": true,
      "state": "ready",
      "queue_length": 0,
      "issues": [],
      "media_remaining": 268,
      "media_capacity": 400,
      "total_prints": 15320
    }
  ],
  "buzzers": [
    {
      "name": "Buzzer 1",
      "connection": "connected",
      "battery_level": 3,
      "millivolt": 4010,
      "seconds_since_press": 25
    }
  ]
}

"Send test report" shows the real content of your own photo booth under "Content sent".

The fields#

A field without a value is null. New fields may be added in later versions; a receiver skips what it does not know. schema_version only rises when the meaning of a field changes or a field is dropped.

FieldContent
schema_versionStructure of the report, currently 1
sent_atTime of sending in the local time of the photo booth, with time zone
report_interval_secondsReporting interval in seconds
computer_nameComputer name of the photo booth - the identifier when several booths report to the same receiver
rawcapturebooth_versionInstalled version
rawcapturebooth_latest_versionLatest released version, empty as long as the photo booth could not retrieve it
cockpit_version, cockpit_latest_versionThe same for RAWCaptureBooth Cockpit, empty without Cockpit
uptimeRunning time since the program started, such as 5:12:03 or 1 day, 2:03:04
network_statusActual network situation: LAN, WLAN (network name), Online or Offline
connectivity_modeConfigured operating mode: online or offline
email_enabled, print_enabled, cloud_enabledWhether e-mail, printing and cloud upload are switched on
event_nameName of the active event
sessions_total, sessions_since_startSessions in total and since the program started
session_limitMaximum number of sessions, 0 means no limit
total_print_jobs, print_jobs_since_start, print_limitThe same for print jobs
ai_portraits_total, ai_portraits_since_start, ai_portrait_limitThe same for AI portraits
ai_portrait_warn_thresholdFrom this number of AI portraits the photo booth warns, 0 means off
last_session_at, last_capture_atTime of the last session and the last capture, usually in UTC (ending in Z)
error_countErrors since the program started
last_error, last_error_atThe last error as text, with its time
cpu_used_percentProcessor load since the last report, empty in the first report after the start
memory_used_percentMemory in use
disk_free_gb, disk_total_gbFree and total space of the drive holding the captures
outbox_pendingWaiting e-mails and uploads, for example without network
outbox_email_pending, outbox_cloud_pendingOf these, e-mails and cloud uploads
camera_battery_percentCamera battery in percent, empty on mains power or when the camera does not report it
camera_power_sourcebattery, ac (mains adapter) or empty
printersOne entry per printer, see below
buzzersOne entry per wireless buzzer, see below

An entry in printers:

FieldContent
roleprint (photo printer) or strip (photo strip printer)
modesingle (one printer) or pool (printer pool, then also strategy)
nameName of the printer in Windows
availableWhether the printer is ready
stateState, such as ready, printing, paused, error, offline or unknown
queue_lengthJobs in the queue
issuesList of reported problems, empty if there are none
media_remaining, media_capacityRemaining media and capacity in sheets, empty if the printer does not report them
total_printsPrints over the printer's lifetime, DNP printers only

An entry in buzzers:

FieldContent
nameName of the wireless buzzer
connectionconnected, sleeping, disconnected, never (never connected) or unavailable (Bluetooth not available)
battery_levelBattery in three levels: 3 full, 2 half, 1 low, 0 unknown
millivoltBattery voltage in millivolts
seconds_since_pressSeconds since the last press, empty if not pressed yet

Checking the signature#

Every report is signed with the key from the admin area. The receiver computes the signature itself and only accepts the report if both match:

  1. Read the content as it arrives, as bytes - do not parse it as JSON and write it out again first.
  2. Join the timestamp from X-RAWCaptureBooth-Timestamp, a dot and the content.
  3. Compute HMAC-SHA256 over this with the key, as a hex string, and put sha256= in front.
  4. Compare with X-RAWCaptureBooth-Signature, in constant time.
  5. Reject reports whose timestamp differs from your own clock by more than five minutes. This way a recorded report cannot be replayed later.

"Generate new" in the admin area replaces the key. Until it has been entered at the receiver, the receiver rejects every report.

Example: display on your own website#

A website cannot accept a report by itself - it needs a small script on the server. This PHP script checks the signature and stores only the values that are meant to be public in a file fotobox-status.json:

<?php
// fotobox-webhook.php - enter as the address in the admin area
$secret = 'ENTER-THE-KEY-HERE';

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_RAWCAPTUREBOOTH_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_RAWCAPTUREBOOTH_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}
$status = json_decode($body, true);
if (!is_array($status)) {
    http_response_code(400);
    exit;
}

// Only pass on what belongs on the website
$public = [
    'event_name' => $status['event_name'] ?? null,
    'sessions_since_start' => $status['sessions_since_start'] ?? 0,
    'report_interval_seconds' => $status['report_interval_seconds'] ?? 30,
    'received_at' => time(),
];
file_put_contents(__DIR__ . '/fotobox-status.json', json_encode($public), LOCK_EX);
http_response_code(204);

The website reads the file and decides for itself whether the photo booth is reachable:

<p id="fotobox">Photo booth: loading …</p>
<script>
fetch('/fotobox-status.json', { cache: 'no-store' })
  .then((response) => response.json())
  .then((status) => {
    const age = Date.now() / 1000 - status.received_at;
    const online = age <= 2 * status.report_interval_seconds;
    document.getElementById('fotobox').textContent = online
      ? 'Photo booth in use, ' + status.sessions_since_start + ' sessions since the start'
      : 'Photo booth currently not reachable';
  });
</script>

Example: receiver in Node.js#

Without additional packages, using the built-in http module:

const http = require('http');
const crypto = require('crypto');

const SECRET = 'ENTER-THE-KEY-HERE';

http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const body = Buffer.concat(chunks);
    const timestamp = req.headers['x-rawcapturebooth-timestamp'] || '';
    const signature = req.headers['x-rawcapturebooth-signature'] || '';
    const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
      .update(timestamp + '.').update(body).digest('hex');
    const valid = signature.length === expected.length
      && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
      && Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
    if (!valid) {
      res.writeHead(401).end();
      return;
    }
    const status = JSON.parse(body.toString('utf8'));
    console.log(status.computer_name, status.event_name, status.sessions_total);
    res.writeHead(204).end();
  });
}).listen(8080);

Detecting that a photo booth no longer reports#

A photo booth cannot report "offline" itself - without a connection no report arrives either. The receiver therefore remembers when the last report arrived. If none arrives for longer than two reporting intervals (report_interval_seconds), the photo booth is not reachable. Two intervals instead of one absorb a single delayed report.

Troubleshooting#

  • "Send test report" returns HTTP 401 or 403: The receiver rejects the signature. Is the key correct? Is the raw content being checked (see above)? Is the server's clock right?
  • HTTP 404: The address is wrong - check path and file name.
  • "Redirect to …": The address redirects, often from http:// to https:// or to an address with www. The photo booth does not follow redirects; enter the target address from the message directly.
  • "No response within 15 seconds": The receiver is not reachable or answers too slowly. It should accept the report and answer immediately instead of processing it first.
  • Reports arrive less often than set: After a failure the photo booth waits longer until the next attempt - 15 seconds first, then twice as long each time, at most a quarter of an hour. After the first success the set interval applies again. How many attempts in a row have failed is shown in the area under "Test and status".

Updated: 01.10.2026 · Version 3.29.0