Bulk and Historical Signal Processing: Backfilling with occurred_at
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