Getting Started
Custom HTTP
Learn when Custom HTTP is appropriate and how a developer can securely send website visits to Honeylog.
Use Custom HTTP only when none of the ready-made Honeylog integrations matches the website. This option requires a developer or platform specialist to build and maintain a private program, called a sender, that sends website visits to Honeylog in groups.
If you are not the developer, use the next section to confirm that Custom HTTP is the right choice. Then send this public guide to the person who will build the sender and arrange secure access to the private Honeylog installation page, where the website-specific values are shown.
#Decide whether Custom HTTP is the right option
Use Custom HTTP only when all of the following statements are true:
- No ready-made Honeylog integration fits the website infrastructure.
- A developer can add a private program to the systems that handle website requests. This program runs outside visitors' browsers.
- That program can send secure web requests, using HTTPS, to Honeylog.
- That program can briefly buffer several events and send them together.
- The developer can store a private secret outside public browser code.
If you do not write or deploy code that runs outside visitors' browsers, stop after this check. The remaining steps are for the developer, hosting provider, or platform team that operates the website.
Custom HTTP must never run in public browser JavaScript. Every visitor can read browser code, which would expose the private ingestion secret.
#Information required before development starts
Open the website in Honeylog, go to Settings > Installation, and select Custom HTTP. The private installation page provides these values:
| Value | Purpose |
|---|---|
| Ingestion endpoint | The HTTPS address that receives the events. Its path ends in /api/events. |
| Site domain | The website host registered in Honeylog, without https:// and without a path. |
| Ingestion secret | The private key used locally by the sender to calculate each signature. |
Give the ingestion secret to the developer through an approved secure channel. The developer must store it in protected server-side configuration, never in browser code, logs, a code repository such as GitHub, or error messages.
Before development starts, confirm that the developer has all three values, a private place to store the secret, and control of the system that will run the sender. That system's clock must be accurate because Honeylog accepts request timestamps only within a 5 minute window.
The developer must also provide a private server-side queue or buffer. Honeylog limits the number of ingestion requests it accepts, so the sender must collect visits and send them in groups instead of making one request for every visit.
#Step 1: Collect website visits into a batch
Each website visit becomes an event. A batch is a group of events sent in one request. The following JSON shows the required events list with two visits:
{
"events": [
{
"n": "pageview",
"d": "<site-domain>",
"u": "https://<site-domain>/catalog"
},
{
"n": "pageview",
"d": "<site-domain>",
"u": "https://<site-domain>/pricing"
}
]
}
The three fields have specific meanings:
nis the event name. Usepageviewfor a normal page visit.dis the site domain. It must match the site domain configured in Honeylog.uis the complete URL that the person or bot visited, includinghttps://.
Do not send each event as soon as it is created. Add it to a private server-side queue or memory buffer, then send the accumulated events together according to the batching policy chosen for the sender. Every event in one batch must belong to the same Honeylog site.
Do not continue until the sender can produce these three values for real website requests and collect several events in an events list.
#Add the available request information
The minimal event is valid, but additional request information allows Honeylog to classify traffic more accurately. The following example shows every supported category of data:
{
"events": [
{
"n": "pageview",
"d": "<site-domain>",
"u": "https://<site-domain>/catalog?utm_source=custom",
"timestamp": "2026-07-15T10:20:30Z",
"r": "https://search.example/search?q=catalog",
"ua": "Mozilla/5.0 CustomCollector/1.0",
"ip": "203.0.113.10",
"method": "GET",
"status_code": 200,
"bytes_sent": 12345,
"response_time": 42,
"headers": {
"accept-language": "en-US,en;q=0.9",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none"
},
"p": {
"collector": "custom-http",
"edge_region": "eu-south-1"
}
},
{
"n": "pageview",
"d": "<site-domain>",
"u": "https://<site-domain>/pricing",
"timestamp": "2026-07-15T10:20:32Z",
"ua": "Mozilla/5.0 CustomCollector/1.0",
"ip": "203.0.113.11",
"method": "GET",
"status_code": 200
}
]
}
Only include information that the sender can observe correctly. Do not invent missing values. The complete field reference appears at the end of this guide.
Result of Step 1: the sender can collect valid Honeylog events in a private buffer and create a batch containing an events list.
#Step 2: Create the exact JSON body and timestamp
Convert the complete batch into one JSON string before calculating the signature.
For example, a program may serialize the event structure into this one-line string:
{"events":[{"n":"pageview","d":"<site-domain>","u":"https://<site-domain>/catalog"},{"n":"pageview","d":"<site-domain>","u":"https://<site-domain>/pricing"}]}
Keep this exact string in memory. Use it both to calculate the signature and as the request body; do not create a second JSON version before sending.
Create the current Unix timestamp in seconds.
Honeylog accepts a timestamp only when it is within 5 minutes of the Honeylog server clock. The sender's system clock must therefore be synchronized.
Result of Step 2: the sender has the exact JSON body and the current Unix timestamp.
#Step 3: Calculate the request signature
Build the canonical string by joining the timestamp, one literal dot, and the exact JSON body, with no additional spaces or line breaks:
canonical_string = "<unix_timestamp>.<raw_json_body>"
signature = hmac_sha256_hex(ingestion_secret, canonical_string)
Calculate HMAC-SHA256 using the ingestion secret as the key and the canonical string as the message. Encode the result as lowercase hexadecimal text, using only 0-9 and a-f.
The ingestion secret itself is never sent in the HTTP request. Only the calculated signature is sent.
The JSON body sent in Step 4 must be byte-for-byte identical to the JSON body used here. A change in whitespace, key order, escaping, or line endings creates a different signature. If the body changes, calculate the signature again.
Result of Step 3: the sender has the exact JSON body, the current timestamp, and a lowercase hexadecimal HMAC-SHA256 signature.
#Step 4: Send the signed HTTP request
Send an HTTPS POST request to the endpoint shown on the private installation page.
POST https://<ingestion-host>/api/events
Attach these four headers:
Content-Type: application/json
X-Honeylog-Site: <site-domain>
X-Honeylog-Timestamp: <unix-timestamp>
X-Honeylog-Signature: <hex-hmac-sha256>
Each header has one responsibility:
| Header | Value and purpose |
|---|---|
Content-Type |
Always application/json; it tells Honeylog how to read the body. |
X-Honeylog-Site |
The Honeylog site domain. It must match d in every event. |
X-Honeylog-Timestamp |
The Unix timestamp created in Step 2. |
X-Honeylog-Signature |
The signature calculated in Step 3. |
Use the exact JSON string from Step 2 as the request body. Send one request for the complete batch, not one request for each event. The Custom HTTP endpoint does not require a separate API key.
Result of Step 4: Honeylog returns 202 Accepted, which means that it accepted the request for processing.
#Step 5: Start from a complete example
The examples below combine Steps 1 through 4. Each example sends two events that have already been collected. In production, replace those example events with events read from the sender's private buffer or queue. Never call the send operation once for every website visit.
#cURL verification example
Use this example to verify the connection from a Linux or macOS command line. cURL sends the HTTP request and OpenSSL calculates the signature; both programs must be installed. Replace the placeholder values with the values from the private Honeylog installation page.
export HONEYLOG_API_URL='https://<ingestion-host>/api/events'
export HONEYLOG_SITE_DOMAIN='<site-domain>'
export HONEYLOG_SITE_SCHEME='https'
export HONEYLOG_INGESTION_SECRET='<ingestion-secret>'
body="$(printf '{"events":[{"n":"pageview","d":"%s","u":"%s://%s/catalog"},{"n":"pageview","d":"%s","u":"%s://%s/pricing"}]}' \
"$HONEYLOG_SITE_DOMAIN" "$HONEYLOG_SITE_SCHEME" "$HONEYLOG_SITE_DOMAIN" \
"$HONEYLOG_SITE_DOMAIN" "$HONEYLOG_SITE_SCHEME" "$HONEYLOG_SITE_DOMAIN")"
timestamp="$(date +%s)"
signature="$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$HONEYLOG_INGESTION_SECRET" -hex | awk '{print $2}')"
curl -i -X POST "$HONEYLOG_API_URL" \
-H "Content-Type: application/json" \
-H "X-Honeylog-Site: $HONEYLOG_SITE_DOMAIN" \
-H "X-Honeylog-Timestamp: $timestamp" \
-H "X-Honeylog-Signature: $signature" \
--data-binary "$body"
The command stores the body once, signs that stored value, and sends the same value with --data-binary. This preserves the exact bytes used by the signature.
#Node.js implementation example
Node.js is a runtime that can execute JavaScript on a server. This example uses its built-in crypto and fetch APIs. Store the three Honeylog values as protected environment variables on the server before running the program.
import crypto from "node:crypto";
const apiUrl = process.env.HONEYLOG_API_URL;
const site = process.env.HONEYLOG_SITE_DOMAIN;
const siteScheme = process.env.HONEYLOG_SITE_SCHEME ?? "https";
const secret = process.env.HONEYLOG_INGESTION_SECRET;
if (!apiUrl || !site || !secret) {
throw new Error("Missing Honeylog environment variables");
}
const payload = {
events: [
{
n: "pageview",
d: site,
u: `${siteScheme}://${site}/catalog`,
timestamp: new Date().toISOString(),
method: "GET",
status_code: 200,
headers: {
"accept-language": "en-US,en;q=0.9",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none"
},
p: {
collector: "custom-http"
}
},
{
n: "pageview",
d: site,
u: `${siteScheme}://${site}/pricing`,
timestamp: new Date().toISOString(),
method: "GET",
status_code: 200
}
]
};
const body = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Honeylog-Site": site,
"X-Honeylog-Timestamp": timestamp,
"X-Honeylog-Signature": signature
},
body
});
if (response.status !== 202) {
throw new Error(`Honeylog ingestion failed: ${response.status} ${await response.text()}`);
}
The call to JSON.stringify runs once. The resulting body variable is used both for the signature and for the HTTP request.
#PHP implementation example
This example uses hash_hmac and the PHP cURL extension. Store the Honeylog values as protected environment variables before running it.
<?php
$apiUrl = getenv('HONEYLOG_API_URL');
$site = getenv('HONEYLOG_SITE_DOMAIN');
$siteScheme = getenv('HONEYLOG_SITE_SCHEME') ?: 'https';
$secret = getenv('HONEYLOG_INGESTION_SECRET');
if ($apiUrl === false || $site === false || $secret === false) {
throw new RuntimeException('Missing Honeylog environment variables');
}
$payload = [
'events' => [
[
'n' => 'pageview',
'd' => $site,
'u' => sprintf('%s://%s/catalog', $siteScheme, $site),
'method' => 'GET',
'status_code' => 200,
],
[
'n' => 'pageview',
'd' => $site,
'u' => sprintf('%s://%s/pricing', $siteScheme, $site),
'method' => 'GET',
'status_code' => 200,
],
],
];
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
$handle = curl_init($apiUrl);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Honeylog-Site: '.$site,
'X-Honeylog-Timestamp: '.$timestamp,
'X-Honeylog-Signature: '.$signature,
],
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
]);
$responseBody = curl_exec($handle);
if ($responseBody === false) {
$error = curl_error($handle);
curl_close($handle);
throw new RuntimeException($error);
}
$statusCode = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
if ($statusCode !== 202) {
throw new RuntimeException("Honeylog ingestion failed: {$statusCode} {$responseBody}");
}
The $body variable is passed unchanged to both hash_hmac and cURL.
#Python implementation example
This example uses only modules included in the Python standard library. Store the Honeylog values as protected environment variables before running it.
import hashlib
import hmac
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
api_url = os.environ["HONEYLOG_API_URL"]
site = os.environ["HONEYLOG_SITE_DOMAIN"]
site_scheme = os.getenv("HONEYLOG_SITE_SCHEME", "https")
secret = os.environ["HONEYLOG_INGESTION_SECRET"]
payload = {
"events": [
{
"n": "pageview",
"d": site,
"u": f"{site_scheme}://{site}/catalog",
"method": "GET",
"status_code": 200,
},
{
"n": "pageview",
"d": site,
"u": f"{site_scheme}://{site}/pricing",
"method": "GET",
"status_code": 200,
}
]
}
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
timestamp = str(int(time.time()))
canonical = timestamp.encode("utf-8") + b"." + body
signature = hmac.new(secret.encode("utf-8"), canonical, hashlib.sha256).hexdigest()
request = Request(
api_url,
data=body,
headers={
"Content-Type": "application/json",
"X-Honeylog-Site": site,
"X-Honeylog-Timestamp": timestamp,
"X-Honeylog-Signature": signature,
},
method="POST",
)
try:
with urlopen(request, timeout=10) as response:
status_code = response.status
response_body = response.read().decode("utf-8")
except HTTPError as error:
status_code = error.code
response_body = error.read().decode("utf-8")
if status_code != 202:
raise RuntimeError(f"Honeylog ingestion failed: {status_code} {response_body}")
The encoded body bytes are used unchanged for both the signature and the HTTP request.
Result of Step 5: one example has been adapted to the sender's environment and returns 202 Accepted when it sends a test batch from private infrastructure.
#Step 6: Process responses and retries
Handle the responses below, but do not assume that they are the only possible responses. A proxy or a temporary network failure can produce other standard HTTP errors.
| Status | Meaning | Required action |
|---|---|---|
202 Accepted |
Honeylog accepted the request for processing. Processing may continue in the background, so this does not confirm that every event has already been stored. | Treat the delivery as successful. |
400 Bad Request |
Honeylog could not process the request as sent. This can mean invalid JSON or a group in which only some events could be processed immediately. | Record the response body and correct the request. Do not repeatedly send the same request unchanged. |
401 Unauthorized |
Honeylog could not authenticate the request. A required header may be missing, or the site, timestamp, signature, secret, or signed body may be wrong. Reusing a signature also returns this response. | Check the configuration, then create a fresh timestamp and signature before trying again. |
403 Forbidden |
An event domain differs from X-Honeylog-Site. |
Correct d in every event before retrying. |
422 Unprocessable Entity |
When Honeylog validates the request immediately, the JSON is readable but one or more event fields are invalid. If processing happens in the background, the request can instead receive 202 before field validation finishes. |
Correct the fields described by the response before retrying. |
429 Too Many Requests |
The sender has reached an ingestion rate limit. | Wait before retrying. Use the Retry-After response header when present, and create a fresh timestamp and signature for the retry. |
5xx or network error |
The sender did not receive confirmation that Honeylog accepted the request. | Retry after a short, increasing delay, using a fresh timestamp and signature each time. |
Never send one HTTP request per event. Keep events in a private buffer or queue and choose a batching policy appropriate for the sender. Use a flush interval so low-traffic sites do not wait indefinitely.
A retry is a new request. Generate a fresh timestamp and signature for every retry. If the body changes, sign the new exact body. Never reuse the previous timestamp and signature headers.
Result of Step 6: the sender treats only 202 Accepted as a successful handoff, records useful failure details, and never retries with a stale signature.
#Event field reference
The sender may use a short field name or any listed alias. For example, n and name have the same meaning. Use one name for each value, not both.
| Field | Aliases | Required | Description |
|---|---|---|---|
n |
name |
Yes | Event name. Use pageview for a normal page visit. |
u |
url |
Yes | Complete visited URL, including https:// and the domain. |
d |
domain |
Recommended | Site domain. It must match X-Honeylog-Site when present. |
timestamp |
ts, time_iso8601 |
No | Time of the website event, preferably in UTC ISO 8601 format. This is different from the request-signing timestamp header. |
r |
referrer |
No | URL of the page that referred the visitor to the current page. |
ua |
user_agent |
No | Identifying text, called a User-Agent, reported by the visitor's browser or bot. |
ip |
remote_addr |
No | Client IP address observed by the sender. |
method |
http_method, request_method |
No | HTTP method used to request the website page, such as GET. |
status_code |
response_status, http_status |
No | HTTP status returned by the website, such as 200 or 404. |
bytes_sent |
response_size |
No | Size of the website response in bytes. |
response_time |
response_time_ms |
No | Time used by the website to respond, in milliseconds. |
headers |
request_headers |
No | JSON object containing useful website request headers. Use lowercase header names. |
p |
props, m, meta |
No | JSON object containing custom metadata defined by the sender. |
headers and custom metadata must be JSON objects, not JSON strings.