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.json on your app's domain.
  • Generate a P-256 keypair and put the public key in the DID document as a verificationMethod whose 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:

bash
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.

ts
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):

ts
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:

bash
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 (see Retry-After).
  • InvalidRequest — malformed body (e.g. bad senderDid).