Перейти к содержимому

NFTPARS API v1

Базовый URL https://nftpars.app/api/v1. Версия контракта 1.1.0 — машиночитаемая спецификация: public-v1.yaml (OpenAPI 3.1). Данные передаются в JSON; время — UTC (RFC 3339, Z); суммы — целые нанотон (1 TON = 109).

Аутентификация и ключи API

Создайте приложение и ключ API в кабинете. Передавайте его в Authorization: Bearer <key>. Ключ показывается один раз и хранится только как защищённый хеш. При ротации задайте переходный период для клиентов. При утечке немедленно отзовите ключ.

Владелец приложения должен иметь действующий Telegram Premium, подтверждённый NFTPARS. Сам по себе Telegram Login не подтверждает Premium. Если проверка устареет, запросы вернут 403 PREMIUM_REQUIRED или 403 PREMIUM_VERIFICATION_STALE; данные не удаляются, а доступ восстанавливается после повторного подтверждения.

curl -s https://nftpars.app/api/v1/gifts/SnoopDogg-534281 \
  -H "Authorization: Bearer $NFTPARS_KEY"

Ответы и ошибки

Все ответы имеют единый формат. Успешные запросы возвращают {"ok": true, "data": …, "meta": …}; ошибки возвращают {"ok": false, "error": {"code", "message", "details"}, "meta": …}. meta.request_id (также X-Request-Id) идентифицирует запрос для поддержки.

{
    "ok": true,
    "data": {
        "slug": "SnoopDogg-534281",
        "owner": {
            "status": "VERIFIED",
            "freshness": "FRESH",
            "…": "…"
        }
    },
    "meta": {
        "request_id": "req_01jc…",
        "generated_at": "2026-11-04T10:00:00Z",
        "api_version": "v1",
        "freshness": "FRESH",
        "billing": {
            "charged_nano": 1000000,
            "reserved_nano": 1000000
        }
    }
}

Python

import os, requests

API = "https://nftpars.app/api/v1"
headers = {"Authorization": f"Bearer {os.environ['NFTPARS_KEY']}"}

r = requests.get(f"{API}/gifts/SnoopDogg-534281", headers=headers, timeout=15)
body = r.json()
if not body["ok"]:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")

owner = body["data"]["owner"]
print(owner["status"], owner["freshness"])          # e.g. VERIFIED FRESH
if owner["status"] == "VERIFIED":
    print("current owner:", owner["telegram"]["id"])
elif owner["status"] == "HISTORICAL":
    print("last known (not current) owner:", owner["telegram"]["id"])
print("charged nanoton:", r.headers["X-NFTPARS-Charged-Nano"])

JavaScript

