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 webhook | Triggers for external programs | |
|---|---|---|
| When | at a fixed interval, every 30 seconds by default | the moment a step happens |
| What | the complete status of the photo booth | a single event with a few values |
| Where to | an address on the internet or on the network | an address or a program on the photo booth |
| Typical | display, monitoring, statistics | lights, 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#
- 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.
- In the area "Status webhook", enter the address and save. This creates the key for the signature.
- Take the key over with "Copy" and enter it at the receiver.
- Press "Send test report". The receiver's answer and the content sent appear below.
- 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:
| Header | Content |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | RAWCaptureBooth-Statuswebhook/<version> |
X-RAWCaptureBooth-Timestamp | Time of the report in seconds since 1970 (Unix time) |
X-RAWCaptureBooth-Signature | sha256= 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.
| Field | Content |
|---|---|
schema_version | Structure of the report, currently 1 |
sent_at | Time of sending in the local time of the photo booth, with time zone |
report_interval_seconds | Reporting interval in seconds |
computer_name | Computer name of the photo booth - the identifier when several booths report to the same receiver |
rawcapturebooth_version | Installed version |
rawcapturebooth_latest_version | Latest released version, empty as long as the photo booth could not retrieve it |
cockpit_version, cockpit_latest_version | The same for RAWCaptureBooth Cockpit, empty without Cockpit |
uptime | Running time since the program started, such as 5:12:03 or 1 day, 2:03:04 |
network_status | Actual network situation: LAN, WLAN (network name), Online or Offline |
connectivity_mode | Configured operating mode: online or offline |
email_enabled, print_enabled, cloud_enabled | Whether e-mail, printing and cloud upload are switched on |
event_name | Name of the active event |
sessions_total, sessions_since_start | Sessions in total and since the program started |
session_limit | Maximum number of sessions, 0 means no limit |
total_print_jobs, print_jobs_since_start, print_limit | The same for print jobs |
ai_portraits_total, ai_portraits_since_start, ai_portrait_limit | The same for AI portraits |
ai_portrait_warn_threshold | From this number of AI portraits the photo booth warns, 0 means off |
last_session_at, last_capture_at | Time of the last session and the last capture, usually in UTC (ending in Z) |
error_count | Errors since the program started |
last_error, last_error_at | The last error as text, with its time |
cpu_used_percent | Processor load since the last report, empty in the first report after the start |
memory_used_percent | Memory in use |
disk_free_gb, disk_total_gb | Free and total space of the drive holding the captures |
outbox_pending | Waiting e-mails and uploads, for example without network |
outbox_email_pending, outbox_cloud_pending | Of these, e-mails and cloud uploads |
camera_battery_percent | Camera battery in percent, empty on mains power or when the camera does not report it |
camera_power_source | battery, ac (mains adapter) or empty |
printers | One entry per printer, see below |
buzzers | One entry per wireless buzzer, see below |
An entry in printers:
| Field | Content |
|---|---|
role | print (photo printer) or strip (photo strip printer) |
mode | single (one printer) or pool (printer pool, then also strategy) |
name | Name of the printer in Windows |
available | Whether the printer is ready |
state | State, such as ready, printing, paused, error, offline or unknown |
queue_length | Jobs in the queue |
issues | List of reported problems, empty if there are none |
media_remaining, media_capacity | Remaining media and capacity in sheets, empty if the printer does not report them |
total_prints | Prints over the printer's lifetime, DNP printers only |
An entry in buzzers:
| Field | Content |
|---|---|
name | Name of the wireless buzzer |
connection | connected, sleeping, disconnected, never (never connected) or unavailable (Bluetooth not available) |
battery_level | Battery in three levels: 3 full, 2 half, 1 low, 0 unknown |
millivolt | Battery voltage in millivolts |
seconds_since_press | Seconds 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:
- Read the content as it arrives, as bytes - do not parse it as JSON and write it out again first.
- Join the timestamp from
X-RAWCaptureBooth-Timestamp, a dot and the content. - Compute HMAC-SHA256 over this with the key, as a hex string, and put
sha256=in front. - Compare with
X-RAWCaptureBooth-Signature, in constant time. - 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://tohttps://or to an address withwww. 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".