Partner API
Read-only statistics from the DOMO SUPPORT call-centre portal, for displaying on your own dashboard.
Overview
Every endpoint is GET, returns JSON, and is versioned under
/api/v1. Nothing in this API can modify portal data.
| Base URL | https://hgt-scripts-api.timurnasriddinov56.workers.dev/api/v1 |
|---|---|
| Auth | Authorization: Bearer <key> |
| Format | JSON, UTF-8 |
| Machine spec | /api/v1/openapi.json — OpenAPI 3.1, no key needed |
| Timezone | All dates and hours are Asia/Tashkent (+05:00) |
| Currency | Uzbek som (UZS) |
Authentication
Send your key on every request. Two header forms are accepted:
Authorization: Bearer hgt_live_xxxxxxxxxxxxxxxx
X-API-Key: hgt_live_xxxxxxxxxxxxxxxx
Origin will then be rejected.
Keys are issued from the portal's admin panel and shown exactly once at creation. We cannot recover a lost key — we can only revoke it and issue a new one. Contact your DOMO SUPPORT administrator to get one, to change its scopes, or to revoke it if it leaks.
Scopes
Each key carries a set of scopes, chosen when it is created. A request to an
endpoint outside your scopes returns 403 naming the scope you
need. GET /ping tells you what your key has.
| Scope | Grants |
|---|---|
calls | Call volumes, daily and hourly series, peak times, per-agent leaderboard |
kpi:aggregate | Team KPI totals, distribution and anonymised ranks — no names, no individual pay |
kpi:detail | Named per-operator KPI rows including individual payouts. This is salary data. Implies kpi:aggregate. |
operators | Operator roster and the off-day schedule |
scripts | Script view counts |
Rate limits & caching
Each key has a per-minute quota (120 by default). Every response carries your current standing:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
Exceeding it returns 429 with Retry-After: 60. The
window is a clean clock minute, not a rolling one.
Responses are cached server-side and report X-Cache: HIT or
MISS. Call statistics are cached for 10 minutes,
portal data for 1 minute. Polling faster than that returns
identical bytes and just burns your quota — once every few minutes is plenty
for a dashboard.
Errors
Errors are JSON: {"error": "…"}, sometimes with required_scope.
| Status | Meaning |
|---|---|
400 | Missing or malformed parameter |
401 | Key missing, invalid, revoked or expired |
403 | Key lacks the scope, or its Origin is not allowed |
404 | No data for that path (e.g. no published KPI sheet for the month) |
429 | Rate limit exceeded — wait for Retry-After |
503 | The upstream call platform is unavailable |
Ping
Verifies a key and reports its scopes. Use it to check your integration.
{
"ok": true,
"key_name": "Partner dashboard",
"scopes": ["calls", "kpi:aggregate"],
"rate_limit_per_minute": 120,
"available_scopes": ["calls", "kpi:aggregate", "kpi:detail", "operators", "scripts"],
"server_time": "2026-08-19T06:40:17.246Z"
}
Call summary
calls
Parameters: start and end as YYYY-MM-DD
(end defaults to start), and optional
direction = inbound (default) or outbound.
GET /calls/summary?start=2026-08-01&end=2026-08-07
{
"period": { "start_date": "2026-08-01", "end_date": "2026-08-07" },
"direction": "inbound",
"clamped": false,
"timezone": "+05:00",
"total_calls": 4821,
"daily": [ { "date": "2026-08-01", "count": 712 }, … ],
"hourly": null,
"peak_day": { "date": "2026-08-04", "count": 894 },
"peak_hour": null,
"peak_hour_count": 0
}
hourly is a 24-bucket histogram, returned only when
start equals end; it is null
for multi-day ranges. Ranges longer than 31 days are trimmed to the most
recent 31 and clamped becomes true — check it before
charting, or the axis will not match what you asked for.
Agent leaderboard
callsSame start / end parameters.
{
"period": { "start_date": "2026-08-01", "end_date": "2026-08-07" },
"agents": [
{
"agent_number": "108",
"name": "Abror",
"calls": 600,
"share_percent": 12.45,
"total_duration_seconds": 52196.4,
"inbound_duration_seconds": 23746.82,
"outbound_duration_seconds": 28449.58,
"average_duration_seconds": 87,
"checklist_percent": 84.59,
"positive_sentiment_percent": 67
}
]
}
checklist_percent is the call-quality score out of 100
(Bayesian-smoothed, so operators with few calls are not over-rewarded).
positive_sentiment_percent is the share of that agent's scored
calls that came out positive. Both are null when the platform
has not scored anyone yet. average_duration_seconds is
total_duration_seconds / calls.
agents[].calls will not equal
total_calls from /calls/summary, which is
direction-filtered. Do not present the two as parts of one whole.
KPI months
kpi:aggregateLists published KPI sheets. Drafts are never exposed.
{
"sheets": [
{ "id": 12, "name": "JULY 2026", "month": "2026-07", "shift": "9-19",
"updated_at": "2026-07-23 04:30:02" },
{ "id": 7, "name": "June 1", "month": "2026-06", "shift": "1",
"updated_at": "2026-07-22 09:41:44" }
]
}
Some months are split across shifts and have more than one sheet. Pass
?shift= to pick one; otherwise the lowest-sorting shift is used.
KPI results
kpi:aggregate or kpi:detail
month is YYYY-MM. What you get back depends on your
scope — always read the granularity field rather than assuming.
With kpi:detail
{
"granularity": "detail",
"sheet": { "id": 12, "name": "JULY 2026", "month": "2026-07",
"shift": "9-19", "updated_at": "2026-07-23 04:30:02" },
"currency": "UZS",
"operator_count": 25,
"totals": { "kpi_sum": 19106542, "fiksa": 0, "bonus": 600000,
"fine": 0, "payout": 19706542 },
"sheet_totals": { "kpi_sum": 19106542, "fiksa": null, "bonus": 600000,
"fine": 0, "payout": 19706542 },
"operators": [
{
"agent_number": "101",
"name": "Ra'no",
"row": 8,
"kpi_percent": 0.4039453333333333,
"kpi_sum": 1211836,
"fiksa": 0,
"bonus": 0,
"fine": 0,
"payout": 1211836,
"metrics": [
{ "name": "1. QO'NG'IROQLAR (60 ta/kun)", "value": 1743,
"ratio": 1.1173076923076923, "kpi": 344872 },
{ "name": "2. PREMIUM (5 ta/kun)", "value": 90,
"ratio": 0.6923076923076923, "kpi": 473964 }
]
}
]
}
| Field | Meaning |
|---|---|
kpi_percent | Overall achievement as a ratio, not a percentage — 0.404 means 40.4%. Can exceed 1. |
kpi_sum | Total KPI-earned money |
fiksa | Fixed salary component |
bonus / fine | Total bonuses / total penalties |
payout | Total payable for the month |
metrics[].ratio | Per-metric achievement, 1 = target met. null if the sheet has no ratio column for it. |
metrics[].value | The raw measured figure (calls made, premium sales, …) |
row | Source spreadsheet row. Useful for support questions; not a stable identifier. |
totals vs sheet_totals
totals is computed from the operators array, so the
two always reconcile — use it. sheet_totals is whatever the
source spreadsheet's own totals row says, exposed for transparency; on a few
historical sheets its formula ranges are stale and it disagrees. Prefer
totals unless you specifically need the sheet's stated figure.
With only kpi:aggregate
Same request, anonymised response — no names, no agent numbers.
{
"granularity": "aggregate",
"sheet": { "id": 12, "name": "JULY 2026", "month": "2026-07", … },
"currency": "UZS",
"operator_count": 25,
"totals": { "kpi_sum": 19106542, "fiksa": 0, "bonus": 600000,
"fine": 0, "payout": 19706542 },
"distribution": {
"payout": { "min": 0, "p25": 421000, "median": 748803,
"p75": 1180000, "max": 1836670, "avg": 788261.68 },
"kpi_percent": { "min": 0, "p25": 0.12, "median": 0.24,
"p75": 0.39, "max": 0.61, "avg": 0.263 }
},
"ranks": [
{ "rank": 1, "kpi_percent": 0.6122233333333333, "payout": 1836670 },
{ "rank": 2, "kpi_percent": 0.5646566666666667, "payout": 1693970 }
]
}
Operators
operatorsThe roster. Usernames, phone numbers and credentials are never returned.
{
"operators": [
{ "agent_number": "101", "name": "Ra'no", "team": "Jamoa 1",
"shift": "1", "staff_role": null, "joined_at": "2026-07-14 10:58:20" }
]
}
staff_role is null for regular operators and a label
(e.g. "HR") for non-operator staff — exclude those from
operator-performance views.
Schedule
operatorsOff-days for one month. month is required, as YYYY-MM.
GET /schedule?month=2026-08
{
"month": "2026-08",
"off_days": [
{ "agent_number": "101", "name": "Ra'no", "date": "2026-08-03",
"kind": "dam", "source": "admin" }
]
}
kind: dam = regular day off, unpaid =
unpaid leave. source: admin (set by hand),
auto (generated rota) or swap (traded between
operators).
Script stats
scriptsMost-viewed call scripts. Optional limit, 1–100, default 20.
{
"scripts": [
{ "id": 1,
"title": { "uz": "Gaz hisoblagich muammolari",
"ru": "Проблемы с газовым счётчиком" },
"category": { "uz": "Kiruvchi qo'ng'iroqlar",
"ru": "Входящие звонки" },
"views": 2 }
]
}
Gotchas
kpi_percentis a ratio. Multiply by 100 to display. It can exceed1when someone beats target.agent_numbercan benull. A few people appear on the KPI sheet without a portal account. They are real, paid operators and are included so the numbers add up — key your UI onrowor the array index, not onagent_number.- Agent call counts cover both directions while
total_callsdoes not. See Agent leaderboard. - Long ranges are clamped to 31 days. Check the
clampedflag. - Money fields can be
nullwhen a sheet has no such column. Treatnullas "not tracked", not as zero. - Only published KPI sheets are visible. A month in progress may 404 until it is published.
- Names are Latin-script Uzbek with apostrophes (
Ra'no,Qo'ng'iroq). Make sure your pipeline is UTF-8 end to end.
Quick start
curl
curl -H "Authorization: Bearer $DOMO_KEY" \
"https://hgt-scripts-api.timurnasriddinov56.workers.dev/api/v1/ping"
Node
const BASE = "https://hgt-scripts-api.timurnasriddinov56.workers.dev/api/v1";
async function domo(path) {
const res = await fetch(BASE + path, {
headers: { Authorization: `Bearer ${process.env.DOMO_KEY}` },
});
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") || 60);
throw new Error(`Rate limited, retry in ${wait}s`);
}
if (!res.ok) throw new Error((await res.json()).error);
return res.json();
}
const kpi = await domo("/kpi/2026-07");
console.log(kpi.granularity, kpi.totals.payout);
Python
import os, requests
BASE = "https://hgt-scripts-api.timurnasriddinov56.workers.dev/api/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['DOMO_KEY']}"
r = S.get(f"{BASE}/calls/summary", params={"start": "2026-08-01", "end": "2026-08-07"})
r.raise_for_status()
data = r.json()
if data["clamped"]:
print("range was trimmed to 31 days")
print(data["total_calls"], data["peak_day"])