diff --git a/README.md b/README.md index 981ad32..a756c8d 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,43 @@ const income = await sec.agentStatement("income_statement", { const insiders = await sec.insiders({ ticker: "AAPL", limit: 20 }) ``` +## Special Situations + +The paid Special Situations API follows durable corporate events across their +lifecycle and retains a separate immutable weekly archive for agent research. + +```ts +// Find current pending mergers. +const current = await sec.situations.list({ + types: "merger", + statuses: "pending", + limit: 25, +}) + +// Browse a frozen weekly edition, then retrieve its full published snapshot. +const issues = await sec.situations.issues({ limit: 12 }) +const issue = await sec.situations.issue(22) + +// Preserve source citations when handing a situation to another agent. +const brief = await sec.situations.export("sit_...") + +// Retrieve the paid, source-cited JSON underwriting bundle for agent workflows. +const pack = await sec.situations.underwrite("sit_...") + +// Create a durable watch. Broad filters start from creation time; exact +// situationIds watches include filing-level provenance in monitor.match events. +const watch = await sec.situations.watch({ + types: ["merger", "tender_offer"], + tickers: ["AAPL", "MSFT"], +}) +``` + +These authenticated calls use the Special Situations API meter classes and +your account's current plan limits. The public website database is a separate, +rate-limited discovery surface; use the SDK for complete archive and agent +workflows. Underwriting packs include only the public/source-cited fields +returned by the API, not TIKR or other internal-only enrichment data. + See the [API documentation](https://docs.secapi.ai) for endpoint coverage, parameters, response fields, and runnable tutorials. ## Pagination diff --git a/src/generated-contracts/v1-post-bodies.ts b/src/generated-contracts/v1-post-bodies.ts index 99ec293..5f1309a 100644 --- a/src/generated-contracts/v1-post-bodies.ts +++ b/src/generated-contracts/v1-post-bodies.ts @@ -269,8 +269,8 @@ export const v1CreateMonitorBodySchema = z.preprocess( name: z.string().trim().min(1), query: z.string().trim().min(1), filters: z.record(z.string(), z.unknown()).nullish(), - /** Only `keyword` is currently supported for monitor execution. */ - searchMode: z.enum(["keyword"]).nullish(), + /** Structured modes subscribe to the corresponding public intelligence plane. */ + searchMode: z.enum(["keyword", "situation", "filing_event", "footnote", "section_delta"]).nullish(), /** Legacy single-webhook path. Kept for backward compat with existing clients. */ webhookUrl: z.string().url().nullish(), /** New path: typed `{type, config}` destination. v1 ships single email destination per monitor. */ diff --git a/src/index.ts b/src/index.ts index 9c5a998..13731ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -167,6 +167,18 @@ export type RequestOptions = { telemetry?: false | TelemetryOptions } +export type SituationWatchInput = { + /** At least one selector is required by the server-facing convenience helper. */ + situationIds?: string[] + types?: string[] + subtypes?: string[] + statuses?: string[] + tickers?: string[] + sectors?: string[] + name?: string + delivery?: { type: "email"; config: { to: string } } +} + export type JsonValue = | null | boolean @@ -950,6 +962,23 @@ export class SecApiClient { creditRating: (...args: Parameters) => this.macroCreditRating(...args), } + readonly situations = { + list: (...args: Parameters) => this.listSituations(...args), + issues: (...args: Parameters) => this.situationsIssues(...args), + issue: (...args: Parameters) => this.situationIssue(...args), + get: (...args: Parameters) => this.getSituation(...args), + filings: (...args: Parameters) => this.situationFilings(...args), + summary: (...args: Parameters) => this.situationSummary(...args), + underwrite: (...args: Parameters) => this.underwriteSituation(...args), + watch: (...args: Parameters) => this.watchSituations(...args), + export: (...args: Parameters) => this.exportSituation(...args), + feed: (...args: Parameters) => this.situationsFeed(...args), + calendar: (...args: Parameters) => this.situationsCalendar(...args), + stats: (...args: Parameters) => this.situationsStats(...args), + performance: (...args: Parameters) => this.situationsPerformance(...args), + byForm: (...args: Parameters) => this.situationsByForm(...args), + } + readonly portfolio = { optimize: (...args: Parameters) => this.portfolioOptimize(...args), hedge: (...args: Parameters) => this.portfolioHedge(...args), @@ -1448,7 +1477,7 @@ export class SecApiClient { name: string query: string filters?: Record - searchMode?: "keyword" + searchMode?: "keyword" | "situation" | "filing_event" | "footnote" | "section_delta" webhookUrl?: string delivery?: { type: "email"; config: { to: string } } }, @@ -1900,6 +1929,107 @@ export class SecApiClient { return this.get("/v1/dilution/coverage", params) } + /** + * List durable special situations (M&A, tender offers, going-private, + * spin-offs, activist campaigns, restructuring, bankruptcy, ...). Pass + * `forms` to filter by the EDGAR forms that triggered the situation, and + * `enrich=false` for the minimal stripped projection. + */ + async listSituations(params: RequestParams<{ types?: string | string[]; subtypes?: string | string[]; statuses?: string | string[]; tickers?: string | string[]; forms?: string | string[]; sectors?: string | string[]; market_cap?: string | string[]; country?: string; announced_from?: string; announced_to?: string; updated_from?: string; enrich?: "true" | "false"; cursor?: string | number; limit?: number; response_mode?: "compact" | "standard" | "verbose" | "agent" }> = {}) { + return this.get("/v1/situations", params) + } + + /** List immutable, numbered weekly Special Situations Digest issues. */ + async situationsIssues(params: RequestParams<{ limit?: number }> = {}) { + return this.get("/v1/situations/issues", params) + } + + /** Retrieve one immutable Special Situations Digest issue by number or slug. */ + async situationIssue(issue: string | number, options?: RequestOptions) { + return this.get(`/v1/situations/issues/${encodeURIComponent(String(issue))}`, options) + } + + async situationsFeed( + params: RequestParams<{ types?: string | string[]; categories?: string | string[]; tickers?: string | string[]; since?: string; cursor?: string; limit?: number; response_mode?: "compact" | "standard" | "verbose" | "agent" }> = {}, + ) { + return this.get("/v1/situations/feed", params) + } + + async situationsCalendar( + params: RequestParams<{ types?: string | string[]; date_types?: string | string[]; tickers?: string | string[]; days?: number; cursor?: string; limit?: number }> = {}, + ) { + return this.get("/v1/situations/calendar", params) + } + + async situationsStats(params: RequestParams<{ window?: string }> = {}) { + return this.get("/v1/situations/stats", params) + } + + async situationsPerformance( + params: RequestParams<{ types?: string | string[]; window?: string; group_by?: "type" | "subtype" }> = {}, + ) { + return this.get("/v1/situations/performance", params) + } + + /** + * List special situations opened or advanced by a given EDGAR form type + * (e.g. `SC 13D`, `SC TO-T`, `425`, `DEFM14A`). The form path segment is + * URL-encoded for you. + */ + async situationsByForm(form: string, params: RequestParams<{ statuses?: string | string[]; tickers?: string | string[]; enrich?: "true" | "false"; cursor?: string | number; limit?: number; response_mode?: "compact" | "standard" | "verbose" | "agent" }> = {}) { + return this.get(`/v1/situations/by-form/${encodeURIComponent(form)}`, params) + } + + /** Retrieve a single situation with its full per-filing timeline. */ + async getSituation(situationId: string, params: RequestParams<{ enrich?: "true" | "false" }> = {}) { + return this.get(`/v1/situations/${encodeURIComponent(situationId)}`, params) + } + + /** Retrieve a situation's per-filing timeline as a paginated sub-resource. */ + async situationFilings(situationId: string, params: RequestParams<{ cursor?: string | number; limit?: number }> = {}) { + return this.get(`/v1/situations/${encodeURIComponent(situationId)}/filings`, params) + } + + /** Retrieve a compact situation summary. */ + async situationSummary(situationId: string, options?: RequestOptions) { + return this.get(`/v1/situations/${encodeURIComponent(situationId)}/summary`, options) + } + + /** Retrieve the paid, source-cited underwriting JSON bundle for one situation. */ + async underwriteSituation(situationId: string, options?: RequestOptions) { + return this.get(`/v1/situations/${encodeURIComponent(situationId)}/underwriting-pack`, options) + } + + /** + * Create a durable public Special Situations monitor. Broad watches begin at + * database time; exact situation watches receive filing-level provenance. + */ + async watchSituations(input: SituationWatchInput, options?: RequestOptions) { + const filters = { + ...(input.situationIds?.length ? { situationIds: input.situationIds } : {}), + ...(input.types?.length ? { types: input.types } : {}), + ...(input.subtypes?.length ? { subtypes: input.subtypes } : {}), + ...(input.statuses?.length ? { statuses: input.statuses } : {}), + ...(input.tickers?.length ? { tickers: input.tickers } : {}), + ...(input.sectors?.length ? { sectors: input.sectors } : {}), + } + if (Object.keys(filters).length === 0) { + throw new Error("Situation watch requires at least one selector") + } + return this.createMonitor({ + name: input.name ?? "Special Situations watch", + query: "situations.watch", + searchMode: "situation", + filters, + ...(input.delivery ? { delivery: input.delivery } : {}), + }, options) + } + + /** Render a source-cited Markdown Copy-for-LLM brief for one situation. */ + async exportSituation(situationId: string, options?: RequestOptions) { + return this.get(`/v1/situations/${encodeURIComponent(situationId)}/export`, options) + } + async form144Filings( params: RequestParams<{ ticker?: string; cik?: string; date_from?: string; date_to?: string; submission_file_limit?: number; cursor?: string; limit?: number; view?: ResponseView }> = {}, ) { diff --git a/test/agent-helpers.test.ts b/test/agent-helpers.test.ts index bdab6fc..10e6fd7 100644 --- a/test/agent-helpers.test.ts +++ b/test/agent-helpers.test.ts @@ -561,6 +561,72 @@ describe("SecApiClient agent helpers", () => { ]) }) + test("situations namespace routes paid archive and research workflows", async () => { + const seenUrls: string[] = [] + const client = new SecApiClient({ + telemetry: false, + fetch: async (url) => { + seenUrls.push(String(url)) + return jsonResponse({ ok: true }) + }, + }) + + await client.situations.list({ types: "merger", statuses: "pending", limit: 10 }) + await client.situations.issues({ limit: 12 }) + await client.situations.issue(22) + await client.situations.feed({ types: "merger", limit: 5 }) + await client.situations.calendar({ date_types: "vote", days: 30 }) + await client.situations.stats({ window: "30d" }) + await client.situations.performance({ group_by: "type" }) + await client.situations.byForm("SC 13D", { limit: 5 }) + await client.situations.get("sit_example") + await client.situations.filings("sit_example", { limit: 5 }) + await client.situations.summary("sit_example") + await client.situations.underwrite("sit_example") + await client.situations.watch({ types: ["merger"], tickers: ["AAPL"] }) + await client.underwriteSituation("sit with/slash") + await client.situations.export("sit_example") + + expect(seenUrls).toEqual([ + "https://api.secapi.ai/v1/situations?types=merger&statuses=pending&limit=10", + "https://api.secapi.ai/v1/situations/issues?limit=12", + "https://api.secapi.ai/v1/situations/issues/22", + "https://api.secapi.ai/v1/situations/feed?types=merger&limit=5", + "https://api.secapi.ai/v1/situations/calendar?date_types=vote&days=30", + "https://api.secapi.ai/v1/situations/stats?window=30d", + "https://api.secapi.ai/v1/situations/performance?group_by=type", + "https://api.secapi.ai/v1/situations/by-form/SC%2013D?limit=5", + "https://api.secapi.ai/v1/situations/sit_example", + "https://api.secapi.ai/v1/situations/sit_example/filings?limit=5", + "https://api.secapi.ai/v1/situations/sit_example/summary", + "https://api.secapi.ai/v1/situations/sit_example/underwriting-pack", + "https://api.secapi.ai/v1/monitors", + "https://api.secapi.ai/v1/situations/sit%20with%2Fslash/underwriting-pack", + "https://api.secapi.ai/v1/situations/sit_example/export", + ]) + }) + + test("situations watch creates a typed monitor and rejects unscoped watches", async () => { + const seen: Array<{ body?: unknown }> = [] + const client = new SecApiClient({ + telemetry: false, + fetch: async (_url, init) => { + seen.push({ body: init?.body ? JSON.parse(String(init.body)) : undefined }) + return jsonResponse({ ok: true }) + }, + }) + + await client.situations.watch({ situationIds: ["sit_abc123"], delivery: { type: "email", config: { to: "you@example.com" } } }) + expect(seen[0]?.body).toEqual({ + name: "Special Situations watch", + query: "situations.watch", + searchMode: "situation", + filters: { situationIds: ["sit_abc123"] }, + delivery: { type: "email", config: { to: "you@example.com" } }, + }) + await expect(client.situations.watch({})).rejects.toThrow("requires at least one selector") + }) + test("paginateFilings follows nextCursor and yields items across pages", async () => { const seenUrls: string[] = [] const responses = [