Перейти до вмісту

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Дані індексатора не пройшли перевірку цілісності, наприклад непідтвердженого власника вказано як підтвердженого. Оплату не списано.