Developer docs
Any atproto app can ask users to receive notifications via notify.atmo.tools. Users approve in the dashboard; the relay delivers via Telegram.
Two endpoints, two auth mechanisms. requestPermission proves the user authorized this request (user OAuth); send proves the sender identity (your app's own DID key).
1. Get a DID for your app
Needed for send. The simplest option is did:web:
- Host
/.well-known/did.jsonon your app's domain. - Generate a P-256 keypair and put the public key in the DID document as a
verificationMethodwhose id ends in#atproto. - Reference: atproto DID spec ↗
2. Request permission (user OAuth)
The user signs into your app via atproto OAuth. Add just the requestPermission method to your app's OAuth scope — send uses your app's own key, not the user's session,
so it doesn't belong here:
atproto rpc?lxm=tools.atmo.notifs.requestPermission&aud=*
Then mint a service-auth JWT on the user's PDS via com.atproto.server.getServiceAuth and call:
curl -X POST https://notifs.atmo.tools/xrpc/tools.atmo.notifs.requestPermission \
-H "Authorization: Bearer $USER_JWT" \
-H "Content-Type: application/json" \
-d '{
"senderDid": "did:web:yourapp.example",
"title": "Bookhive",
"description": "New comments on your books"
}'Returns { id, status } (pending or alreadyGranted). The user approves in their dashboard or
via Telegram. title ≤ 50 chars, description ≤ 200 chars, optional iconUrl.
3. Send a notification (your app's key)
Once granted, sign with your app's own key (no user involved) and send. Field limits: title ≤ 100, body ≤ 500, optional uri and threadKey.
import { createServiceJwt } from '@atcute/xrpc-server/auth';
// Signed with YOUR app's key — this proves the sender identity.
const jwt = await createServiceJwt({
keypair: yourKeypair,
issuer: 'did:web:yourapp.example',
audience: 'did:web:notifs.atmo.tools',
lxm: 'tools.atmo.notifs.send'
});Easiest with @atcute/client (pass the JWT per call):
import { Client, simpleFetchHandler } from '@atcute/client';
const client = new Client({
handler: simpleFetchHandler({ service: 'https://notifs.atmo.tools' })
});
await client.post('tools.atmo.notifs.send', {
headers: { authorization: `Bearer ${jwt}` },
input: {
recipient: 'did:plc:recipient',
title: 'New reply',
body: 'alice replied to your post',
uri: 'https://yourapp.example/thread/123'
}
});…or any HTTP client:
curl -X POST https://notifs.atmo.tools/xrpc/tools.atmo.notifs.send \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"recipient": "did:plc:recipient",
"title": "New reply",
"body": "alice replied to your post",
"uri": "https://yourapp.example/thread/123"
}'4. Rate limits
- At most 1 outstanding pending request per (sender, recipient).
requestPermission: 50 / hour per recipient and 100 / hour per sender.send: 1 / second and 100 / day per (sender, recipient).
5. Error handling
Common XRPC errors:
AuthenticationRequired— missing/invalid JWT.NotAuthorized— no active grant for this recipient.RateLimitExceeded— slow down (seeRetry-After).InvalidRequest— malformed body (e.g. badsenderDid).