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 URLhttps://hgt-scripts-api.timurnasriddinov56.workers.dev/api/v1
AuthAuthorization: Bearer <key>
FormatJSON, UTF-8
Machine spec/api/v1/openapi.json — OpenAPI 3.1, no key needed
TimezoneAll dates and hours are Asia/Tashkent (+05:00)
CurrencyUzbek 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
Keep the key server-side. Anyone holding it can read everything its scopes allow. Call this API from your backend and pass the result to your frontend. If you must call it from browser code, ask us to pin the key to your domain — requests from any other 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.

ScopeGrants
callsCall volumes, daily and hourly series, peak times, per-agent leaderboard
kpi:aggregateTeam KPI totals, distribution and anonymised ranks — no names, no individual pay
kpi:detailNamed per-operator KPI rows including individual payouts. This is salary data. Implies kpi:aggregate.
operatorsOperator roster and the off-day schedule
scriptsScript 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.

StatusMeaning
400Missing or malformed parameter
401Key missing, invalid, revoked or expired
403Key lacks the scope, or its Origin is not allowed
404No data for that path (e.g. no published KPI sheet for the month)
429Rate limit exceeded — wait for Retry-After
503The upstream call platform is unavailable

Ping

GET/pingno scope required

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

GET/calls/summaryscope 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

GET/calls/agentsscope calls

Same 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.

Agent counts include both directions. Summing 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

GET/kpi/monthsscope kpi:aggregate

Lists 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

GET/kpi/{month}scope 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 }
      ]
    }
  ]
}
FieldMeaning
kpi_percentOverall achievement as a ratio, not a percentage — 0.404 means 40.4%. Can exceed 1.
kpi_sumTotal KPI-earned money
fiksaFixed salary component
bonus / fineTotal bonuses / total penalties
payoutTotal payable for the month
metrics[].ratioPer-metric achievement, 1 = target met. null if the sheet has no ratio column for it.
metrics[].valueThe raw measured figure (calls made, premium sales, …)
rowSource 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

GET/operatorsscope operators

The 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

GET/schedulescope operators

Off-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

GET/scripts/statsscope scripts

Most-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

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"])