Bulk and Historical Signal Processing: Backfilling with occurred_at

2026-08-25

You've got two years of call history for a customer and you want to load it into HuMetric in one pass. That's a backfill: every signal arrives today, but each one actually happened on a different date in the past. This post covers the one field you need to get right (occurred_at), how it drives decay on the read side, and how it keeps two different read endpoints in sync — with real curl examples you can paste and run.

The examples below use a real, working pack (packs/cagri-merkezi.yaml, shown in full at the end). Its entity_type and metric keys are Turkish (musteri, eskalasyon_riski) — those are your data's field names, not UI strings, so HuMetric never translates them. Copy the requests as they are.

1. occurred_at: the signal's own date

POST /v1/signals accepts this body:

{
  "entity_id": "musteri-demo-118",
  "entity_type": "musteri",
  "text": "...if this isn't escalated I'll post about it publicly...",
  "occurred_at": "2024-09-15T14:32:00Z"
}

Leave occurred_at out for live, real-time signals — that's the default path, and the system stamps the request's arrival time. But when you supply it, that timestamp is recorded as when the signal actually happened, not when it reached the API. Always set it for a backfill:

curl -X POST https://api.gethumetric.com/v1/signals \
  -H "Authorization: Bearer $HUMETRIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_id": "musteri-demo-118",
    "entity_type": "musteri",
    "text": "...if this isn'\''t escalated I'\''ll post about it publicly...",
    "occurred_at": "2024-09-15T14:32:00Z"
  }'

The response comes back immediately, processing continues in the background:

{ "signal_id": "sig_8fa2c1", "status": "queued" }

Poll GET /v1/signals/{signal_id} to see it move from queued to completed (or failed). When you're pushing a whole archive through the queue, the order you send signals in doesn't matter — each one is stamped with its own occurred_at, so send order becomes irrelevant inside the engine.

2. What decay is computed from

Every metric carries two numbers: the raw confidence that was recorded, and an effective_confidence computed at read time. The second one drops over time:

effective_confidence = confidence * exp(-λ * age_days)
λ = ln(2) / 365   # confidence's half-life is one year

age_days is today minus occurred_at — not the date the signal was uploaded. In the example above, occurred_at is 2024-09-15; if today is 2026-08-25, that's an age of ~710 days, and for eskalasyon_riski a raw confidence of 0.40 becomes an effective_confidence of ≈ 0.2605. This is recomputed on every read; the stored raw value never changes, which keeps it auditable.

3. Two endpoints, one number

You can read a metric two ways:

curl https://api.gethumetric.com/v1/entities/musteri-demo-118/metrics \
  -H "Authorization: Bearer $HUMETRIC_API_KEY"
{
  "entity_id": "musteri-demo-118",
  "metrics": [
    {
      "metric_key": "eskalasyon_riski",
      "value": 0.72,
      "confidence": 0.40,
      "effective_confidence": 0.2605,
      "source_count": 1,
      "last_updated": "2024-09-15T14:32:00Z"
    }
  ],
  "metric_count": 1
}
curl https://api.gethumetric.com/v1/entities/musteri-demo-118/metrics/eskalasyon_riski/history \
  -H "Authorization: Bearer $HUMETRIC_API_KEY"
{
  "entity_id": "musteri-demo-118",
  "metric_key": "eskalasyon_riski",
  "points": [
    {
      "recorded_at": "2024-09-15T14:32:00Z",
      "value": 0.72,
      "confidence": 0.40,
      "effective_confidence": 0.2605,
      "source_count": 1
    }
  ],
  "total": 1
}

Notice: last_updated (on the current-metrics side) and recorded_at (on the history side) are the same date, and effective_confidence is the same number on both. That consistency isn't incidental — last_updated tracks the occurred_at of the newest real observation that contributed to that metric, not the moment the signal was written to the database. Even in an out-of-order backfill, this value never moves backward: if an older-dated signal for the same metric gets processed later, last_updated still reflects the newest real observation.

4. An end-to-end backfill loop

If you have a transcript archive in no particular chronological order, a typical backfill script looks like this:

import httpx

BASE = "https://api.gethumetric.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

for call in call_history:  # any order is fine
    httpx.post(f"{BASE}/signals", headers=HEADERS, json={
        "entity_id": call["customer_id"],
        "entity_type": "musteri",
        "text": call["transcript"],
        "occurred_at": call["happened_at"].isoformat(),
    })

Every request returns 202 right away; signals are processed in the background. The last step, once the whole archive has been processed, is to read the current state with GET /v1/entities/{id}/metrics — you'll now see the exact same effective_confidence as the latest point from GET .../history.

Reference: the pack used above

The examples above use a call-center pack that defines seven metrics, including eskalasyon_riski. Here's the full pack (packs/cagri-merkezi.yaml) if you want to load it into your own tenant:

entity_type: musteri
label: "Call Center Customer"
version: 3
required_fields:
  - key: kanal
    type: str
    label: "Channel"
metrics:
  - key: memnuniyet
    label: "Satisfaction"
    type: float
    default_confidence: 0.5
    prompt: "The customer's overall satisfaction during the call: tone,
             intensity of complaints, expressions of thanks or praise.
             HIGH value = satisfied customer."
  - key: cozum_basarisi
    label: "First-Contact Resolution"
    type: float
    default_confidence: 0.5
    prompt: "Whether the request was actually resolved within this
             call/chat: transfer, promise of a callback, an issue left
             open. HIGH value = the issue closed on this contact."
  - key: eskalasyon_riski
    label: "Escalation Risk"
    type: float
    default_confidence: 0.4
    prompt: "The customer's tendency to escalate to a supervisor, demand
             a refund/cancellation, or threaten legal action or social
             media exposure. HIGH value = high risk (a bad outcome,
             inverted relative to the other metrics)."
  - key: niyet_netligi
    label: "Intent Clarity"
    type: float
    default_confidence: 0.5
    prompt: "How clearly the customer stated their request: one single
             clear ask, or several scattered/conflicting topics. HIGH
             value = clear intent."
  - key: tekrar_temas_egilimi
    label: "Repeat Contact Tendency"
    type: float
    default_confidence: 0.4
    prompt: "Likelihood of the customer calling/writing again about the
             same issue soon: an unfinished process, an 'I'll call again'
             remark, a vague promise. HIGH value = repeat contact is
             likely (a neutral-to-bad signal, low operational
             efficiency)."
  - key: yanit_hizi_algisi
    label: "Perceived Response Speed"
    type: float
    default_confidence: 0.4
    prompt: "The customer's satisfaction with how quickly they were
             answered/responded to: 'you picked up right away', 'I was
             kept waiting too long', complaints about wait time. This is
             the customer's PERCEPTION, not a measured duration. HIGH
             value = felt like a fast response."
  - key: saglik_aciliyeti
    label: "Health Urgency"
    type: float
    sensitive: true
    requires_consent_scope: saglik_verisi
    default_confidence: 0.4
    prompt: "How urgently the health condition the customer described
             needs to be prioritized. HIGH value = urgent. This metric is
             based on special-category personal data under GDPR/KVKK
             Art. 6; without consent it is never stored, even if
             produced."
kvkk:
  sensitive_metrics:
    - saglik_aciliyeti
A signal with occurred_at of 2024-09-15 reads back with confidence 0.26, not 1.00 — decay is computed from the signal's own date
Before, the history and metrics endpoints disagreed (0.26 vs 1.00); now both return the same number