# Holler webhook API

Create an alert in the Holler iPhone app, share its invite with friends, then publish notifications through its private webhook URL.

**Keep the webhook URL private.** Anyone who has it can send an alert to every subscriber.

## Quick start

```
curl -X POST "https://holler-api.krill.workers.dev/v1/hooks/YOUR_PUBLISH_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: campground-upper-pines-2026-07-18" \
  -d '{
    "title": "Campground available",
    "message": "Upper Pines opened for July 18",
    "url": "https://www.recreation.gov/",
    "image": "https://example.com/upper-pines.jpg"
  }'
```

A valid event returns `202 Accepted` after it is stored and queued. Notification delivery continues in the background.

## Payload

| Field | Required | Description |
| --- | --- | --- |
| `title` | required | Sender name shown on the notification. 1-120 characters. |
| `message` | required | Notification body. 1-500 characters. |
| `url` | optional | HTTPS destination opened when someone taps the notification. Max 2,000 characters. |
| `image` | optional | Public HTTPS JPEG, PNG, or GIF used as the circular sender avatar. Max 5 MB; failures fall back to the default avatar. |

### Headers

| Header | Required | Description |
| --- | --- | --- |
| `Content-Type` | required | Must describe a JSON request body. |
| `Idempotency-Key` | recommended | Up to 128 characters. Safe retries with the same key and payload return the original event. |

## Responses

Every API error uses the same JSON shape.

| Status | Meaning |
| --- | --- |
| 202 Accepted | The event is stored and queued for subscribers. |
| 400 Bad request | Malformed JSON or invalid fields. |
| 413 Too large | The request body is larger than 4 KB. |
| 404 Not found | The webhook is unknown, rotated, deleted, or paused. |
| 409 Conflict | An idempotency key was reused with different content. |
| 429 Rate limited | The webhook burst or creator daily limit was reached. |
| 503 Capacity reached | The service-wide daily safety limit was reached. |

```json
{
  "error": {
    "code": "bad_request",
    "message": "Invalid notification",
    "fields": { "title": ["Required"] }
  }
}
```

## Free limits

Holler has one free product with fixed safety rails.

- 3 alerts per creator installation
- 50 subscribers per alert
- 250 events per creator per UTC day
- 30 webhook requests per minute

Holler's servers keep events and delivery diagnostics for about 24 hours for delivery processing. There is no server-side event history; each recipient's iPhone separately caches its own received notifications locally for 7 days.

## Security model

Sharing and publishing use separate capabilities.

- **Webhook URL** (private) — Only give this to the service publishing notifications. Rotate it immediately if it leaks.
- **Invite URL and join code** (shareable) — Send these to friends. They can join the alert, but they cannot publish or manage it.
- **Creator identity** (on device) — Holler has no accounts. The anonymous owner token and raw alert credentials stay in iOS SecureStore.
- **Notification images** (direct fetch) — Each recipient's iPhone downloads the image directly. The image host may observe recipient requests and IP addresses.

Holler is a lightweight, best-effort notification relay. It does not guarantee delivery or confirm that a person saw an alert.
