Base URL https://nftpars.app/api/v1. Contract version 1.1.0 — the machine-readable spec is
public-v1.yaml (OpenAPI 3.1). JSON everywhere; timestamps are UTC (RFC 3339, Z); amounts are integer nanoton (1 TON = 109).
Authentication & API keys
Create an application and an API key in the dashboard. Send it as Authorization: Bearer <key>. Keys are shown once and stored only as a keyed hash; rotate them with a grace period so clients can switch without downtime, and revoke them instantly if leaked.
The application owner must hold an active Telegram Premium subscription verified by NFTPARS (Telegram Login alone does not prove Premium). If verification lapses, requests fail with 403 PREMIUM_REQUIRED or 403 PREMIUM_VERIFICATION_STALE; nothing is deleted and access returns after verified renewal.
Limits are per application (see your plan on the pricing page). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix time). On 429 honour Retry-After.
Billing
Top up by sending TON with your personal memo (dashboard → Billing). Deposits are credited only after on-chain verification.
Each call reserves its price, then settles only for delivered units and releases the rest. Not-found items, failures and unfinished jobs are never charged.
Send Idempotency-Key on POSTs and refreshes: a retry with the same key replays the original response (Idempotent-Replayed: true) and is not charged again. X-NFTPARS-Charged-Nano shows what a call cost.
Fast vs fresh
Fast (default) returns the indexed value immediately with freshness FRESH, STALE or PENDING; ?max_age= sets what counts as fresh. Fresh (?refresh=true) asks the engine for a new observation and returns 202 with a job; poll GET /jobs/{id}, or subscribe to job.completed webhooks / the jobs WebSocket channel.
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
Batch jobs
POST /gifts/batch with up to your plan's batch limit of distinct slugs. mode: "cached" answers immediately and charges only found items; mode: "fresh" creates a job and charges only items that succeeded.
VERIFIED — current Telegram owner, verified at verified_at.
HISTORICAL — the last known Telegram owner; not a current owner.
WALLET_ONLY — held by a TON wallet; no Telegram identity is implied.
HIDDEN, UNKNOWN, CONFLICT — no reliable current identity (conflicts list each candidate observation).
Freshness is separate from status. Each owner carries sources (provenance) and observed_at.
Offer filters
Filter profiles restrict your application's offer stream (webhooks and the offers WebSocket channel) and market/search results: country, self-declared gender, collections, price range, gift number range, owner status, minimum freshness and market status. Temporary filters (1 h, 2 h, 6 h, 24 h or a custom duration) override the persistent profile and restore it automatically on expiry. Country and gender exist only for owners who self-declared them with consent; values that are unknown are excluded unless you set unknown.<field> = "include". Check coverage first.
# 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"
Webhooks
Events: gift.discovered, gift.owner_changed, market.listing_created, market.listing_updated (price changes), market.listing_removed, market.sale, collection.updated, job.completed, job.failed. HTTPS endpoints only. Each delivery is signed — X-NFTPARS-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")> — and retried with exponential backoff; after the maximum attempts it is dead-lettered and visible in your delivery log. Reject signatures older than 5 minutes and de-duplicate on 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).
Pusher protocol v7 at wss://nftpars.app/app/<key>. Configure channels first (dashboard or POST /ws/subscriptions): gift:<slug>, collection:<slug>, market:<collection>, offers (filtered) and jobs. Private channels are authorized by POST /ws/auth with your API key, from your server. Connections, frame size and message rate are capped; clients cannot publish.
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
Endpoint reference
GET/statusPublic service status (no auth, not billed)
Public service status (no auth, not billed)
No authentication required.
GET/pricingActive operation prices for this application (not billed)
Active operation prices for this application (not billed)
GET/gifts/{slug}
Parameter
In
Description
slug*
path
refresh
query
true = request a fresh observation. Returns 202 with a job when the cached value is not fresh enough.
max_age
query
Seconds; data older than this is STALE.
Idempotency-Key
header
GET/gifts/{slug}/owner
Parameter
In
Description
slug*
path
refresh
query
true = request a fresh observation. Returns 202 with a job when the cached value is not fresh enough.
max_age
query
Seconds; data older than this is STALE.
Idempotency-Key
header
GET/gifts/{slug}/history
Parameter
In
Description
slug*
path
cursor
query
limit
query
POST/gifts/batch
Parameter
In
Description
Idempotency-Key
header
GET/collections
Parameter
In
Description
cursor
query
limit
query
GET/collections/{collection_slug}
Parameter
In
Description
collection_slug*
path
GET/search
Application filters (persistent + active temporary override) are applied automatically.
Parameter
In
Description
q
query
collection
query
model
query
backdrop
query
symbol
query
owner_status
query
cursor
query
limit
query
GET/owners/{telegram_id}/gifts
Parameter
In
Description
telegram_id*
path
include_historical
query
cursor
query
limit
query
GET/wallets/{address}/gifts
Parameter
In
Description
address*
path
cursor
query
limit
query
GET/market/listings
Parameter
In
Description
collection
query
gift_slug
query
marketplace
query
status
query
price_min_nano
query
price_max_nano
query
sort
query
cursor
query
limit
query
GET/jobs/{job_id}
Parameter
In
Description
job_id*
path
GET/applications/{id}/usage
Parameter
In
Description
id*
path
from
query
to
query
GET/applications/{id}/filters
Parameter
In
Description
id*
path
PUT/applications/{id}/filters
Parameter
In
Description
id*
path
POST/applications/{id}/filters/temporaryTemporary filter, e.g. { "country": "RU", "gender": "female", "duration_minutes": 120 } or (1.1) { "profile_id": "...", "duration_minutes": 60 } or { "criteria": FilterCriteria, ... }; previous configuration is restored on expiry
Temporary filter, e.g. { "country": "RU", "gender": "female", "duration_minutes": 120 } or (1.1) { "profile_id": "...", "duration_minutes": 60 } or { "criteria": FilterCriteria, ... }; previous configuration is restored on expiry
POST/applications/{id}/filters/activate(1.1) Make a profile the persistent filter (profile_id null = no filter)
(1.1) Make a profile the persistent filter (profile_id null = no filter)
Parameter
In
Description
id*
path
GET/applications/{id}/filters/history(1.1) Filter change history (who, what, when)
(1.1) Filter change history (who, what, when)
Parameter
In
Description
id*
path
GET/jobs(1.1) Recent jobs of this application
(1.1) Recent jobs of this application
Parameter
In
Description
status
query
limit
query
GET/filters/coverage
Parameter
In
Description
country
query
gender
query
GET/webhooks
POST/webhooks
GET/webhooks/{id}
Parameter
In
Description
id*
path
PATCH/webhooks/{id}
Parameter
In
Description
id*
path
DELETE/webhooks/{id}
Parameter
In
Description
id*
path
POST/webhooks/{id}/rotate-secret
Parameter
In
Description
id*
path
POST/webhooks/{id}/test
Parameter
In
Description
id*
path
GET/webhooks/{id}/deliveries
Parameter
In
Description
id*
path
GET/ws/configWebSocket connection parameters (Pusher protocol v7, served by Laravel Reverb)
WebSocket connection parameters (Pusher protocol v7, served by Laravel Reverb)
Connect to `wss://nftpars.app/app/<app_key>?protocol=7`. Subscribe to private channels:
`private-gift.<slug>`, `private-collection.<slug>`, `private-market.<collection_slug>`,
`private-application.<application_id>.jobs`. Each event is named `nftpars.event` and carries
the webhook-delivery body (events/v1/webhook-delivery.json).
POST/ws/authAuthorize a private channel subscription (standard Pusher private-channel auth)
Authorize a private channel subscription (standard Pusher private-channel auth)
GET/stats/indexing
Error codes
Code
HTTP
Meaning
UNAUTHENTICATED
401
No API key was sent.
INVALID_API_KEY
401
The key does not exist.
API_KEY_REVOKED
401
The key was revoked or has expired (e.g. after rotation).
PREMIUM_REQUIRED
403
The owner has no verified, active Telegram Premium. Access resumes after verified renewal.
PREMIUM_VERIFICATION_STALE
403
Premium could not be re-verified within the grace window. Re-verify in the bot.
API_ACCESS_DISABLED
403
An administrator disabled API access for the account.
APPLICATION_SUSPENDED
403
The application is disabled or suspended.
WS_CHANNEL_NOT_CONFIGURED
403
Add the WebSocket channel to the application first.
NOT_FOUND / GIFT_NOT_FOUND
404
Unknown endpoint or a gift that is not indexed. Not charged.
VALIDATION_FAILED
422
Invalid input; see error.details.errors.
BATCH_TOO_LARGE
422
More distinct slugs than the plan allows.
IDEMPOTENCY_KEY_REUSED
422
Same Idempotency-Key, different request.
IDEMPOTENCY_IN_PROGRESS
409
The original request is still running; retry shortly.
INSUFFICIENT_BALANCE
402
Top up and retry (402s are never pinned to an idempotency key).
RATE_LIMITED
429
Per-minute limit hit; honour Retry-After.
QUOTA_EXCEEDED
429
Monthly request quota used up.
PRICING_NOT_CONFIGURED
503
The operation has no published price yet.
UPSTREAM_UNAVAILABLE
503
Indexing engine unavailable. Not charged; retry with backoff.
UPSTREAM_CONTRACT_VIOLATION
502
Engine data failed integrity checks (e.g. an unverified owner claimed as verified). Not charged.