Skip to main content
Push subscriptions deliver the real-time event log to your endpoint, so you never poll. You register an HTTPS URL and optional filters, and FocusAlpha POSTs each matching row to it, in log order, signed with a secret only you hold. Push subscriptions configure delivery; they never return event or news content themselves. To read content, use Company News Events and Company News Articles. MCP tool: manage_push_subscriptions — see Connect via MCP.
Plan: Free and above · Credits: none — managing subscriptions is not a data read

Two doors, two products

There are two subscription endpoints, and they carry different things. They are the same two interfaces as the WebSocket stream.
The news door carries no model scoring, by design. impacts is removed from every news payload before delivery: this door reports what arrived, not what FocusAlpha thinks of it. If you want scored push, use the events door.
You do not choose a stream in the request body; the door you call decides it. Each door lists and changes only its own subscriptions.

Filters

A subscription can narrow what it receives:
  • symbols — only rows whose payload symbols include one of these tickers (case-insensitive).
  • company_ids — only rows whose payload company_id is one of these.
  • label_families — only rows carrying one of these label families, such as earnings, guidance or merger_acquisition. See Event labels.
Filters you set are combined with AND. A filter you leave out passes everything, so a subscription with no filters receives every row on its door.

Where delivery starts

By default a new subscription starts at the current end of the log: “from now on”. Pass from_seq to replay instead, and you receive every matching row with a seq greater than it.

The signing secret is shown once

The secret (prefixed whsec_) appears only in the response to create. It is never listed back. Store it immediately; if you lose it, deactivate the subscription and create a new one.

What a delivery looks like

Each delivery is a POST to your URL with a JSON body and these headers:
  • Content-Type: application/json
  • X-FA-Signature: t=<unix seconds>,v1=<hex> — the signature, described below.
  • X-FA-Subscription — the subscription id.
The body carries a batch of rows in log order:
stream is merged for the events door and raw for the news door. next_after_seq is your position in the log once you have processed this delivery. Each payload is the full message documented on the Events and Raw news pages. Respond with any 2xx status to acknowledge. Any other status, a redirect, a timeout or a connection error counts as a failed delivery.

Verify the signature

The signature is HMAC-SHA256, keyed with your secret, over the string <t>.<raw body>, where t is the timestamp in the header and the raw body is the exact bytes you received. To verify:
  1. Parse t and v1 from X-FA-Signature.
  2. Reject the delivery if t is more than 5 minutes from your current time. This stops a captured delivery from being replayed later.
  3. Compute the HMAC over <t>.<raw body> and compare it with v1 using a constant-time comparison.
Compute the HMAC over the raw body, before any JSON parsing. Re-serialising parsed JSON changes the bytes and the signature will not match.

Delivery semantics

  • At-least-once. The same row can arrive more than once. Dedupe events-door rows on (event_id, revision) and news-door rows on the payload’s id; both are stable across re-deliveries.
  • In order. A subscription’s position is one integer, cursor_seq. Delivery never skips ahead after a failure, so order per subscription is preserved and a retry resumes exactly where delivery stopped.
  • Filtered rows still move the cursor. cursor_seq tracks position in the log, not the last row delivered to you.
  • Freshness is about 2 minutes end to end, because the log is written in 2-minute cycles. Push removes polling; it does not make the underlying data arrive sooner.

Failures and suspension

After a failed delivery, the subscription waits before trying again: 1 minute, then 5 minutes, 30 minutes, 2 hours, and 6 hours for every later retry. A successful delivery resets the count. After 25 consecutive failures the subscription suspends itself. suspended_at and suspended_reason on a listed subscription say so, and consecutive_failures shows the count. Fix your endpoint, then call resume: delivery picks up from the stored cursor and nothing is skipped, so a subscription that was suspended for a long time delivers a burst.

Limits

Each door allows 10 active subscriptions per account. Creating an eleventh returns 403; deactivate one first. Suspended subscriptions are still active and count toward the limit. The URL must use https; plain http is rejected with 400.

POST /v1/events/subscriptions

Creates a subscription on the events door. POST /v1/news/subscriptions takes the same body and creates one on the news door.
string
required
The HTTPS endpoint that deliveries POST to. Up to 2,000 characters.
string[]
Only rows naming one of these tickers. Up to 500.
string[]
Only rows about one of these company_ids. Up to 500.
string[]
Only rows carrying one of these label families. Up to 50.
integer
Replay every matching row with a seq greater than this. Omit to start from now.
The response is the subscription, with its secret included this one time.

GET /v1/events/subscriptions

Lists your subscriptions on the events door, oldest first. GET /v1/news/subscriptions lists the news door. Each subscription has id, url, stream, symbols, company_ids, label_families, active, cursor_seq, consecutive_failures, suspended_at, suspended_reason and created_at. The secret is never included.

DELETE /v1/events/subscriptions/{id}

Deactivates a subscription on the events door; DELETE /v1/news/subscriptions/{id} does the same on the news door. Deactivation is soft: delivery stops, and the subscription’s cursor and history are kept. Returns { "id": "…", "active": false }. An id that is not one of your subscriptions on that door returns 404.
string
required
The subscription id from create or list.

POST /v1/events/subscriptions/{id}/resume

Lifts a failure suspension on the events door; POST /v1/news/subscriptions/{id}/resume does the same on the news door. It also clears the retry wait and the failure count, and marks the subscription active. Delivery resumes from the stored cursor. Returns { "id": "…", "active": true, "resumed": true }.
string
required
The subscription id from create or list.

The live stream instead of webhooks

If you would rather hold a WebSocket open than receive POSTs, GET /v1/events/stream-token returns a short-lived credential for the live stream, with a filter value per door and a paged catch_up URL for filling gaps after a disconnect. The WebSocket has no server-side filtering by symbol, company or label; webhook subscriptions do. See WebSocket overview for how to connect. See also Real-time events overview for what each live dataset covers, WebSocket overview, Events interface and Raw news interface for the payloads, and Event labels for the label families you can filter on.