Simple, straightforward API for sending push notifications to your team.
Your API endpoint:
https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_IDReplace YOUR_TEAM_ID with your actual team ID. Sign up to get started.
Example request:
await fetch("https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_ID", {method: "POST",headers: {"Authorization": "Bearer YOUR_API_KEY","Content-Type": "application/json"},body: JSON.stringify({title: "New Message",body: "Hello from the API!"})});
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Get your API keys from the API Keys page after signing up.
title(string, max 200 chars) - Notification title. Required with body unless using AI to summarize raw data.body(string, max 500 chars) - Notification message. Required with title unless using AI to summarize raw data.groups(array of strings) - Group slugs to target. Paid plans only. Example: ["engineering", "sales"]priority(string) - Optional. Set to high for urgent alerts; omit for normal delivery.data(object, max 10KB) - Arbitrary JSON payload. Used as the source when ai.rewrite is true and title/body are omitted.ai(object) - Per-request AI controls, overriding your team default. Fields below.ai.rewrite(boolean) - Turn AI on/off for this request. With title/body and a prompt it restyles them; with only data it writes the notification from raw JSON.ai.rewrite_title(boolean, default true) - Set to false to keep your supplied title and let AI rewrite only the body. Requires a title/body request.ai.prompt(string, max 200 chars) - AI style instruction, e.g. "make it celebratory". Sending a prompt turns AI on for the request. Required for title/body restyling unless your team default prompt is set.// Minimal notification{"title": "New payment","body": "Alex paid $49.00 for the Pro plan"}// Target paid-plan groups by slug{"title": "Deploy failed","body": "Billing API deploy 842 failed during migration.","groups": ["engineering", "on-call"]}// Let AI write the push from raw JSON{"ai": {"rewrite": true,"prompt": "summarize as a calm incident alert"},"data": {"event": "deploy.failed","service": "billing-api","environment": "production","deploy_id": 842,"failed_step": "migration"}}// Restyle title/body and keep your supplied title{"title": "Stripe","body": "4900 usd from alex@acme.co","ai": {"rewrite": true,"rewrite_title": false,"prompt": "make the body polished, keep the amount"}}
ai.rewrite is the per-request on/off switch. Set it to true to use AI for this request, set it to false to skip AI even if your team default is enabled, or omit it to use the setting from the web UI.ai.prompt overrides the prompt saved in the web UI and, on its own, turns AI on for that request. To keep a prompt in the request but force AI off, set ai.rewrite to false — an explicit false always wins.groups, each group can have its own AI voice set in the dashboard. Prompt precedence is your request ai.prompt › the group's voice › your team voice; a group with no voice inherits the team's. The on/off switch ai.rewriteis team-wide — groups don't have a separate toggle.title/body, AI restyles the notification and needs either a request prompt or your saved UI prompt. With only data, AI writes the title/body from raw JSON and the prompt is optional style guidance.title and data, title/body restyling wins and the raw data is not summarized. Omit the title to summarize raw data instead.ai.rewrite_title: false only applies to title/body restyling. It preserves your supplied title and lets AI rewrite only the body.{"title": "New Task Assigned","body": "You have a new task waiting for you."}
// Restyle your own title/body with an AI prompt{"title": "Payment received","body": "4900 usd from alex@acme.co","ai": {"rewrite": true,"prompt": "make it celebratory"}}// Keep your title and rewrite only the body{"title": "Stripe","body": "4900 usd from alex@acme.co","ai": {"rewrite": true,"rewrite_title": false,"prompt": "make the body polished"}}// Send raw JSON and let AI write the title/body{"ai": {"rewrite": true,"prompt": "summarize for a founder checking their phone"},"data": {"type": "payment_intent.succeeded","amount": 4900,"currency": "usd","customer": "alex@acme.co"}}
Add Tiny Owl notifications to this app.Public docs: https://www.tinyowl.io/docsUse server-side code only. Store the API key in TINYOWL_API_KEY and never expose it to client-side code.Payload shape:type TinyOwlPayload = {title?: string;body?: string;groups?: string[];data?: Record<string, unknown>;ai?: {rewrite?: boolean;rewrite_title?: boolean;prompt?: string;};};Endpoint:POST https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_IDAuthorization: Bearer YOUR_API_KEYContent-Type: application/jsonHelper:async function sendTinyOwlNotification(payload: TinyOwlPayload) {const response = await fetch("https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_ID", {method: "POST",headers: {Authorization: "Bearer " + process.env.TINYOWL_API_KEY,"Content-Type": "application/json"},body: JSON.stringify(payload)});if (!response.ok) {throw new Error("Tiny Owl notification failed: " + response.status);}return response.json();}Examples:await sendTinyOwlNotification({title: "New payment",body: "Alex paid $49.00 for the Pro plan"});await sendTinyOwlNotification({title: "Deploy failed",body: "Billing API deploy 842 failed during migration.",groups: ["engineering", "on-call"],ai: {rewrite: true,prompt: "short, calm incident alert with next action"}});await sendTinyOwlNotification({ai: {rewrite: true,prompt: "summarize for a phone push"},data: {event: "checkout.completed",amount: 4900,customer: "alex@acme.co"}});Rules:- Use groups only when the app knows the Tiny Owl group slugs.- Use ai.rewrite true when the payload is raw JSON or machine data that needs tobe turned into readable phone-sized copy.- When the event already has a clear, consistent wording, send a plaintitle/body and skip AI entirely.- Use ai.rewrite_title false only when preserving a supplied title.- Keep title/body concise for mobile push notifications.Where to add notifications:Look through this app for the moments its owner would actually want to knowabout on their phone — new signups or payments, failed jobs or deploys, errorsand outages, support requests, and any milestone worth celebrating. Wire thoseup, and skip routine noise that would be ignored.
{"title": "On-call handoff","body": "Primary support moved to Jamie for the next 12 hours.","groups": ["support", "on-call"]}
Use the group slugs from your team groups. Groups are created and managed in the Groups page. Paid plans only.
{"title": "DB CPU 94%","body": "primary-db has been above 90% CPU for 12 minutes.","groups": ["engineering", "on-call"],"ai": {"rewrite": true,"prompt": "short, calm incident alert with the next action"}}
Request prompts override group voices; groups without a voice inherit your team AI voice.
// Included when targeted groups fan out into separate AI-voice deliveries{"id": "ntf_eng_123","status": "delivered","devices_reached": 7,"attempted_devices": 7,"title": "Deploy failed","body": "Billing API deploy 842 failed during migration.","encrypted": false,"ai_rewritten": true,"ai_allowance_exhausted": false,"ai_usage": {"used": 42,"limit": 25000,"resetsAt": "2026-08-01T00:00:00.000Z"},"groups": ["engineering", "on-call"],"deliveries": [{"id": "ntf_eng_123","groups": ["engineering"],"title": "Deploy failed","body": "Billing API deploy 842 failed during migration.","devices_reached": 5,"ai_rewritten": true},{"id": "ntf_oncall_456","groups": ["on-call"],"title": "Heads up: deploy failed","body": "Billing API deploy 842 needs attention. Check the migration step.","devices_reached": 2,"ai_rewritten": true}],"created_at": "2026-07-26T15:30:00.000Z"}
Successful responses return a 200 status with:
{"id": "ntf_8x7gYkL2m","status": "delivered","devices_reached": 4,"attempted_devices": 4,"title": "New payment","body": "Alex paid $49.00 for the Pro plan","encrypted": false,"ai_rewritten": false,"ai_allowance_exhausted": false,"ai_usage": null,"groups": null,"created_at": "2026-07-26T15:30:00.000Z"}
These endpoints require a production API key (not staging). Use these to retrieve team information programmatically.
GET https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_ID/members
Returns a list of team members with their email addresses, subgroups (group slugs), and date joined.
curl -X GET "https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_ID/members" \-H "Authorization: Bearer YOUR_PRODUCTION_API_KEY"
Example response:
[{"email": "user@example.com","subgroups": ["engineering", "backend"],"joined_at": "2024-01-16T10:30:00Z"},{"email": "another@example.com","subgroups": [],"joined_at": "2024-01-15T08:00:00Z"}]
GET https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_ID/groups
Returns a list of all subgroups (groups) in the team with their slugs, names, emojis, and member counts.
curl -X GET "https://www.tinyowl.io/api/v1/webhook/YOUR_TEAM_ID/groups" \-H "Authorization: Bearer YOUR_PRODUCTION_API_KEY"
Example response:
[{"slug": "engineering","name": "Engineering","emoji": "⚙️","member_count": 5},{"slug": "sales","name": "Sales","emoji": "💰","member_count": 3}]
Usage is counted per API call, not per device or team member. If you send one notification to a team of 10 people, that counts as 1 notification toward your monthly limit, even though 10 devices receive the push.
401 UnauthorizedInvalid or missing API key
400 Bad RequestInvalid request body or missing required fields
403 ForbiddenAPI key does not belong to this team, feature requires higher tier, or admin endpoints require production API key (not staging)
429 Too Many RequestsRate limit exceeded
Sign up to get started and access your API keys on the API Keys page.