Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/generated-contracts/v1-post-bodies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
132 changes: 131 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -950,6 +962,23 @@ export class SecApiClient {
creditRating: (...args: Parameters<SecApiClient["macroCreditRating"]>) => this.macroCreditRating(...args),
}

readonly situations = {
list: (...args: Parameters<SecApiClient["listSituations"]>) => this.listSituations(...args),
issues: (...args: Parameters<SecApiClient["situationsIssues"]>) => this.situationsIssues(...args),
issue: (...args: Parameters<SecApiClient["situationIssue"]>) => this.situationIssue(...args),
get: (...args: Parameters<SecApiClient["getSituation"]>) => this.getSituation(...args),
filings: (...args: Parameters<SecApiClient["situationFilings"]>) => this.situationFilings(...args),
summary: (...args: Parameters<SecApiClient["situationSummary"]>) => this.situationSummary(...args),
underwrite: (...args: Parameters<SecApiClient["underwriteSituation"]>) => this.underwriteSituation(...args),
watch: (...args: Parameters<SecApiClient["watchSituations"]>) => this.watchSituations(...args),
export: (...args: Parameters<SecApiClient["exportSituation"]>) => this.exportSituation(...args),
feed: (...args: Parameters<SecApiClient["situationsFeed"]>) => this.situationsFeed(...args),
calendar: (...args: Parameters<SecApiClient["situationsCalendar"]>) => this.situationsCalendar(...args),
stats: (...args: Parameters<SecApiClient["situationsStats"]>) => this.situationsStats(...args),
performance: (...args: Parameters<SecApiClient["situationsPerformance"]>) => this.situationsPerformance(...args),
byForm: (...args: Parameters<SecApiClient["situationsByForm"]>) => this.situationsByForm(...args),
}

readonly portfolio = {
optimize: (...args: Parameters<SecApiClient["portfolioOptimize"]>) => this.portfolioOptimize(...args),
hedge: (...args: Parameters<SecApiClient["portfolioHedge"]>) => this.portfolioHedge(...args),
Expand Down Expand Up @@ -1448,7 +1477,7 @@ export class SecApiClient {
name: string
query: string
filters?: Record<string, unknown>
searchMode?: "keyword"
searchMode?: "keyword" | "situation" | "filing_event" | "footnote" | "section_delta"
webhookUrl?: string
delivery?: { type: "email"; config: { to: string } }
},
Expand Down Expand Up @@ -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<string>(`/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 }> = {},
) {
Expand Down
66 changes: 66 additions & 0 deletions test/agent-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down