Webhooks
When Ototo updates its calculation rules — for example after a change in tax legislation — Ototo can notify your endpoint so you know your cached results may be stale and should be recalculated.
Getting registered
Webhook endpoints are configured by Ototo, not self-service. Send your Ototo contact the URL you want notifications posted to; they'll register it and, if you want signed deliveries, set up a shared secret with you at the same time.
What you receive
A single HTTP POST to the URL you registered:
POST https://your-endpoint.example.com/ototo-webhook
Content-Type: application/json; charset=utf-8
X-Ototo-Event-Id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Ototo-Signature-256: sha256=<lowercase-hex> (only when signing is enabled)
{
"eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"updatedCalculations": [
{ "value": 1, "label": "RoadTax" },
{ "value": 2, "label": "YearlyTax" },
{ "value": 4, "label": "BenefitInKind" },
{ "value": 8, "label": "FiscalDeduction" },
{ "value": 16, "label": "RejectedExpenses" },
{ "value": 32, "label": "CO2Contribution" }
]
}
Only the calculation types that were actually updated are included — you'll rarely see all six at once.
Payload
| Field | Type | Description |
|---|---|---|
eventId | string (GUID) | Unique per delivery, stable across retries. Use as an idempotency key. Also sent as the X-Ototo-Event-Id header. |
updatedCalculations | array | The calculation types that were updated. Always at least one item. |
Each entry in updatedCalculations
| Field | Type | Description |
|---|---|---|
value | int | Numeric flag, matching the viewOptions bitmask used by Calculate |
label | string | Human-readable name, matching the ViewOptions name exactly |
| Value | Label | Calculator |
|---|---|---|
| 1 | RoadTax | BIV / TMC |
| 2 | YearlyTax | Verkeersbelasting (VKB) |
| 4 | BenefitInKind | Voordeel alle aard (VAA) |
| 8 | FiscalDeduction | Fiscale aftrekbaarheid |
| 16 | RejectedExpenses | Verworpen uitgaven |
| 32 | CO2Contribution | CO₂-bijdrage |
Because value matches the ViewOptions flag, you can OR the received values together into a viewOptions bitmask and re-run only the affected calculations via Calculate.
Event ID and de-duplication
Every delivery carries a unique eventId (a GUID), present both in the JSON body and as the X-Ototo-Event-Id header. The ID stays the same across retries of the same notification — if a transient failure causes a retry, you may receive the same eventId more than once.
Record the IDs you've processed and ignore duplicates. Each partner receives its own ID per event.
Liveness pings
Separately, Ototo periodically sends a lightweight ping to your endpoint to confirm it's still reachable. A ping is the same kind of signed POST, with this body instead:
{ "event": "ping", "ping": true, "eventId": "b1e0…" }
Respond with any 2xx status. Detect a ping by the presence of "ping": true (or the absence of updatedCalculations) and skip recalculation for it. Pings carry an eventId and X-Ototo-Event-Id too.
Verifying the signature
When a shared secret is configured for your subscription, every request carries an HMAC signature so you can confirm it genuinely came from Ototo and wasn't tampered with in transit:
X-Ototo-Signature-256: sha256=<lowercase-hex>
The hex value is the HMAC-SHA256 of the raw request body bytes — the exact UTF-8 JSON you received, before any parsing or re-serialization — keyed with your shared secret. This is the same scheme GitHub uses for webhook delivery.
To verify:
- Read the raw request body as bytes — don't deserialize and re-serialize first, since that can change whitespace, key order, or escaping and break the signature.
- Compute
HMAC-SHA256(raw_body_bytes, shared_secret)and hex-encode it (lowercase). - Compare against the value after
sha256=using a constant-time comparison, to avoid timing attacks. - Reject the request if the signatures don't match.
Signing isn't enabled for your subscription. Treat such requests as unauthenticated — contact Ototo if you need verified delivery.
Verification examples
Node.jsconst crypto = require('crypto');
function isValid(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(signatureHeader ?? '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
C#
static bool IsValid(byte[] rawBody, string signatureHeader, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = "sha256=" + Convert.ToHexString(hmac.ComputeHash(rawBody)).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signatureHeader ?? string.Empty),
Encoding.UTF8.GetBytes(expected));
}
Python
import hashlib
import hmac
def is_valid(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
What to expect
| HTTP method | Always POST |
|---|---|
| Content type | application/json; charset=utf-8 |
| Trigger | Notifications are manual — sent when an Ototo administrator triggers them, not on a schedule. Liveness pings are automatic, on a recurring interval. |
| Timeout | Ototo waits up to 30 seconds for your endpoint to respond |
| Success | Respond with any 2xx status as soon as you've accepted the payload |
| Retries | Up to 3 attempts with exponential backoff on transient failures (network errors, timeouts, HTTP 5xx, 408, 429). A 4xx response is treated as permanent and not retried. |
| Liveness | Your endpoint is pinged periodically; repeated failures are flagged to Ototo staff so they can follow up with you |
| Ordering / dedup | Not guaranteed. Treat each notification as "these calculation types may have changed — recalculate." |
| Payload growth | New calculation types may be added later. Ignore value/label entries you don't recognise rather than failing. |
Recommendations for your endpoint
- Acknowledge quickly. Return
2xximmediately and do heavy work — recalculation, cache invalidation — asynchronously. The 30s timeout applies to your HTTP response. - Verify the signature before acting on the payload, whenever a secret is configured.
- Be tolerant of unknown fields and of receiving a subset of calculation types.
- Be idempotent. A retry may deliver the same notification more than once — key off
eventId.
See also
- Calculate — re-run calculations for the updated types
- API Reference