Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.belio.co.ke/llms.txt

Use this file to discover all available pages before exploring further.

HMAC Request Signing

Every callback we send to your endpoint is signed using HMAC-SHA256. This lets you verify that the request genuinely came from us and has not been tampered with.

Signature Headers

Each signed callback includes two additional HTTP headers:
HeaderDescription
X-TimestampUnix epoch seconds at the time the request was sent
X-SignatureHMAC-SHA256 signature in the format sha256=<base64-encoded-value>

How the Signature Is Computed

The signature is computed over the timestamp and the raw request body: HMAC-SHA256(api_client_secret, "{X-Timestamp}.{body}")\text{HMAC-SHA256}(\text{api\_client\_secret},\ \texttt{"\{X\text{-}Timestamp\}.\{body\}"}) where body is the exact JSON string of the request body (no reformatting or whitespace changes).

Verifying the Signature

Follow these steps in your callback handler to authenticate each inbound request:
1

Read the headers

Extract the X-Timestamp and X-Signature headers from the incoming request.
2

Check the timestamp

Reject the request if X-Timestamp is too old or too far in the future (we recommend a 5-minute tolerance window). This protects against replay attacks where an attacker resends a previously captured request.
3

Recompute the signature

Using your API client secret (UTF-8 encoded), compute:
HMAC-SHA256("{X-Timestamp}.{raw request body}")
Make sure you use the raw, unmodified request body bytes — do not parse and re-serialize the JSON.
4

Compare the signatures

Base64-encode your computed HMAC, then compare it against the value in X-Signature (strip the sha256= prefix first). If they match, the request is authentic. If they don’t match, reject it with 401 Unauthorized.
When you regenerate your API client secret, signed callbacks may continue to be generated with your previous secret for up to 5 minutes. During this propagation window, verify signatures against both the old and new secrets.

Code Examples

import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.util.Base64
import scala.util.Try

def verifyCallback(
    apiClientSecret: String,
    timestampHeader: String,
    signatureHeader: String,
    rawBody: String
): Boolean = {

  // Compute HMAC-SHA256 over an arbitrary payload
  def hmac(payload: String): String = {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(new SecretKeySpec(apiClientSecret.getBytes("UTF-8"), "HmacSHA256"))
    Base64.getEncoder.encodeToString(
      mac.doFinal(payload.getBytes("UTF-8"))
    )
  }

  Try {
    val timestamp = timestampHeader.toLong
    val now       = System.currentTimeMillis() / 1000L
    val isFresh   = math.abs(now - timestamp) <= 300  // reject requests older / newer than 5 min
    val expected  = hmac(s"$timestampHeader.$rawBody") // sign "{timestamp}.{body}"
    val received  = signatureHeader.stripPrefix("sha256=")
    isFresh && expected == received
  }.getOrElse(false)
}
Always use a timing-safe comparison (e.g. crypto.timingSafeEqual, hmac.compare_digest, hash_equals) when comparing signatures. Regular string equality is vulnerable to timing attacks.

IP Whitelisting

As an additional layer of defense, you can restrict your callback endpoint to only accept inbound requests from our IP address ranges. Any request arriving from an address outside this list can be rejected at the network or application level before your handler code even runs.

Our Callback IP Ranges

All outbound callback requests originate from the following IP addresses:
IP AddressRoleLocation
88.99.209.40PrimaryFalkenstein, Saxony, Germany
94.130.34.45FailoverFalkenstein, Saxony, Germany

Circuit Breaking for Unreachable Callback URLs

To protect both your infrastructure and ours from cascading failures, we apply a per-URL circuit breaker to all outbound callbacks. If your endpoint becomes temporarily unavailable, the circuit breaker automatically backs off and retries without flooding your server.

Circuit Breaker States

Each callback URL you register gets its own independent circuit breaker with three states:
StateBehaviour
Closed (normal)Callbacks are delivered as usual
Open (tripped)Callbacks to this URL are silently skipped — your endpoint is not contacted
Half-Open (testing)One test callback is attempted; success closes the circuit, failure re-opens it with increased backoff

Configuration

The circuit breaker behaviour is governed by the following parameters:
ParameterDefaultDescription
Max failures5Number of consecutive failures within a call timeout window before the circuit opens
Reset timeout1 minuteHow long the circuit stays open before moving to half-open and attempting a test request
Max reset timeout10 minutesUpper bound on the reset timeout regardless of how many times backoff has multiplied it
Backoff factor2.0Multiplier applied to the reset timeout on each successive failure — e.g. 1 min → 2 min → 4 min → …
Randomization factor0.2Adds ±20 % jitter to each reset timeout to prevent synchronized reconnect storms across multiple URLs
Call timeout5 secondsHow long we wait for your endpoint to respond before counting the attempt as a failure

Backoff Sequence

With the default settings, successive circuit opens produce the following reset timeouts (before jitter is applied):
Open #Reset timeout
1st1 minute
2nd2 minutes
3rd4 minutes
4th8 minutes
5th +10 minutes (capped)
The ±20 % jitter means the actual wait time varies slightly around each value above — for example, a 4-minute timeout becomes somewhere between 3 min 12 s and 4 min 48 s. This prevents multiple circuit breakers from all attempting recovery at exactly the same moment.

Recommendations

  • Return HTTP 200 quickly. Acknowledge the callback as soon as you receive it and process it asynchronously. This keeps your response time well under the 5-second call timeout.
  • Keep your endpoint healthy. A single URL accumulates failures independently — a slow endpoint will trip its own breaker without affecting other URLs you have registered.
  • Monitor for silent skips. While the circuit is open, callbacks are not delivered and are not retried after the circuit closes. Make sure your integration does not rely solely on callbacks for critical state updates.