Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
319 changes: 120 additions & 199 deletions docs/mini-apps/technical-guides/base-notifications.mdx
Original file line number Diff line number Diff line change
@@ -1,278 +1,199 @@
---
title: "Notifications"
description: "Send notifications to your mini app users by wallet address using the Base.dev REST API."
description: "Send in-app notifications to your app's users through the Base Dashboard REST API."
---

Send notifications to users who have installed your app in the Base App using the Base.dev REST API. No webhooks, no database, no token management.

<Note>
Notifications are scoped to your app. Your API key only returns users who opted into your app and can only send to those users.
Notifications are delivered through the **Base App** only. Users who interact with your app on other platforms will not receive notifications through this API.
</Note>

## Quickstart

<Steps>
<Step title="Generate your API key">
Go to your project on [Base.dev](https://base.dev), open **Settings > API Key**, and generate a new key. Add it to your environment:

```bash .env
BASE_DEV_API_KEY=bdev_your_api_key_here
```

<Warning>
Never commit API keys to version control.
</Warning>
</Step>

<Step title="Query your opted-in users">
<RequestExample>
```bash cURL
curl "https://www.base.dev/v1/notifications/app/users?app_url=https://your-app.com&notification_enabled=true" \
-H "x-api-key: $BASE_DEV_API_KEY"
```
</RequestExample>

<ResponseExample>
```json Response
{
"success": true,
"users": [
{ "address": "0xc0c3132DFc4929bb6F68FA76B8fB379fF8c5bE74", "notificationsEnabled": true },
{ "address": "0x4e2bC8463190fBa0C5bF5921a98552f4728E3e9f", "notificationsEnabled": true }
]
}
```
</ResponseExample>
</Step>

<Step title="Send a notification">
<RequestExample>
```bash cURL
curl -X POST "https://www.base.dev/v1/notifications/send" \
-H "x-api-key: $BASE_DEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_url": "https://your-app.com",
"wallet_addresses": ["0xc0c3132DFc4929bb6F68FA76B8fB379fF8c5bE74"],
"title": "Hey from your app!",
"message": "You have a new reward waiting.",
"target_path": "/rewards"
}'
```
</RequestExample>

<ResponseExample>
```json Response
{
"success": true,
"results": [
{ "walletAddress": "0xc0c3132DFc4929bb6F68FA76B8fB379fF8c5bE74", "sent": true }
],
"sentCount": 1,
"failedCount": 0
}
```
</ResponseExample>
</Step>
</Steps>

## Send from code

```typescript app/api/notify/route.ts
import { NextResponse } from "next/server";

const API_KEY = process.env.BASE_DEV_API_KEY!;
const APP_URL = process.env.NEXT_PUBLIC_URL!;

export async function POST(req: Request) {
const { title, message, targetPath } = await req.json();

const usersRes = await fetch(
`https://www.base.dev/v1/notifications/app/users?app_url=${APP_URL}&notification_enabled=true`,
{ headers: { "x-api-key": API_KEY } }
);
const { users } = await usersRes.json();
const addresses = users.map((u: { address: string }) => u.address);

const sendRes = await fetch("https://www.base.dev/v1/notifications/send", {
method: "POST",
headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ app_url: APP_URL, wallet_addresses: addresses, title, message, target_path: targetPath }),
});

return NextResponse.json(await sendRes.json());
}
The Notifications API lets you send in-app notifications to users who have pinned your app and opted in to notifications. Two REST endpoints handle the full workflow: fetch your audience's wallet addresses, then send targeted or broadcast messages.

## Prerequisites

