FromIran API documentation
Build alerts into your own market monitor, risk tools, or internal workflows. All timestamps use UTC.
Download OpenAPI schemaQuick start
Activate a subscription, create a token in API tokens, then fetch the latest alerts.
curl 'https://fromiran-api.sh1n.org/api/v1/alerts?limit=50' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'User-Agent: fromiran-client/1.0' \
-H 'Accept: application/json'Base URL: https://fromiran-api.sh1n.org/api/v1
Authentication
Send Authorization: Bearer YOUR_TOKEN on every authenticated API request. Tokens are shown once, stored hashed, and revocable from your account. Keep them server-side.
Send an identifying User-Agent, such as fromiran-client/1.0. The API does not use Turnstile; Cloudflare may reject generic blocked client signatures.
read:alerts permits alert reads. Browser account, payment, webhook, and Telegram management use secure sessions.
The API hostname never requires Turnstile for bearer calls. Signup, sign-in, and recovery are available only on the website hostname and use Turnstile. Browser mutations require the session CSRF token in X-CSRF-Token and the website Origin.
Read alerts
/alertsAn active subscription is required. Query parameters:
| Parameter | Purpose |
|---|---|
limit | Page size from 1 to 200. Default 50. |
category | Single included category. Use exact spelling; cannot combine with include_categories. |
include_categories | One comma-separated include list. Omit for all entitled categories. |
exclude_categories | One comma-separated exclude list. Exclusions win. |
after | Numeric alert cursor returned as next_cursor. Returns alerts newer than this value. |
Without after, results are ordered by their original publication time, newest first. The initial next_cursor marks the current end of the matching feed, including any category filter. With after, results follow increasing cursor order. Keep the largest returned next_cursor for your next poll; do not derive it from publication dates.
The response contains alerts, next_cursor, and an independent stream_cursor for WebSocket recovery. Each alert includes id, category, text, city,province, occurred_at, and published_at. An incident time may be unknown. Historical records have historical: true, curated concise text and available location fields, an empty source_url, and the original publication time in published_at.
/alerts/{id}Read one alert by its stable ID. Supported categories: BM, Cruise, UAV, Explosion, Air Defense, Jet activity, Hormuz, Fire, Other.
Categories & include/exclude filters
Discover the complete catalog before building your filter controls. Both options routes return the same public response; no token is required. The catalog is cached for five minutes and contains no account data.
GET https://fromiran-api.sh1n.org/api/v1/alerts/options
GET https://fromiran-api.sh1n.org/api/v2/alerts/options| Response field | Meaning |
|---|---|
version | Catalog schema version 1, independent of API v1/v2. |
categories | All 12 category values. |
native_categories, v1_categories | Nine native Iran categories. |
v2_categories | All native and additional-feed categories accepted by v2. |
destination_categories | Native categories accepted by Webhook & Telegram settings. |
additional_feeds | Feed IDs, labels, available/default event types and enabled_by_default. Defaults, not your account selections. |
event_type_categories | Mappings for missile, uav, warning and all_clear. |
filters | Query names, comma-separated encoding, case sensitivity, exclusion precedence and defaults. |
Every supported category
| Exact API value | Meaning | Available in |
|---|---|---|
BM | Ballistic missile | v1, v2, destinations |
Cruise | Cruise missile | v1, v2, destinations |
UAV | Unmanned aerial vehicle | v1, v2, destinations |
Explosion | Explosion | v1, v2, destinations |
Air Defense | Air defence activity | v1, v2, destinations |
Jet activity | Jet activity | v1, v2, destinations |
Hormuz | Developments around the Strait of Hormuz | v1, v2, destinations |
Fire | Fire | v1, v2, destinations |
Other | Other incident | v1, v2, destinations |
Missile | Public-source missile alert | v2 |
Siren | Public-source siren | v2 |
Warning | Public-source warning or standalone all-clear | v2 |
Values are case-sensitive. Preserve spaces and spelling exactly. A category is not a geographic search: Hormuz does not match every alert mentioning Hormuz.
Include, exclude, or enable all
Both REST lists and both public alert WebSockets accept include_categories and exclude_categories. Use one comma-separated value per parameter. An omitted include list means every entitled category. Exclusions always win. The existing category parameter remains a single-value include alias; do not combine it with include_categories.
| Query | Selection |
|---|---|
include_categories=BM,UAV | BM or UAV |
exclude_categories=Fire,Other | Everything eligible except Fire and Other |
include_categories=BM,UAV&exclude_categories=UAV | BM only |
category=BM&exclude_categories=BM | Valid empty selection |
include_categories=Air%20Defense,Jet%20activity | URL-encoded spaces |
For “Enable all,” omit both lists. For “Select none,” exclude every category for that API version or pause the connection. There are no all, none or wildcard values. Filters only narrow what your account can read; they never enable feeds, grant access, spend another trial slot or change delivery settings.
curl --fail-with-body --get \
'https://fromiran-api.sh1n.org/api/v2/alerts' \
-H "Authorization: Bearer $FROMIRAN_API_TOKEN" \
-H 'User-Agent: fromiran-client/1.0' \
--data-urlencode 'include_categories=BM,UAV,Air Defense' \
--data-urlencode 'exclude_categories=UAV' \
--data-urlencode 'limit=100'HTTP 400 rejects unknown or repeated parameters, empty values, duplicate entries within a list, wrong-case/unknown categories, malformed encoding, or category combined with include_categories. No padding around commas is accepted. Maximum list length: 9 in v1, 12 in v2. Maximum raw query: 2,048 bytes. REST limit is 1–200, default 50; it is not supported on WebSockets.
Feeds and event types
Optional feed IDs are shin_x, uae, ksa, bahrain, and kuwait. All are off by default. Each defaults to types missile, uav, and warning; all_clear is separately opt-in.
| Event type | Categories |
|---|---|
missile | BM, Cruise, Missile |
uav | UAV |
warning | Siren, Warning |
all_clear | Warning |
include_categories=Warning cannot enable all-clears. Select the feed and its types in Settings first. A prior warning may change lifecycle to all_clear while retaining its original event type; update that record silently. Read /api/v1/me/additional-alerts with a website session for current preferences. See additional alerts for activation and revision rules.
Snapshots, edits and reconnects
Fetch a filtered snapshot, then connect to the same-version stream with its stream_cursor and identical filters. Keep those filters when reconnecting from the last processed cursor. If you change filters, clear the filtered view and fetch a new snapshot: an old cursor cannot recover unchanged incidents that were previously excluded.
const filters = new URLSearchParams({
include_categories: 'BM,UAV',
exclude_categories: 'UAV',
});
// Use the same filters for the REST snapshot and WS connection.
const restURL = 'https://fromiran-api.sh1n.org/api/v2/alerts?' + filters;
// Authenticate with a private bearer header, then parse a successful response.
const response = await fetch(restURL, { headers });
if (!response.ok) throw new Error('Feed HTTP ' + response.status);
const snapshot = await response.json();
const query = new URLSearchParams(filters);
query.set('after', String(snapshot.stream_cursor));
const wsURL = 'wss://fromiran-api.sh1n.org/api/v2/alerts/stream?' + query;
// See the complete WebSocket connection/recovery example below.Excluded creations advance with a content-free cursor frame. An edited incident that leaves your category selection sends alert.deleted with reason: category_filter, no alert body, and notify:false in v2. Remove that ID from this view; it does not mean the incident was globally deleted. Re-entering the selection produces a silent upsert. Deletions/removals may refer to IDs you never cached.
v2 change polling applies the same rule through deleted_ids. It scans up to limit changed entitled IDs before category filtering, so an empty alerts array may still contain removals. Apply the full page, then save next_cursor. Keep fetching until it equals stream_cursor; do not jump to the head over unread pages. Individual alert cursor values are creation sequences, not v2 change positions.
v1 next_cursor is a creation sequence and does not synchronize edits/deletions. Use WS or v2 for a maintained view. Snapshot lists are latest windows of at most 200 records, not backward history pagination. HTTP 409 reset_required, WS 4009, or preference changes require a new snapshot. See WebSocket recovery for authentication, limits and close codes.
Webhook & Telegram selections
Read filters do not change connected destinations. Their categories field is an explicit JSON include array of native values; there is no destination exclude_categories field. To exclude native categories, subtract them from destination_categories and save the resulting array. At least one native category is required; use enabled:false to pause a connection.
Destination additional_feeds is independent of native categories and uses the account’s selected event types. Quiet hours apply to deliveries, not direct API reads. The optional enable_additional_feeds save field can enable a subset of the submitted feeds atomically, preserving existing type choices without implicitly selecting all-clears. See destination API.
Native admin application
The admin proxy /v1/alerts uses selected v2 semantics and accepts these filters; /v1/alerts/options returns the catalog. Native /v1/stream remains a separate multiplexed protocol: filter its alert frames locally. These public feed filters do not alter report review queues or APNs subscriptions.
Incident links & media
Every alert has a shareable incident page. Reuse its existing alert.id: feed and webhook alert objects keep their current shape.
const id = encodeURIComponent(alert.id);
const permalink = 'https://fromiran.sh1n.org/incidents/' + id;
const response = await fetch(
'https://fromiran-api.sh1n.org/api/v1/incidents/' + id,
{ headers: {
Authorization: 'Bearer ' + process.env.FROMIRAN_API_TOKEN,
'User-Agent': 'fromiran-client/1.0',
} },
);
if (!response.ok) throw new Error('Incident HTTP ' + response.status);
const incident = await response.json(); // { alert, media, permalink }/incidents/{id}This API route accepts a read:alerts bearer token or the website's browser session and checks current entitlement. Optional regional incidents must also match your enabled feed and event type, including its activation time. Trials can open only their allocated incident sample while time-valid. A shared link does not expand these permissions.
The response contains alert, permalink, and a media array. Each attachment has id, kind (photo or video), bytes, and state (pending, ready, retry, or failed). Only ready files include url. Resolve that relative URL against the API origin you used, and authenticate the media request too. An incident without attachments returns an empty array.
/incidents/{id}/media/{mediaid}Media supports HTTP byte ranges, for example Range: bytes=0-1023, for video playback. A complete file returns 200, a range returns 206, and an invalid range returns 416. HEAD returns headers without a body. Photos are sanitized JPEG; videos are MP4 or WebM.
Every GET, HEAD, and range request checks authentication, current access, and incident deletion. Full incident and media responses use private browser/CDN no-store headers. Never put a token in a link or expose it in browser source. Missing or revoked authentication returns 401; missing scope or active access returns 403; an unavailable incident or attachment returns 404. A temporarily unavailable media file returns 503.
/incidents/{id}/previewThis public API route needs no login. It returns only id, a short category/location title, card_url, permalink, and published_at. Preserve the returned card URL, including its version query string. The preview contains no full report, original media URL, or private source information.
On the website, https://fromiran.sh1n.org/incidents/{id} provides the public preview and social metadata. Its /card.png image uses a dimmed attachment thumbnail, preferring a photo and otherwise a video frame; incidents without media use the default card. Full text and attachments load only after the subscription check. Exclusive Iran reports reach members 120 seconds before Shin's public X and Telegram posts; the protected incident does not become free afterward.
Additional alerts
The existing /api/v1/alerts feed and v1 WebSocket stay Iran-only. Additional public-source feeds are off by default. Enable them in Settings → Additional alerts, then use the explicit v2 endpoints below. The dashboard switches automatically.
| Feed | API ID |
|---|---|
| Shin X alerts | shin_x |
| UAE official alerts | uae |
| Saudi Arabia official alerts | ksa |
| Bahrain official alerts | bahrain |
| Kuwait official alerts | kuwait |
Select missiles (missile), UAVs (uav), or sirens and air-raid warnings (warning) separately for each feed. These selections are independent of the nine native Iran categories. New selections start from new evidence; earlier source history is not replayed.
Lifted warnings use all_clear and are off by default. Select “All-clear / danger passed” for the feed to receive them through the selected API, WebSocket, or connected Telegram and webhooks. Each connection must also include that feed. Existing warnings changing status to all clear update silently; unsent stale warnings are cancelled.
/api/v2/alerts?limit=50/api/v2/alerts/{id}/api/v2/alerts/stream?after=CHANGE_CURSORUse the same hosts, bearer token, and read:alerts scope as v1. Responses include alerts, next_cursor, stream_cursor, and preference_revision. In v2, both cursors belong to the same change journal. Never mix them with the v1 alert-sequence cursor. Fetch the v2 snapshot before opening its stream with stream_cursor.
Alerts include country, origin, event_type, lifecycle, and public source_refs. Origins distinguish FromIran, official announcements, and Shin X posts. Public-source evidence does not imply an on-ground informant. Unknown locations remain unknown. Regional categories also include Missile, Siren, and Warning (air-raid warning).
Keep one record per alert ID. Protocol-v2 data frames include notify: when it is false, update the existing record silently. For example, an official alert can later gain a Shin tweet link without another notification. A subscriber who selected only Shin X can first receive that incident when eligible Shin evidence arrives. Source overlap does not spend another trial slot or send the same incident twice to one destination.
Preferences changing sends reset_required and closes the stream with code 4009. Discard cached alerts, reload preferences and a fresh v2 snapshot, and reconnect. Save the last processed cursor only after applying its event. The v1 stream authentication and reconnect rules below also apply to v2.
Browser settings use GET/PATCH /api/v1/me/additional-alerts. A PATCH supplies the latest revision and a feeds array of {id, enabled, types}; omitted feeds stay unchanged. On a 409, reload and review the latest settings before saving again. These routes require a browser session, with CSRF and the website Origin for PATCH.
Each webhook or Telegram connection separately selects additional_feeds, a subset of account-enabled feeds, defaulting to []. Existing quiet hours apply to those alerts too. Disabled optional feeds do not consume trial slots; category and quiet-hour filtering retain their existing counting rules.
WebSocket push
/alerts/streamReceive new alerts, edits, and deletions over a persistent connection. The dashboard connects automatically. API clients connect to wss://fromiran-api.sh1n.org/api/v1/alerts/stream with an Authorization: Bearer YOUR_TOKEN header and the read:alerts scope. Use an identifying User-Agent. No Turnstile is required.
The API stream is for server clients: omit the Origin header and keep bearer tokens out of URLs, subprotocols, and browser code. The website stream uses the existing secure session cookie and requires the exact website Origin. Access matches the feed; a trial receives only its five allocated alerts within its 24-hour term. WebSocket delivery does not consume an extra trial alert or webhook slot.
Minimal Node.js connection
// Node.js: npm install ws. Set FROMIRAN_API_TOKEN privately.
import WebSocket from 'ws';
const token = process.env.FROMIRAN_API_TOKEN;
if (!token) throw new Error('Set FROMIRAN_API_TOKEN');
const headers = {
Authorization: 'Bearer ' + token,
'User-Agent': 'fromiran-client/1.0',
};
const response = await fetch(
'https://fromiran-api.sh1n.org/api/v1/alerts?limit=50',
{ headers, signal: AbortSignal.timeout(10000) },
);
if (!response.ok) throw new Error('Feed HTTP ' + response.status);
const snapshot = await response.json();
let cursor = snapshot.stream_cursor;
const socket = new WebSocket(
'wss://fromiran-api.sh1n.org/api/v1/alerts/stream?after=' + cursor,
{ headers, handshakeTimeout: 10000, perMessageDeflate: false },
);
socket.on('message', (raw) => {
const event = JSON.parse(raw.toString());
if (event.type === 'reset_required') {
// Discard cached alerts, fetch a fresh snapshot, then reconnect.
socket.close();
return;
}
if (event.type === 'ready' || event.cursor <= cursor) return;
// Process the event successfully BEFORE saving its cursor.
console.log(event.type, event.alert?.id ?? event.alert_id ?? '');
cursor = event.cursor;
});
socket.on('error', () => console.error('Stream connection failed'));
socket.on('close', (code) => {
// 4001/4003: restore access first. 4009: fetch a new snapshot.
// Other disconnects: reconnect with cursor, backoff, and jitter.
console.log('Stream closed', code);
});This example opens one connection. Add the recovery flow below for a long-running client.
Connect without missing an alert
- Fetch
GET /alerts?limit=50and save its alert snapshot. - Connect to
/alerts/stream?after=STREAM_CURSORusing that response’sstream_cursor. This differs from the RESTnext_cursor. - Process frames in order, then save each processed
cursor. Deduplicate retries by cursor and upsert alert records byid. - After a temporary disconnect, reconnect with the last processed stream cursor, using exponential backoff with jitter.
Without after, the connection starts at the current journal head. It does not send old alerts. Use the same include/exclude category parameters on the snapshot and stream, or omit them for every category you can access. Webhook and Telegram snooze settings do not pause this direct feed.
| Frame type | How to handle it |
|---|---|
ready | Protocol v: 1 is ready. Sent before replay; its cursor is the requested starting position. |
alert.created / alert.updated | Upsert the alert object. revision identifies its current version. Replay resolves current reviewed content rather than restoring an old version. |
alert.deleted | Remove alert_id from your local state. The frame also includes its revision. |
cursor | Save the cursor. No alert data is included. |
reset_required | Discard cached feed state, fetch a fresh REST snapshot, and reconnect using its stream cursor. |
Replay is at least once, limited to 1,000 journal entries per catch-up. The journal retains up to 30 days and 10,000 entries. A cursor outside that window, an oversized backlog, or an access-tier change requires a fresh snapshot. Do not use the reset frame’s cursor to skip that snapshot.
Connection health and limits
The server sends protocol pings every 20 seconds and requires a pong within 10 seconds; standard WebSocket clients reply automatically. Do not send application messages. Transient network failures and deployments can close a connection, so always reconnect with backoff. Slow clients have a five-second write deadline.
Limits: 12 simultaneous connections per account, 20 per IP, and 1,000 overall. Upgrade attempts are limited to 20 per IP and 30 per account per minute. HTTP 429 includes Retry-After.
Close 4001 means authentication expired or was revoked; 4003means alert access ended. Clear cached private data and restore access before retrying. Close 4009 requires a new REST snapshot. Credentials and access are rechecked before sending alerts and at most every five seconds while idle.
The OpenAPI document includes the upgrade endpoint, frame schemas, and an x-websocket contract. A successful handshake returns HTTP 101.
Webhook delivery
Add a public HTTPS endpoint in Webhooks. Keep the signing secret shown at creation. The receiver must validate the HMAC signature before processing the payload and deduplicate using the stable event ID.
Delivery is at least once. Return a successful HTTP status promptly after durable acceptance. Failed deliveries receive bounded retries and remain visible in delivery history. Your current subscription, enabled state, categories, and quiet hours are checked at delivery time.
Read X-FromIran-Timestamp (Unix seconds), X-FromIran-Event-ID, and X-FromIran-Signature. Compute HMAC-SHA256 with your signing secret over timestamp + "." + raw_body. Compare the lowercase hex result against the signature value after v1=, in constant time. Reject timestamps outside a short tolerance, such as five minutes. Never reconstruct JSON before verification. The OpenAPI contract includes runnable verification examples.
Administrator announcements use event: announcement.published and an announcement object with id, title, text and created_at. They use the same signature and stable event ID. Dispatch by event; an announcement has no alert object and never spends a trial alert. Selected announcements reach enabled verified connections independently of incident categories and quiet hours.
Endpoints cannot point to private or reserved networks. Redirects are not followed. Quiet-hour alerts are suppressed, without a catch-up burst afterward.
Webhooks & Telegram
Browser sessions manage /destinations with GET and POST, and /destinations/{id} with PATCH and DELETE. Each has kind, label, categories, timezone, quiet_start, quiet_end, and enabled.
Webhook creation also requires a public HTTPS url. Telegram connections require a one-time connection proof. POST to /connections/telegram with destination_id, then complete the returned link/code flow.
Quiet hours use local HH:MM in an IANA time zone, for example Europe/London. Overnight windows are supported. Set both times to null to allow delivery at any hour.
Subscriptions & payment
Browser sessions read /billing, create a hosted checkout via POST /billing/checkout, and read owned invoices at /billing/invoices. Checkout accepts period (monthly, quarterly, semiannual or yearly), extra_webhooks, and pay_currency.
| Period | Access term | Access total | Each extra webhook | Save vs monthly |
|---|---|---|---|---|
monthly | 1 month | $250 | $100 | — |
quarterly | 3 months | $700 | $280 | $50 |
semiannual | 6 months | $1,350 | $540 | $150 |
yearly | 12 months | $2,500 | $1,000 | $500 |
One upfront payment covers the selected term. Renewals extend existing paid access; they do not replace unused time. Automatic renewal is off by default, requires explicit consent and uses prepaid balance only. Each renewal uses the saved period and extras at the current standard price; it never debits an external wallet automatically.
Active subscribers can buy extra webhook slots immediately through POST /billing/webhooks/checkout with period, quantity, and pay_currency. These slots have their own expiry, and the base subscription must stay active. All terms include one webhook, Telegram and API access. Prices are paid upfront in USD; the billing response publishes the current catalog in plans. The legacy prices fields remain available, with additional quarterly and semiannual fields. Supported currencies appear in pay_currencies and their names and networks in payment_currency_options. Use the exact currency code and network shown at checkout.
For an optional subscription discount, preview POST /billing/quote with period, extra_webhooks, and discount_code. The server returns the subtotal, discount, and total without reserving the code. Send the same code at checkout; it is rechecked and reserved to one invoice. Extra webhooks keep their normal price. Eligible periods and expiry depend on the code. Public promotions apply once per account to an eligible base subscription; later payments use normal pricing. Resume a reserved checkout from Billing, or contact Support if creation failed. Invoices and receipts show the discount.
Trial codes activate separately at the payment step through POST /billing/trial with code. A single-use trial starts its 24-hour clock on redemption and includes the next five distinct live alerts, one webhook, and one Telegram connection. It creates no payment invoice. Both delivery routes share the same five-alert allowance. Trial API reads are limited to the allocated sample; historical alerts are excluded. Billing shows the deadline and remaining alerts. Paid access retains its normal limits.
The returned checkout_url is a hosted NOWPayments page. Open it in a new tab. The server checks pending transactions every 60 seconds and grants access only after verified completion. Partial or unconfirmed payments do not activate a subscription.
POST /billing/invoices/{id}/refresh requests a provider status check. GET /billing/invoices/{id}/receipt returns a printable owned invoice.
Errors & retries
Errors use {"error":{"code":"…","message":"…"}}. Retry read requests with exponential backoff after temporary errors.
| Status | Meaning |
|---|---|
400 | Invalid input |
401 | Missing, invalid, or expired authentication |
402 / 403 | Subscription, scope, role, or origin requirement not met |
404 | Resource not found or not owned by you |
409 | Conflicting or duplicate operation |
429 | Rate limit reached; wait before retrying |
500 / 503 | Temporary service failure |
Complete API contract
The machine-readable contract includes request schemas, response schemas, required authentication, session and CSRF rules, error responses, payment states, integration callbacks, and passkey ceremonies.
Open the full API schemaAccount endpoints include /auth/register, /auth/login, /auth/logout, /auth/recover, /me, /tokens, and passkey start/finish/list/delete routes. Telegram integration callbacks authenticate the provider and are not client API endpoints.