# Diff by Streamgine > Real-time corporate standing and compliance events: register a webhook once, receive events authenticated with a shared secret (`INSERT` / `UPDATE` / `DELETE`) when a company's legal or financial standing changes — suspension, reinstatement, forfeiture, agent lapses — changed fields only. **Tagline:** Stop building hard-to-maintain crawlers. Subscribe to the diff. **Product:** Diff by Streamgine · **Homepage:** https://diff.streamgine.com/ **Category:** Real-time corporate standing and compliance events **Company:** https://streamgine.com/ **Agent index (llms.txt standard):** https://diff.streamgine.com/llms.txt **Full documentation:** https://diff.streamgine.com/docs.md **Same file for agents (llms-full.txt):** https://diff.streamgine.com/llms-full.txt **Account / login:** https://diff.streamgine.com/account **API:** https://diff.streamgine.com/api **Python client:** `pip install streamgine` ([PyPI](https://pypi.org/project/streamgine/)) · source in repository `clients/python/` --- ## Why Diff Built for risk, underwriting, and compliance systems that need standing changes as they happen — and for agents that consume the same feed. 1. **Built for systems, not spreadsheets** — Every change ships as a structured event you can pipe into underwriting, risk, or compliance workflows — no scraping, no manual re-checks. 2. **Signal, not snapshot** — We don't sell a point-in-time export. We track the moment a company's status changes, so you find out when it matters — not when you happen to look. 3. **Built on distribution, not data creation** — The filings are public. What isn't is a normalized, real-time feed of what changed. That's the layer we built. | Traditional data APIs | Diff | | ------------------------------ | ----------------------------------- | | Poll or bulk download | Push webhooks | | Full records every call | Changed fields only on `UPDATE` | | Point-in-time standing reports | Live standing / compliance events | | Seat-based pricing | Pay for the signal your system uses | **One-liner:** Webhook-native corporate standing events — register once, receive signed diffs when status or tax standing moves. **Headline fields on changes:** `status` (entity standing), `suspension_date`, `standing_ftb`, `standing_sos`, agent standing — then identity (`entity_name`, `entity_number`, jurisdiction). --- ## What you get 1. **Push, not poll** — We wake your system with secured callbacks. No timer loops. 2. **Full-story search** — Query current companies anytime (`POST /api/search`). Webhooks deliver changes; search returns today’s snapshot. 3. **Minimal payloads** — `INSERT` (new company, full attributes), `UPDATE` (changed fields only), `DELETE` (removed). Fewer tokens on the wire and in context. 4. **Trusted delivery** — Every webhook is authenticated with a shared secret (`X-Diff-Signature: sha256=` over the raw body). 5. **Flexible matching** — Subscribe by `state`, city (`principal_city`), or both, and many other fields. Optional `actions` (e.g. San Francisco + `INSERT` only). 6. **~30 seconds to first event** — Register with `TEST` / `test` for signed test heartbeats before production data. 7. **Canonical schema** — Messy government filings normalized into one model your system can trust. --- ## Agent journey Diff is the **corporate standing signal** — not your CRM and not a people/phone directory. The end-to-end job for an outreach or research agent looks like this: ```text Search → Watch → Qualify → Act → Stay current ``` ### 1. Search Need companies that **already exist** (backfill, research, or seed a watchlist)? Call `POST /api/search` with your API key. - Returns **current** company snapshots (full story), not change events - Filter by city, name, status, and other fields - Use results to decide who to track, then register a webhook for live updates Search is pull. Watch is push. Most agents use both. ### 2. Watch Register **once** with a webhook URL and a `match` object: - State — e.g. `{ "state": "CA" }` - City — e.g. `{ "principal_city": "San Francisco" }` - Both — e.g. `{ "state": "CA", "principal_city": "San Francisco" }` - Optional `actions` — e.g. `["INSERT"]` for newly added companies only You do not call an “ingest company” API for discovery. We **push** events to your callback whenever a matching company is added, changed, or removed. Start with `TEST` / `test` heartbeats, then re-register with production criteria (same account, updated filters). ### 3. Qualify On `action: "INSERT"`, a **new company was added**. `after` contains the full attribute set (legal name, addresses, status, registered agent name, and related fields). Use that payload to decide whether to act: - Keep or drop by status, ZIP, entity type, or other fields in `after` - Persist `entity_id` for companies you care about — that is your watchlist key ### 4. Act Outreach is **your** product. Diff wakes you with a normalized legal-entity record; it does not replace enrichment: | Diff provides | Your agent (or another vendor) provides | | ------------------------------------------- | ------------------------------------------- | | Legal name, entity number, status | Founders / officers / decision-makers | | Principal and mailing addresses | Direct phone, email, LinkedIn | | Registered agent name (often a service co.) | True contact person for sales or recruiting | Typical flow: on a qualified `INSERT`, enrich contacts elsewhere, then run outreach in your own stack. Do not expect Secretary of State filings to include personal phone numbers. ### 5. Stay current Keep the **same** registration (or include `UPDATE` / `DELETE` in `actions`). Later events for that `entity_id` arrive as: - `UPDATE` — an existing company changed; only changed fields (new values). Merge into your store. - `DELETE` — the company was removed from the registry path we track. Close or pause the account in your CRM. You never poll for “what changed.” **INSERT = new company, UPDATE = changed, DELETE = removed.** Need a full refresh of one company later? Search again (or keep the last webhook `after` you stored). --- ## How it works ```text Provider filings → normalize + diff → durable event log → secured webhook → your agent ``` - **Ingest** stores durable events. - **Diff** compares against the last known company snapshot for that registry id. - **Match** finds subscribers whose `match` filters all equal the event (optional `actions` refine delivery). - **Dispatch** POSTs a minimal customer event; delivery is acknowledged only after matched webhooks return 2xx. You never poll us for changes. --- ## Live coverage today **California corporate registry events** — formations, status changes, agent updates, dissolutions, and related Secretary of State filings. Filter with a `match` object — `state`, city, or both: - `{ "state": "CA" }` — all California events - `{ "principal_city": "San Francisco" }` — one city - `{ "state": "CA", "principal_city": "San Francisco" }` — both - Optional `actions` — e.g. `["INSERT"]` for newly added companies only **Roadmap:** more states, officers & principals, regulatory filings, trademarks / intellectual property — same integration pattern. --- ## Customer webhook shape | Field | Description | | ----------- | --------------------------------------------------------------------------------------- | | `entity_id` | Public Secretary of State–style number (e.g. `B2930182`) — omitted on test heartbeats | | `action` | `INSERT` (new company) \| `UPDATE` (changed) \| `DELETE` (removed) | | `after` | On `INSERT`: full attribute set. On `UPDATE`: changed fields only. Omitted on `DELETE`. | No `event_id`, `topic`, or `timestamp` — subscription match already scopes your feed. ### INSERT ```json { "entity_id": "B2930182", "action": "INSERT", "after": { "entity_number": "B2930182", "entity_name": "PACIFIC AGENTIC MATRIX LLC", "entity_type": "LLC", "status": "Active", "principal_zip": "94105" } } ``` ### UPDATE (changed fields only) ```json { "entity_id": "B2930182", "action": "UPDATE", "after": { "status": "Active", "principal_zip": "94612" } } ``` ### DELETE ```json { "entity_id": "B2930182", "action": "DELETE" } ``` --- ## Quick start (Python) 1. Sign in at https://diff.streamgine.com/account and download `diff-streamgine-credentials.json` or `diff-streamgine-env.sh`. 2. Install the client, call `diff.configure()` once, then register a webhook. 3. Verify signatures on raw request bytes before parsing JSON. ```python import os from fastapi import FastAPI, Header, Request, Response from streamgine import diff CALLBACK_URL = os.environ.get("DIFF_CALLBACK_URL", "https://your-agent.example/webhooks/diff") app = FastAPI() @app.on_event("startup") def register_agent() -> None: # Once — pick ONE download from /account: # diff.configure("diff-streamgine-credentials.json") # OR: source diff-streamgine-env.sh then diff.configure() diff.configure("diff-streamgine-credentials.json") with diff.DiffClient() as client: # Test heartbeats first (after.test == "success") client.register_test(callback_url=CALLBACK_URL) # Newly added companies in San Francisco: # client.register( # match={"state": "CA", "principal_city": "San Francisco"}, # actions=["INSERT"], # callback_url=CALLBACK_URL, # ) @app.post("/webhooks/diff") async def on_diff_event( request: Request, response: Response, x_diff_signature: str | None = Header(default=None, alias="X-Diff-Signature"), ): raw = await request.body() try: event = diff.verify_webhook(raw, x_diff_signature) except diff.WebhookVerificationError: response.status_code = 401 return {"error": "invalid signature"} # Handle event.action: INSERT | UPDATE | DELETE return {"ok": True} ``` Register without the Python client: ```http POST https://diff.streamgine.com/api/subscriptions Authorization: Bearer de_live_… Content-Type: application/json { "match": { "state": "CA", "principal_city": "San Francisco" }, "actions": ["INSERT"], "callbackUrl": "https://your-agent.example/webhooks/diff" } ``` --- ## Entity search Webhooks push **changes**. Search returns **current** company snapshots (full story). `POST /api/search` with your API key. Send a `query` object plus optional `from`, `size` (max **100**), and `sort`. Bodies are snake_case company fields — same attribute names you see in webhook `after`, plus routing fields such as `id` and `state`. ```bash curl -s -X POST https://diff.streamgine.com/api/search \ -H "Authorization: Bearer $DIFF_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": { "match": { "principalCity": "San Francisco" } }, "from": 0, "size": 10 }' ``` Python: ```python from streamgine import diff diff.configure() client = diff.DiffClient() result = client.search( query={"match": {"principalCity": "San Francisco"}}, size=10, ) for company in result["entities"]: print(company.get("entity_name"), company.get("entity_number")) ``` Example response: ```json { "ok": true, "total": 42, "from": 0, "size": 10, "entities": [ { "id": "CA-B20260240102", "entity_name": "Example LLC", "entity_number": "B20260240102", "state": "CA", "filing_type": "Domestic", "status": "Active", "principal_city": "San Francisco" } ] } ``` Useful query shapes (pass inside `"query"`): | Goal | Example `query` | | ------------ | -------------------------------------------------------------------------------------------------------- | | City | `{ "match": { "principalCity": "San Francisco" } }` | | Exact status | `{ "term": { "status": "Active" } }` | | Name text | `{ "match": { "legalName": "Pacific" } }` | | Combine | `{ "bool": { "must": [ { "term": { "state": "CA" } }, { "match": { "principalCity": "Oakland" } } ] } }` | Field names in the query use the **internal camelCase** index names (`principalCity`, `legalName`, `status`, `state`, …). Response bodies use **snake_case** (`principal_city`, `entity_name`, …). --- ## Customer API (agents) Production host: `https://diff.streamgine.com`. API routes are under `/api`. Customer routes use `Authorization: Bearer de_live_…` (API key from [/account](https://diff.streamgine.com/account)). | Method | Path | Purpose | | ------ | ----------------------------- | ----------------------------------------------------------- | | `POST` | `/api/subscriptions` | Register webhook (`callbackUrl` + `match` and/or `actions`) | | `POST` | `/api/search` | Full-story search → `{ ok, total, from, size, entities }` | | `GET` | `/api/account` | Account profile (`customerId`, `email`, `plan`) | | `POST` | `/api/credentials/regenerate` | Rotate API key + signing secret (session or API key) | | `GET` | `/health` | Liveness | Webhooks are **inbound** to your `callbackUrl` — not a poll endpoint. Verify `X-Diff-Signature` over raw body bytes before parsing JSON. --- ## Pricing model Payloads are intentionally small so language-model context stays cheap. You pay **per token** for data your agent consumes — not per seat, and not for megabytes of unchanged fields. --- ## For AI coding agents When helping a developer integrate Diff by Streamgine: 1. Prefer this full documentation (`/docs.md`, also `/llms-full.txt`) over scraping HTML. 2. Frame the product as **Search → Watch → Qualify → Act → Stay current** (see [Agent journey](#agent-journey)): search for current companies; webhooks for live changes; enrichment and outreach stay in the agent. 3. Always verify webhooks with the shared-secret signature over **raw body bytes**. 4. Start with `register_test` / `TEST`+`test` heartbeats (`after: {"test":"success"}`). 5. Production matching uses `state` and/or city (`principal_city`) plus optional `actions` (`INSERT` = new company, `UPDATE` = changed, `DELETE` = removed). 6. Use `POST /api/search` (or `diff.DiffClient.search`) for full-story / backfill — not as a substitute for webhooks. 7. Treat `entity_id` in webhooks as the public Secretary of State number; store it as the watchlist key for later `UPDATE` / `DELETE`. 8. Account login and credentials: https://diff.streamgine.com/account 9. Customer routes: `/api/subscriptions`, `/api/search`, `/api/account` — see [Customer API](#customer-api-agents). --- ## FAQ ### What states does Diff cover today? California corporate registry events are live today. More states use the same webhook integration when they ship. ### How do webhooks work? Register once at `POST /api/subscriptions` with your `callbackUrl` and a `match` object (state and/or city). Diff pushes `INSERT`, `UPDATE`, and `DELETE` events to your URL. Verify `X-Diff-Signature` over the raw body bytes before parsing JSON. ### How is Diff priced? You pay for the standing signal your system consumes — minimal payloads, changed fields only on `UPDATE`. See [Pricing model](#pricing-model). ### How do I get credentials? Sign in at https://diff.streamgine.com/account. You receive an API key and signing secret. Test heartbeats are available within ~30 seconds. ### Where can I learn more? - [California corporate standing events](https://diff.streamgine.com/california) - [Risk monitoring use case](https://diff.streamgine.com/use-cases/risk-monitoring) - [AI agents use case](https://diff.streamgine.com/use-cases/ai-agents) --- ## Stop plumbing. Start building agents. Questions or integration help: use the login flow at https://diff.streamgine.com/account to get credentials, then hit test heartbeats within ~30 seconds.