- A project on [Base Dashboard](https://dashboard.base.org) with your app URL registered
- An API key generated from **Settings > API Key** in your Base Dashboard project

## Quick start

Both endpoints require your API key in the `x-api-key` header.

<Info>
The notification endpoints share a rate limit of **10 requests per minute per IP**. Requests to either endpoint count toward the same limit. Exceeding it returns a `429 Too Many Requests` response.
</Info>

Fetch the wallet addresses of users who have opted in to notifications for your app:

```bash title="Get users with notifications enabled"
curl "https://dashboard.base.org/api/v1/notifications/app/users?app_url=<your-app-url>&notification_enabled=true" \
-H "x-api-key: <your-api-key>"
```

## Scheduled notifications

Use [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs) to send notifications on a recurring schedule.

```typescript app/api/cron/notify/route.ts
import { NextResponse } from "next/server";

const API_KEY = process.env.BASE_DEV_API_KEY!;
const APP_URL = process.env.NEXT_PUBLIC_URL!;

export async function GET(req: Request) {
if (req.headers.get("Authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const usersRes = await fetch(
`https://www.base.dev/v1/notifications/app/users?app_url=${APP_URL}&notification_enabled=true`,
{ headers: { "x-api-key": API_KEY } }
);
const { users } = await usersRes.json();
const addresses = users.map((u: { address: string }) => u.address);

const sendRes = await fetch("https://www.base.dev/v1/notifications/send", {
method: "POST",
headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
app_url: APP_URL,
wallet_addresses: addresses,
title: "Daily reminder",
message: "Check in today to keep your streak alive.",
target_path: "/streak",
}),
});

return NextResponse.json(await sendRes.json());
```json title="Response"
{
"success": true,
"users": [
{ "address": "0xA11ce00000000000000000000000000000000000", "notificationsEnabled": true },
{ "address": "0xB0B0000000000000000000000000000000000000", "notificationsEnabled": true }
]
}
```

Register the schedule in `vercel.json`:
Send a notification to one or more of those addresses. The `target_path` sets the route within your app that opens when the user taps the notification:

```bash title="Send a notification"
curl -X POST "https://dashboard.base.org/api/v1/notifications/send" \
-H "x-api-key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"app_url": "<your-app-url>",
"wallet_addresses": ["<wallet-address>"],
"title": "<title>",
"message": "<message>",
"target_path": "<target-path>"
}'
```

```json vercel.json
```json title="Response"
{
"crons": [{ "path": "/api/cron/notify", "schedule": "0 9 * * *" }]
"success": true,
"results": [
{ "walletAddress": "0xA11ce00000000000000000000000000000000000", "sent": true }
],
"sentCount": 1,
"failedCount": 0
}
```

## API reference

Both endpoints require your API key in the `x-api-key` header.

### GET /v1/notifications/app/users

```
GET https://www.base.dev/v1/notifications/app/users
Returns users who have pinned your app, with optional filtering by notification opt-in status. Results are paginated.

```http
GET https://dashboard.base.org/api/v1/notifications/app/users
```

**Query parameters**
#### Query parameters

<ParamField query="app_url" type="string" required>
Your mini app URL as registered on Base.dev.
Your app URL as registered on the Base Dashboard.
</ParamField>

<ParamField query="notification_enabled" type="boolean">
Set to `true` to return only users who have enabled notifications.
Set to `true` to return only users who have enabled notifications for your app.
</ParamField>

<ParamField query="cursor" type="string">
Pagination cursor returned from a previous response. Omit for the first page.
</ParamField>

<ParamField query="limit" type="integer">
Maximum users per page. Capped at 100.
</ParamField>

**Response**
#### Response

<ResponseField name="success" type="boolean">
Whether the request succeeded.
</ResponseField>

<ResponseField name="users" type="array">
List of user objects.

<Expandable title="User properties">
<ResponseField name="address" type="string">
The user's wallet address.
</ResponseField>
<ResponseField name="notificationsEnabled" type="boolean">
Whether the user has enabled notifications for your app.
</ResponseField>
</Expandable>
Users who have pinned your app.
</ResponseField>

<ResponseField name="users[].address" type="string">
The user's wallet address.
</ResponseField>

<ResponseField name="users[].notificationsEnabled" type="boolean">
Whether the user has enabled notifications for your app.
</ResponseField>

<ResponseField name="nextCursor" type="string">
Cursor for the next page. Absent when no more results exist.
</ResponseField>



---

### POST /v1/notifications/send