const API = "https://nftpars.app/api/v1";
const res = await fetch(`${API}/gifts/SnoopDogg-534281`, {
  headers: { Authorization: `Bearer ${process.env.NFTPARS_KEY}` },
});
const body = await res.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message}`);

const { owner } = body.data;
console.log(owner.status, owner.freshness, body.meta.request_id);

PHP

<?php
$ch = curl_init('https://nftpars.app/api/v1/gifts/SnoopDogg-534281');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer '.getenv('NFTPARS_KEY')],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 15,
]);
$body = json_decode(curl_exec($ch), true);
if (! $body['ok']) {
    throw new RuntimeException($body['error']['code'].': '.$body['error']['message']);
}
echo $body['data']['owner']['status'], PHP_EOL;

Лимиты запросов и квоты

Лимиты применяются к каждому приложению (ваш план указан на странице тарифов). Каждый ответ содержит X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset (время Unix). При 429 учитывайте Retry-After.

Оплата

  • Для пополнения отправьте TON со своим личным комментарием (кабинет → Оплата). Средства зачисляются только после проверки в блокчейне.
  • Каждый запрос сначала резервирует стоимость, затем списывает оплату только за выданные результаты, освобождая остаток. Ненайденные элементы, ошибки и незавершённые задачи не оплачиваются.
  • Отправить Idempotency-Key в POST и запросах обновления. Повтор с тем же ключом возвращает исходный ответ (Idempotent-Replayed: true) без повторного списания. X-NFTPARS-Charged-Nano показывает стоимость запроса.

Быстрый ответ и новое наблюдение

Быстрый (по умолчанию) сразу возвращает значение из индекса со статусом freshness FRESH, STALE или PENDING; ?max_age= задаёт допустимый возраст свежих данных. Свежие данные (?refresh=true) запрашивает у индексатора новое наблюдение и возвращает 202 с задачей; опрашивайте GET /jobs/{id}или подпишитесь на job.completed вебхуки / через jobs канал WebSocket.

import time, uuid, requests

# Fresh mode: ask for a new observation. Use an Idempotency-Key so a retry never charges twice.
r = requests.get(f"{API}/gifts/SnoopDogg-534281/owner", params={"refresh": "true"},
                 headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, timeout=15)
job = r.json()["data"]["job"]                       # HTTP 202

while job["status"] in ("QUEUED", "RUNNING"):
    time.sleep(2)
    job = requests.get(f"{API}/jobs/{job['id']}", headers=headers, timeout=15).json()["data"]

print(job["status"], job["billing"])                # only SUCCEEDED items are charged

Пакетные задачи

POST /gifts/batch с различными идентификаторами в пределах пакетного лимита вашего плана. mode: "cached" отвечает сразу; оплачиваются только найденные элементы; mode: "fresh" создаёт задачу; оплачиваются только успешные элементы.

const start = await fetch(`${API}/gifts/batch`, {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({ mode: "fresh", slugs: ["SnoopDogg-534281", "PlushPepe-1234"] }),
});
let { data: { job } } = await start.json();          // 202 Accepted
while (["QUEUED", "RUNNING"].includes(job.status)) {
  await new Promise((r) => setTimeout(r, 2000));
  job = (await (await fetch(`${API}/jobs/${job.id}`, { headers: { Authorization: `Bearer ${KEY}` } })).json()).data;
}

Статусы владельцев

  • VERIFIED — текущий владелец Telegram, подтверждённый в verified_at.
  • HISTORICAL — последний известный владелец Telegram; не текущий владелец.
  • WALLET_ONLY — подарок хранится в кошельке TON; личность в Telegram не подразумевается.
  • HIDDEN, UNKNOWN, CONFLICT — достоверная текущая личность не определена; при конфликте перечислены все наблюдаемые кандидаты.

Актуальность оценивается отдельно от статуса. У каждого владельца есть sources (происхождение данных) и observed_at.

Фильтры предложений

Профили фильтров ограничивают поток предложений приложения (вебхуки и offers канал WebSocket) и результаты поиска/рынка: страна, добровольно указанный гендер, коллекции, диапазон цен и номеров, статус владельца, минимальная актуальность и рыночный статус. Временные фильтры на 1, 2, 6, 24 часа или свой срок заменяют постоянный профиль и автоматически восстанавливают его по истечении срока. Страна и гендер доступны только при добровольном указании и согласии владельца; неизвестные значения исключаются, если не задано unknown.<field> = "include". Сначала проверьте покрытие.

# Two-hour temporary filter; the previous profile is restored automatically.
curl -s -X POST https://nftpars.app/api/v1/applications/$APP_ID/filters/temporary \
  -H "Authorization: Bearer $NFTPARS_KEY" -H "Content-Type: application/json" \
  -d '{"criteria": {"country": "RU", "gender": "female", "unknown": {"country": "exclude", "gender": "exclude"}},
       "duration_minutes": 120}'

# How many indexed owners actually have reliable (self-declared, consented) attributes?
curl -s "https://nftpars.app/api/v1/filters/coverage?country=RU&gender=female" -H "Authorization: Bearer $NFTPARS_KEY"

Вебхуки

События: gift.discovered, gift.owner_changed, market.listing_created, market.listing_updated (изменения цены), market.listing_removed, market.sale, collection.updated, job.completed, job.failed. Только HTTPS. Каждая доставка подписана — X-NFTPARS-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")> — с повторными попытками и экспоненциальной задержкой. После исчерпания попыток доставка отмечается как неудачная в журнале. Отклоняйте подписи старше 5 минут и устраняйте дубли по X-NFTPARS-Delivery.

Python

import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts = int(parts["t"])
    if abs(time.time() - ts) > tolerance:            # replay protection
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

# Then de-duplicate on the X-NFTPARS-Delivery header (stable across retries).

JavaScript

import crypto from "node:crypto";

export function verify(secret, rawBody, header, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const ts = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;   // replay protection
  const expected = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

PHP

<?php
function nftpars_verify(string $secret, string $rawBody, string $header, int $tolerance = 300): bool
{
    parse_str(str_replace(',', '&', $header), $parts);
    $ts = (int) ($parts['t'] ?? 0);
    if (abs(time() - $ts) > $tolerance) {
        return false; // replay protection
    }
    $expected = hash_hmac('sha256', $ts.'.'.$rawBody, $secret);

    return hash_equals($expected, (string) ($parts['v1'] ?? ''));
}

WebSocket

Протокол Pusher v7 по адресу wss://nftpars.app/app/<key>. Сначала настройте каналы (в кабинете или через POST /ws/subscriptions): gift:<slug>, collection:<slug>, market:<collection>, offers (с учётом фильтров) и jobs. Приватные каналы авторизуются через POST /ws/auth с вашего сервера с ключом API. Число соединений, размер сообщений и частота ограничены; клиенты не могут публиковать события.

import Pusher from "pusher-js";

// 1) GET https://nftpars.app/api/v1/ws/config for host/key, 2) add channels via POST https://nftpars.app/api/v1/ws/subscriptions
const pusher = new Pusher(APP_KEY, {
  wsHost: "nftpars.app", wsPort: 443, forceTLS: true, enabledTransports: ["ws", "wss"], cluster: "",
  channelAuthorization: {
    endpoint: "https://nftpars.app/api/v1/ws/auth",
    headers: { Authorization: `Bearer ${KEY}` },   // server-side only — never ship keys to browsers
  },
});
pusher.subscribe("private-market.PlushPepe").bind("nftpars.event", (event) => console.log(event.type, event.data));
pusher.subscribe(`private-application.${APP_ID}.offers`).bind("nftpars.event", console.log); // filtered offer stream

Справочник endpoints

GET /status

Публичный статус сервисов (без авторизации и списания)

Аутентификация не требуется.

GET /pricing

Действующие цены операций для этого приложения (без списания)

GET /gifts/{slug}
ПараметрГдеОписание
slug *path
refreshquerytrue — запросить новое наблюдение. Если кеш недостаточно свежий, возвращается задача с кодом 202.
max_agequeryВ секундах; более старые данные считаются STALE.
Idempotency-Keyheader
GET /gifts/{slug}/owner
ПараметрГдеОписание
slug *path
refreshquerytrue — запросить новое наблюдение. Если кеш недостаточно свежий, возвращается задача с кодом 202.
max_agequeryВ секундах; более старые данные считаются STALE.
Idempotency-Keyheader
GET /gifts/{slug}/history
ПараметрГдеОписание
slug *path
cursorquery
limitquery
POST /gifts/batch
ПараметрГдеОписание
Idempotency-Keyheader
GET /collections
ПараметрГдеОписание
cursorquery
limitquery
GET /collections/{collection_slug}
ПараметрГдеОписание
collection_slug *path
GET /search

Фильтры приложения (постоянный профиль и активное временное переопределение) применяются автоматически.

ПараметрГдеОписание
qquery
collectionquery
modelquery
backdropquery
symbolquery
owner_statusquery
cursorquery
limitquery
GET /owners/{telegram_id}/gifts
ПараметрГдеОписание
telegram_id *path
include_historicalquery
cursorquery
limitquery
GET /wallets/{address}/gifts
ПараметрГдеОписание
address *path
cursorquery
limitquery
GET /market/listings
ПараметрГдеОписание
collectionquery
gift_slugquery
marketplacequery
statusquery
price_min_nanoquery
price_max_nanoquery
sortquery
cursorquery
limitquery
GET /jobs/{job_id}
ПараметрГдеОписание
job_id *path
GET /applications/{id}/usage
ПараметрГдеОписание
id *path
fromquery
toquery
GET /applications/{id}/filters
ПараметрГдеОписание
id *path
PUT /applications/{id}/filters
ПараметрГдеОписание
id *path
POST /applications/{id}/filters/temporary

Временный фильтр, например { "country": "RU", "gender": "female", "duration_minutes": 120 }, либо (1.1) { "profile_id": "...", "duration_minutes": 60 }, либо { "criteria": FilterCriteria, ... }. Прежняя конфигурация восстанавливается после истечения срока.

ПараметрГдеОписание
id *path
DELETE /applications/{id}/filters/temporary
ПараметрГдеОписание
id *path
GET /applications/{id}/filter-profiles

(1.1) Сохранённые профили фильтров

ПараметрГдеОписание
id *path
POST /applications/{id}/filter-profiles

(1.1) Создать профиль фильтров

ПараметрГдеОписание
id *path
PATCH /applications/{id}/filter-profiles/{profile_id}
ПараметрГдеОписание
id *path
profile_id *path
DELETE /applications/{id}/filter-profiles/{profile_id}
ПараметрГдеОписание
id *path
profile_id *path
POST /applications/{id}/filters/activate

(1.1) Назначить постоянный профиль фильтров (profile_id null отключает фильтр)

ПараметрГдеОписание
id *path
GET /applications/{id}/filters/history

(1.1) История изменений фильтров: кто, что и когда

ПараметрГдеОписание
id *path
GET /jobs

(1.1) Последние задачи приложения

ПараметрГдеОписание
statusquery
limitquery
GET /filters/coverage
ПараметрГдеОписание
countryquery
genderquery
GET /webhooks
POST /webhooks
GET /webhooks/{id}
ПараметрГдеОписание
id *path
PATCH /webhooks/{id}
ПараметрГдеОписание
id *path
DELETE /webhooks/{id}
ПараметрГдеОписание
id *path
POST /webhooks/{id}/rotate-secret
ПараметрГдеОписание
id *path
POST /webhooks/{id}/test
ПараметрГдеОписание
id *path
GET /webhooks/{id}/deliveries
ПараметрГдеОписание
id *path
GET /ws/config

Параметры соединения WebSocket (Pusher v7, обслуживается Laravel Reverb)

Подключайтесь к wss://nftpars.app/app/<app_key>?protocol=7. Приватные каналы: private-gift.<slug>, private-collection.<slug>, private-market.<collection_slug>, private-application.<application_id>.jobs. Событие nftpars.event содержит тело доставки вебхука (events/v1/webhook-delivery.json).

POST /ws/auth

Авторизовать подписку на приватный канал (стандартная авторизация Pusher)

GET /stats/indexing

Коды ошибок

КодHTTPЗначение
UNAUTHENTICATED401Ключ API не передан.
INVALID_API_KEY401Ключ не существует.
API_KEY_REVOKED401Ключ отозван или срок действия истёк, например после ротации.
PREMIUM_REQUIRED403У владельца нет подтверждённого действующего Telegram Premium. Доступ возобновится после подтверждённого продления.
PREMIUM_VERIFICATION_STALE403Не удалось повторно подтвердить Premium в льготный период. Пройдите проверку в боте.
API_ACCESS_DISABLED403Администратор отключил доступ к API для этого аккаунта.
APPLICATION_SUSPENDED403Приложение отключено или приостановлено.
WS_CHANNEL_NOT_CONFIGURED403Сначала добавьте канал WebSocket в приложение.
NOT_FOUND / GIFT_NOT_FOUND404Неизвестный endpoint или подарок, которого нет в индексе. Оплата не списана.
VALIDATION_FAILED422Неверные входные данные; см. error.details.errors.
BATCH_TOO_LARGE422Различных идентификаторов больше, чем допускает план.
IDEMPOTENCY_KEY_REUSED422Тот же Idempotency-Key используется для другого запроса.
IDEMPOTENCY_IN_PROGRESS409Исходный запрос ещё выполняется; повторите попытку немного позже.
INSUFFICIENT_BALANCE402Пополните баланс и повторите запрос. Ответ 402 не закрепляется за ключом идемпотентности.
RATE_LIMITED429Превышен минутный лимит; учитывайте Retry-After.
QUOTA_EXCEEDED429Месячная квота запросов исчерпана.
PRICING_NOT_CONFIGURED503Цена этой операции ещё не опубликована.
UPSTREAM_UNAVAILABLE503Индексатор недоступен. Оплата не списана; повторите запрос с задержкой.
UPSTREAM_CONTRACT_VIOLATION502Данные индексатора не прошли проверку целостности, например неподтверждённый владелец указан как подтверждённый. Оплата не списана.