OOtoto API Docs
Guide

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:

Request Ototo sends you
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

FieldTypeDescription
eventIdstring (GUID)Unique per delivery, stable across retries. Use as an idempotency key. Also sent as the X-Ototo-Event-Id header.
updatedCalculationsarrayThe calculation types that were updated. Always at least one item.

Each entry in updatedCalculations

FieldTypeDescription
valueintNumeric flag, matching the viewOptions bitmask used by Calculate
labelstringHuman-readable name, matching the ViewOptions name exactly
ValueLabelCalculator
1RoadTaxBIV / TMC
2YearlyTaxVerkeersbelasting (VKB)
4BenefitInKindVoordeel alle aard (VAA)
8FiscalDeductionFiscale aftrekbaarheid
16RejectedExpensesVerworpen uitgaven
32CO2ContributionCO₂-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.

Treat eventId as an idempotency key

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:

  1. 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.
  2. Compute HMAC-SHA256(raw_body_bytes, shared_secret) and hex-encode it (lowercase).
  3. Compare against the value after sha256= using a constant-time comparison, to avoid timing attacks.
  4. Reject the request if the signatures don't match.
No signature header?

Signing isn't enabled for your subscription. Treat such requests as unauthenticated — contact Ototo if you need verified delivery.

Verification examples

Node.js
const 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 methodAlways POST
Content typeapplication/json; charset=utf-8
TriggerNotifications are manual — sent when an Ototo administrator triggers them, not on a schedule. Liveness pings are automatic, on a recurring interval.
TimeoutOtoto waits up to 30 seconds for your endpoint to respond
SuccessRespond with any 2xx status as soon as you've accepted the payload
RetriesUp 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.
LivenessYour endpoint is pinged periodically; repeated failures are flagged to Ototo staff so they can follow up with you
Ordering / dedupNot guaranteed. Treat each notification as "these calculation types may have changed — recalculate."
Payload growthNew calculation types may be added later. Ignore value/label entries you don't recognise rather than failing.

Recommendations for your endpoint

  • Acknowledge quickly. Return 2xx immediately 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