```
POST https://www.base.dev/v1/notifications/send
Sends an in-app notification to one or more wallet addresses.

```http
POST https://dashboard.base.org/api/v1/notifications/send
```

**Request body**
#### Request body

<ParamField body="app_url" type="string" required>
Your mini app URL as registered on Base.dev.
Your app URL as registered on the Base Dashboard.
</ParamField>

<ParamField body="wallet_addresses" type="string[]" required>
Wallet addresses to notify. Maximum 1,000 per request.
Wallet addresses to notify. Minimum 1, maximum 1,000 per request.
</ParamField>

<ParamField body="title" type="string" required>
Notification title.
Notification title. Maximum 30 characters.
</ParamField>

<ParamField body="message" type="string" required>
Notification body text.
Notification body text. Maximum 200 characters.
</ParamField>

<ParamField body="target_path" type="string">
Relative path to open when the user taps the notification (for example, `/leaderboard`). Omit to open your app at its root.
Path to open when the user taps the notification, such as `/rewards`. Must start with `/` if provided. Maximum 500 characters. Omit to open your app at its root URL.
</ParamField>

**Response**
#### Response

<ResponseField name="success" type="boolean">
Whether the request completed.
`true` only when every address in the request delivered successfully.
</ResponseField>

<ResponseField name="results" type="array">
Per-address delivery status.
</ResponseField>

<ResponseField name="results[].walletAddress" type="string">
The targeted wallet address.
</ResponseField>

<Expandable title="Result properties">
<ResponseField name="walletAddress" type="string">
The wallet address targeted.
</ResponseField>
<ResponseField name="sent" type="boolean">
Whether delivery succeeded for this address.
</ResponseField>
</Expandable>
<ResponseField name="results[].sent" type="boolean">
Whether delivery succeeded for this address.
</ResponseField>

<ResponseField name="results[].failureReason" type="string">
Present when `sent` is `false`. Possible values: `user has not saved this app`, `user has notifications disabled`.
</ResponseField>

<ResponseField name="sentCount" type="number">
Total notifications sent.
Total notifications delivered successfully.
</ResponseField>

<ResponseField name="failedCount" type="number">
Total notifications failed.
Total notifications that failed to deliver.
</ResponseField>

**Errors**

| Error | Cause |
|-------|-------|
| `InvalidArgument` | More than 1,000 wallet addresses in a single request. |

## Batching
## Errors

Each request accepts up to 1,000 addresses. For larger audiences, split into chunks.
Both endpoints return the following errors:

```typescript
const BATCH_SIZE = 1000;
| Status | Code | Cause |
|--------|------|-------|
| 400 | `Bad Request` | Possible causes:<ul><li>`app_url` is missing</li><li>`title` is missing or exceeds 30 characters</li><li>`message` is missing or exceeds 200 characters</li><li>`wallet_addresses` is missing or exceeds 1,000 addresses</li><li>`target_path` exceeds 500 characters or does not start with `/`</li></ul> |
| 401 | `Unauthorized` | Missing or invalid API key. |
| 403 | `Forbidden` | The `app_url` does not belong to your project, or your project is not whitelisted for notifications. |
| 404 | `Not Found` | The project associated with your API key does not exist. |
| 503 | `Service Unavailable` | The notification service is temporarily unavailable. Retry the request. Send endpoint only. |

async function notifyAllUsers(allAddresses: string[], title: string, message: string, targetPath: string) {
for (let i = 0; i < allAddresses.length; i += BATCH_SIZE) {
const batch = allAddresses.slice(i, i + BATCH_SIZE);
await fetch("https://www.base.dev/v1/notifications/send", {
method: "POST",
headers: { "x-api-key": process.env.BASE_DEV_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({ app_url: "https://your-app.com", wallet_addresses: batch, title, message, target_path: targetPath }),
});
}
}
```
## Batching and deduplication

## Rate limits
Each request accepts up to 1,000 addresses. For larger audiences, split your address list across multiple requests.

| Constraint | Limit |
|------------|-------|
| Requests per minute (per API key) | 10 |
| Wallet addresses per request | 1,000 |
| Max users notifiable per minute | 10,000 |
Duplicate addresses within a single request are deduplicated automatically. Identical notifications — same app URL, wallet address, title, message, and target path — sent within a 24-hour window are also deduplicated and return a success response without sending a duplicate push.
Loading