Most code that follows something on TON ends up as a loop: read the address, compare it with what
you saw last time, sleep, repeat. v2.Watch in the apiton API does that
loop for you. You name an address, and apiton stores an event for every change and pushes it to
your server, or keeps it for you to collect.
This is the walkthrough: a key, a watch, a handler that checks the signature, and what to do if you
have no server at all.
What you can watch
A watch is one address, and the server works out what kind it is:
- An NFT item (a Telegram username, a +888 number, a .ton domain, a gift or any other NFT) is watched for its sale and auction.
- Any other address is watched for its transactions, above an amount you choose.
| Event | Means |
|---|---|
listed |
put on sale |
price |
the sale price changed |
delisted |
the sale or auction ended with the same owner |
sold |
the sale or auction ended with a new owner |
auction_start |
an auction began |
bid |
a higher bid |
auction_ending |
under 2 hours left |
tx |
a wallet's transaction |
Step 1: get a key
Send /get_api to @apitonBot. The key looks like 123456789:aB3dE… and
goes in a header on every call. v2.Watch needs one: a watch has to belong to somebody.
Step 2: create a watch
curl -X POST https://app.apiton.org/v2.Watch/Create \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 123456789:aB3dE…' \
-d '{"address":"UQCdqXGvONLwOr3zCNX5FjapflorB6ZsOdcdfLrjsDLt3AF4",
"webhook_url":"https://example.com/apiton","min_amount":1}'
The answer has the watch and a webhook_secret starting with whsec_. It is shown once, so
store it now. It signs your deliveries, and it is not your API key.
For a wallet, min_amount is the smallest amount that raises an event, in GRAM, 1 by default. 0
means every transaction. The webhook_url must be HTTPS on port 443 or 8443 on a public host, and
redirects are not followed.
The watch starts from the state it finds: what happened before Create raises nothing.
Step 3: receive the webhook
Each event arrives as a POST of JSON with three headers: X-Apiton-Signature,
X-Apiton-Timestamp and X-Apiton-Event-Id. The signature is sha256= plus the hex HMAC-SHA256
of <timestamp>.<body>, keyed with your secret.
Check it over the raw body, as received. Parsing the JSON and serialising it again gives
different bytes.
import hashlib, hmac, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["APITON_WEBHOOK_SECRET"]
seen = set() # use your database
def verify(headers, body: bytes) -> bool:
ts = headers.get("X-Apiton-Timestamp", "0")
if abs(time.time() - int(ts)) > 300:
return False # a replayed request
want = hmac.new(SECRET.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + want, headers.get("X-Apiton-Signature", ""))
@app.post("/apiton")
def apiton():
body = request.get_data()
if not verify(request.headers, body):
abort(401)
event = request.get_json()
if event["id"] not in seen:
seen.add(event["id"])
queue_for_later(event) # your slow work goes here, not in this request
return "", 204
Two things in that handler are deliberate.
Delivery is at least once. If your 2xx gets lost, the same event comes again with the same id.
So remember the ids you have processed.
Answer first, work later. Only a 2xx within 5 seconds counts as delivered. A slow handler counts
as a failure even if its work finished.
To try it, call Test. It sends a test event right away and tells you what your endpoint answered:
curl -X POST https://app.apiton.org/v2.Watch/Test \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 123456789:aB3dE…' -d '{"id":"41"}'
When your endpoint is down
A failed delivery is retried after 1, 5 and 30 minutes, then every 30 minutes. After about 24 hours
of failures the webhook is paused and nothing is pushed until a Test gets through. Nothing is lost
in the meantime: events stay readable for 7 days, as the next section shows. List shows each
webhook's state and last error.
No server? Use a cursor
Leave out webhook_url. Every event is stored anyway, so you can collect them from a script or a
cron job:
curl -X POST https://app.apiton.org/v2.Watch/Poll \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 123456789:aB3dE…' \
-d '{"after_id":"0","limit":50}'
Send next_after_id as the next after_id. Ids only grow, so a stored cursor never skips an event.
Keep an eye on the cost: each Poll is one request of your plan, while webhook deliveries cost none.
A free key's 10,000 requests a month is one Poll every 4½ minutes and nothing else.
Limits
| Plan | Watches | Each address read every | Webhook pushes per hour | Price for 30 days |
|---|---|---|---|---|
| free | 3 | 3 minutes | 10 | 0 |
| builder | 30 | 30 seconds | 100 | 5 GRAM or 1,000 Stars |
| pro | 30 | 30 seconds | 100 | 30 GRAM or 6,000 Stars |
Events above the hourly cap are not lost: they stay in Poll. Builder and pro also raise the
general API limits; /upgrade in @apitonBot shows them.
Worth knowing
-
A status change arrives one read later.
listed,soldand the like are reported when two reads in a row agree, so one bad read cannot firedelistedandlisted. Prices and bids need only one. -
A busy wallet. One read returns up to 16 transactions. A wallet with more between two reads
gets the newest 16 and
truncated: true; read the rest withAddress/Transactions. -
Amounts are in GRAM, the coin of TON, formerly Toncoin. The API's
symbolfor it staysTON.
If you only want the alert in Telegram and no code, send the address to
@apitonBot and tap Watch. The full reference is at
docs.apiton.org.
Top comments (0)