- Loan-tape and document uploads take three steps. The server returns a pre-signed URL and the client
PUTs the file itself. trigger_selection_criteria_runis asynchronous. It returns a job ID, not results.update_webhookreplaces the event list. It does not append to it.complete_bulk_loan_uploadcan report per-deal failures inside a successful response.- Calls that delete records or commit capital need explicit human confirmation.
Skills describe how to use the MCP server. They do not register it. Connect an AI client first.
Available skills
| Skill | Covers |
|---|---|
9squid-mcp | Core skill: registration, roles, auth, tool inventory, error handling, safety rules |
9squid-mcp-loan-pipeline | Originator: tape upload, QC, selection criteria, pools, ALM, approvals |
9squid-mcp-investor-desk | Investor: browse, diligence, IOI, subscribe, allocations, portfolio |
9squid-mcp-webhooks | Webhook subscriptions, event catalogue, delivery debugging, replay |
9squid-mcp in every case. The workflow skills assume it is present. You can install any subset of the other three.
Install
Each skill is a directory that holds oneSKILL.md file. Copy the directories to the place your client loads skills from.
Claude Code
- Per project
- Per user
Commit the skills alongside the code that uses the MCP server. Everyone who works in the repository gets them.
mkdir -p .claude/skills
cp -R 9squid-mcp* .claude/skills/
Make the skills available in every project on your machine.
mkdir -p ~/.claude/skills
cp -R 9squid-mcp* ~/.claude/skills/
/9squid-mcp-loan-pipeline.
Claude Desktop and claude.ai
Zip each skill directory, so the archive contains9squid-mcp/SKILL.md and so on. Then upload the archives in Settings → Capabilities → Skills.
Other agents
Any client that supports the Agent Skills format can load these files. For an agent without skill support, paste the relevantSKILL.md content into its system prompt or project instructions.
Create the files
To create the skill files, copy eachSKILL.md below into a directory with the same name as the skill:
9squid-mcp/SKILL.md
9squid-mcp-loan-pipeline/SKILL.md
9squid-mcp-investor-desk/SKILL.md
9squid-mcp-webhooks/SKILL.md
Skill files
9squid-mcp/SKILL.md
9squid-mcp/SKILL.md
Read first. Every workflow skill depends on it.
9squid-mcp/SKILL.md
---
name: 9squid-mcp
description: Work with the 9squid securitization platform through the 9squid Platform MCP server — registering the server, choosing the originator or investor role, discovering the available tools, reading errors, and the safety rules that apply to every tool call. Use whenever a task involves 9squid data (loans, deals, pools, portfolio, alerts, webhooks) through MCP, or whenever a 9squid MCP tool needs to be called or debugged.
---
# Using the 9squid Platform MCP server
The 9squid MCP server exposes the 9squid Platform API Gateway as **53 MCP
tools**. It forwards the caller's Clerk API key to the backend unchanged and
gates tool *visibility* by the role declared at registration time.
This skill covers the invariants that apply to *every* tool call. For
multi-step business workflows, see the companion skills:
**9squid-mcp-loan-pipeline** (originator), **9squid-mcp-investor-desk**
(investor), **9squid-mcp-webhooks**.
> The same 53 operations are also available as a command-line tool. If the
> environment has the `9squid` CLI instead of this MCP server, use the
> **9squid-cli** skill family.
## Registration
Transport is MCP **Streamable HTTP**. Two things must be supplied on every
request: a bearer token and a role.
```json
{
"mcpServers": {
"9squid-originator": {
"type": "http",
"url": "https://<mcp-host>/mcp/?role=originator",
"headers": {
"Authorization": "Bearer ${NINE_SQUID_API_KEY}",
"x-9squid-role": "originator"
}
}
}
}
```
Use `?role=investor` (and the matching header) for investor users. Register
one server per role — a single connection cannot hold both.
Claude Code equivalent:
```sh
claude mcp add --transport http --scope local \
--header "Authorization: Bearer <your-api-key>" \
--header "x-9squid-role: originator" \
9squid-originator "https://<mcp-host>/mcp/?role=originator"
```
Notes that matter:
- **`--scope local`** keeps a personal key out of a shared project `.mcp.json`.
If a project-level config is needed, use `${ENV_VAR}` expansion — never a
literal key.
- The `x-9squid-role` header is a **fallback** for clients and proxies (MCP
Inspector among them) that drop the query string. The query parameter is the
primary mechanism; sending both is safe and recommended.
- The hosted staging server is `https://mcp-stg.9squid.com`. For local
development the host is `http://localhost:8013`.
## Authentication
The credential is a **Clerk API key** — the same bearer token the 9squid
backend uses. There is no separate MCP auth scheme.
The MCP server only checks that *a* bearer token is present; the upstream
Platform API decides whether it is valid. So a connected server with visible
tools does **not** mean the key works — the first `get_platform_health` or
list call is the real test.
**Never** ask a human to paste a key into chat, and never write one into a
file, a logged command, or a commit. It belongs in the MCP client's secret or
config mechanism only.
## Roles are fail-closed
| Registration | Tools visible |
|---|---|
| `role=originator` | 39 |
| `role=investor` | 31 |
| missing or invalid role | **0** |
The union of both roles is the full 53 operations. Role gating is enforced at
tool *listing* time, so a tool that is not visible cannot be called at all.
**Zero tools visible is a registration problem, not a permissions problem.**
Check that the URL contains exactly `?role=originator` or `?role=investor`, or
that the `x-9squid-role` header is present. Do not look for a way around role
gating — reconnect with the correct role instead.
## Tool groups
**Originator only** — see **9squid-mcp-loan-pipeline**
- *Loans*: `initiate_loan_upload`, `complete_loan_upload`,
`reinitiate_loan_upload`, `list_loans`, `get_loan`, `approve_or_reject_deal`,
`initiate_bulk_loan_upload`, `complete_bulk_loan_upload`, `get_bulk_job`,
`get_bulk_job_status`, `regenerate_bulk_loan_upload_urls`
- *Selection criteria*: `trigger_selection_criteria_run`,
`get_sc_results_for_deal`, `list_sc_exceptions`, `get_sc_exception`
- *Pools*: `list_pools`, `get_pool`, `get_pool_by_deal`, `get_pool_summary`,
`get_pool_stratification_report`
- *Analytics*: `run_alm_analysis`, `get_alm_analysis`
**Investor only** — see **9squid-mcp-investor-desk**
- *Deals*: `list_investor_deals`, `get_investor_deal`, `get_investor_deal_pool`,
`get_investor_deal_tranches`, `get_investor_deal_documents`,
`get_investor_comparable_deals`, `get_investor_originator_profile`
- *Orders*: `submit_indication_of_interest`, `submit_subscription`,
`list_subscriptions`, `get_deal_allocations`
- *Portfolio*: `get_portfolio_summary`, `get_portfolio_positions`,
`get_portfolio_reports`
**Shared by both roles**
- *Deal-room documents*: `get_document_upload_url`, `complete_document_upload`,
`list_deal_documents`, `get_deal_document`
- *Webhooks* — see **9squid-mcp-webhooks**: `create_webhook`, `list_webhooks`,
`get_webhook`, `update_webhook`, `delete_webhook`, `test_webhook`,
`replay_webhook_delivery`, `get_webhook_deliveries`,
`get_global_webhook_deliveries`
- *Alerts*: `get_alerts`, `get_alert`, `delete_alert`
- *Health*: `get_platform_health`
## Reading errors
Tool failures surface as HTTP status from the upstream gateway. Read the
status before retrying:
| Status | Meaning | What to do |
|---|---|---|
| 400 / 422 | validation | an argument is wrong or missing — fix and retry |
| 401 | auth | the Clerk API key is missing, expired, or wrong for this environment |
| 403 | authorization | the key is valid but not entitled to this resource |
| 404 | not found | the ID does not exist **in this environment** — re-list to get a real one |
| 429 | rate limited | back off, then retry once |
| 5xx | upstream degraded | call `get_platform_health`, retry later, do not loop |
Two distinctions worth making explicitly:
- **401/403 means the MCP connection is fine and the credential is not.** The
server connected and the tools listed; the backend rejected the key.
- **404 very often means "right ID, wrong environment."** The MCP server
points at whatever `NEST_API_URL` is configured — dev, staging, or prod.
Confirm which one before concluding a record was deleted.
Use `get_platform_health` to separate "my credential is broken" from "the
platform is down" when calls start failing.
## Never invent identifiers
Deal, pool, webhook, alert, document, and exception IDs must come from a
previous tool's output — never from memory, a pattern, or an example in this
document. List first, then act.
The same rule covers file names, `file_key` values, pre-signed URLs, monetary
amounts, statuses, and tranche preferences. If a value was not returned by a
tool or stated by the human, do not supply it.
Response envelopes vary by endpoint (`{data: [...]}`, `{result: {...}}`, or a
bare object). Inspect the shape actually returned rather than assuming one.
## Confirm before state-changing calls
These tools change state, and several are irreversible or commit money.
Get explicit human instruction — naming the resource, and for money commands
the amount — before calling any of them:
- `approve_or_reject_deal` — **`status` (`APPROVED` or `REJECTED`) must be
stated explicitly**; never infer an approval from QC or SC output
- `submit_subscription` — commits capital
- `submit_indication_of_interest` — non-binding, but visible to the originator
- `complete_loan_upload`, `complete_bulk_loan_upload`,
`complete_document_upload` — register files against a deal
- `create_webhook`, `update_webhook`, `delete_webhook`,
`replay_webhook_delivery`
- `delete_alert`
Pre-signed upload and download URLs are credentials in themselves — treat them
as sensitive and do not echo them into shared output.
## Uploads are not single calls
Loan tapes and deal-room documents both follow a **get URL → PUT the file →
register** pattern. The MCP server never transfers file bytes; the client does
the `PUT` to the pre-signed URL directly. See **9squid-mcp-loan-pipeline** for
the loan-tape flow.
## Verifying a connection
```sh
# Server liveness (no auth required):
curl https://<mcp-host>/health # → {"status":"ok"}
# Tool listing for a role:
npx @modelcontextprotocol/inspector "https://<mcp-host>/mcp/?role=originator"
```
Expected: `originator` → 39 tools, `investor` → 31, no role → 0. Listing tools
outside an HTTP request (for example calling `main.mcp.list_tools()` directly)
also returns 0, because no role is present — that is correct behavior, not a
bug.
9squid-mcp-loan-pipeline/SKILL.md
9squid-mcp-loan-pipeline/SKILL.md
9squid-mcp-loan-pipeline/SKILL.md
---
name: 9squid-mcp-loan-pipeline
description: Run the 9squid originator loan pipeline through the MCP server — upload loan tapes (single or bulk), track QC and deal status, run selection criteria and triage exceptions, inspect securitization pools and stratification, run ALM analysis, and approve or reject deals. Use for any originator-side 9squid task involving loans, loan tapes, deals, SC runs, pools, or ALM.
---
# 9squid originator loan pipeline (MCP)
Read **9squid-mcp** first for registration, roles, error handling, and the
safety rules. Every tool here requires an **originator** registration
(`?role=originator`); on an investor connection they are not visible at all.
Pipeline shape:
```
upload tape → QC → selection criteria → exceptions → pool → ALM → approve/reject
```
## 1. Upload a loan tape (three steps, not one)
The MCP server does **not** transfer the file. The API hands back a pre-signed
URL and the file is uploaded to it directly by the client.
1. **`initiate_loan_upload(loan_type)`** → returns `deal_id`, `upload_url`,
`file_name`, `expires_in`.
2. **`PUT` the file to `upload_url`** — outside MCP. For example:
`curl -sSf -X PUT --upload-file ./tape.csv "<upload_url>"`
3. **`complete_loan_upload(deal_id, file_name)`** → registers the file; the
deal moves to `IN_REVIEW`.
Notes that matter:
- **Use the `file_name` from step 1**, not the local filename. Step 3
validates against what the gateway registered.
- Only **CSV or XLSX** are accepted; step 3 rejects anything else.
- The URL expires in **1 hour**. If step 2 or 3 fails after that, call
`reinitiate_loan_upload(deal_id)` for a fresh URL rather than starting over —
it reuses the same deal.
- Calling `initiate_loan_upload` again with the same loan type **reuses an
existing DRAFT deal** instead of creating a second one. That is expected.
- `complete_loan_upload` is state-changing — confirm the deal and file with the
human before calling it.
## 2. Bulk upload
1. **`initiate_bulk_loan_upload(loan_types)`** → one `bulk_job_id` plus one
upload URL per deal.
2. `PUT` each file to its own URL.
3. **`complete_bulk_loan_upload(bulk_job_id, loans)`** where `loans` is a list
of `{"loan_id": "...", "path": "..."}` objects — one per uploaded file,
using the loan ID and file path returned for that file in step 1.
Tracking:
- `get_bulk_job(job_id)` — the job record.
- `get_bulk_job_status(job_id)` — per-deal statuses.
- `regenerate_bulk_loan_upload_urls(job_id)` — fresh URLs when they expire
mid-run.
**`complete_bulk_loan_upload` returns per-deal results including errors — a
successful call does not mean every deal succeeded.** Inspect each entry and
report failures individually rather than summarizing the call as "done".
## 3. Inspect deals and QC
- `list_loans(page, limit)` — paginated deal list.
- `get_loan(deal_id)` — the full record: QC results and lifecycle state.
`get_loan` is the single source of truth for where a deal sits. Read its QC
results before running selection criteria or proposing an approval.
## 4. Selection criteria
- `trigger_selection_criteria_run(deal_id)` → returns `sc_job_id`.
- `get_sc_results_for_deal(deal_id)` — results across all runs, showing which
loans passed, failed, or raised exceptions.
- `list_sc_exceptions()` — exceptions across every run for the originator.
- `get_sc_exception(exception_file_id)` — loan-level detail and exception
history.
**`trigger_selection_criteria_run` is asynchronous** — it returns a job id, not
results. Poll `get_sc_results_for_deal` instead of assuming completion. Wait
several seconds between attempts, give up after a bounded number of tries, and
report the `sc_job_id` so the human can follow up.
## 5. Pools
- `list_pools()` — all pools for the originator.
- `get_pool(pool_id)` — full pool detail.
- `get_pool_by_deal(deal_id)` — which pool a deal landed in.
- `get_pool_summary(pool_id)` — aggregates: `total_balance`, `loan_count`,
`wa_coupon`, `wa_maturity`, `wa_ltv`, `wa_fico`, `wa_dti`, `wa_seasoning`,
min/max/avg balance, `delinquency_breakdown`, `geographic_top_5`,
`vintage_distribution`.
- `get_pool_stratification_report(pool_id)` — returns a **download URL** for
the report file, not the report contents. Fetch it separately if the human
wants the file, and treat the URL as sensitive.
## 6. ALM analysis
- `run_alm_analysis(deal_id)` — recomputes and returns the full TCT Risk
report: duration (Macaulay, modified, effective, spread), convexity, key rate
durations, NII sensitivity, EVE analysis, gap analysis, VaR metrics, stress
test results.
- `get_alm_analysis(deal_id)` — retrieves the stored results.
The report is large. Prefer `get_alm_analysis` when the human just wants to see
existing numbers, and summarize rather than dumping the whole payload.
## 7. Approve or reject — a human decision
`approve_or_reject_deal(deal_id, status, reason)`
- `status` must be exactly `APPROVED` or `REJECTED`; anything else is rejected
before the call reaches the gateway.
- **Never call this without the human explicitly stating the decision.** Do not
infer an approval from clean QC or SC output — surface the evidence and let
them choose.
- A rejection requires a reason. Use the human's wording, not a paraphrase.
- The call is effectively irreversible from the pipeline's point of view.
## Triage recipe
When asked "what needs attention?":
1. `get_alerts(status="UNREAD")` — unread notifications, highest priority first.
2. `list_sc_exceptions()` — unresolved selection-criteria exceptions.
3. `list_loans()` — then filter on lifecycle state for deals stuck in
`IN_REVIEW`.
Report each item **with its ID**, so follow-up calls can use it. Push
notification instead of polling is available — see **9squid-mcp-webhooks**
(`loan.qc.failed`, `loan.qc.exception`, `pool.finalized`,
`report.alm.ready`).
9squid-mcp-investor-desk/SKILL.md
9squid-mcp-investor-desk/SKILL.md
9squid-mcp-investor-desk/SKILL.md
---
name: 9squid-mcp-investor-desk
description: Run the 9squid investor workflow through the MCP server — browse securitization deals, do diligence on pool data, tranches, documents and comparables, check originator track records, submit indications of interest and subscriptions, and review allocations and portfolio positions. Use for any investor-side 9squid task involving deals, IOIs, subscriptions, allocations, or portfolio.
---
# 9squid investor desk (MCP)
Read **9squid-mcp** first for registration, roles, error handling, and the
safety rules. Every tool here requires an **investor** registration
(`?role=investor`); on an originator connection they are not visible at all.
Flow:
```
browse → diligence → IOI (soft) → subscribe (binding) → allocation → portfolio
```
## 1. Browse deals
`list_investor_deals(page, limit, search, status)` — paginated deal summaries
with metrics and a tranche overview. `search` matches on name; `status` filters
the lifecycle state.
Take deal IDs from this output. Never construct one.
## 2. Diligence
- `get_investor_deal(deal_id)` — deal detail including tranche structure.
- `get_investor_deal_pool(deal_id)` — anonymized loan-level pool data plus
aggregate statistics.
- `get_investor_deal_tranches(deal_id)` — tranches with pricing.
- `get_investor_comparable_deals(deal_id)` — comparables for relative value.
- `get_investor_deal_documents(deal_id)` — investor-visible documents with
download URLs.
- `get_investor_originator_profile(originator_id)` — profile and track record.
For a diligence summary, pull `get_investor_deal`,
`get_investor_deal_tranches`, and `get_investor_comparable_deals` and present
them together — **pricing means little without the comparable set.**
The pool data is anonymized by design. Do not present it as loan-level PII or
attempt to re-identify borrowers from it.
Deal-room documents are also reachable through the shared document tools:
- `list_deal_documents(deal_id, document_type=..., status=...)` —
`document_type` is one of `legal`, `internal`, `loan_tape`,
`credit_union_export`; `status` is `active`, `archived`, or `all`.
- `get_deal_document(deal_id, document_id)` — metadata plus a **download URL**.
The document tools return URLs, not file contents. Fetch the file separately if
the human wants it, and treat the URL as sensitive.
## 3. Indication of interest — soft, but still a signal
`submit_indication_of_interest(deal_id, interest_amount, notes)`
`interest_amount` is in **USD** (e.g. `500000` for $500k). An IOI is
non-binding but **visible to the originator**. Only submit one with an amount
the human has explicitly given.
## 4. Subscription — binding, confirm first
`submit_subscription(deal_id, subscription_amount, tranche_preferences, notes)`
This **commits capital** and triggers compliance checks. Rules:
- Never call it without the human naming both the deal and the amount.
- `subscription_amount` is in USD. **Confirm the figure back before
executing** — a misplaced zero is a six-figure error.
- `tranche_preferences` is an optional list of
`{"tranche_id": "...", "amount": 500000}` objects. Use tranche IDs returned
by `get_investor_deal_tranches`, never invented ones, and check that the
preference amounts reconcile with `subscription_amount`.
- The response carries `subscription_id`, a `status` (typically
`pending_confirmation`), and `compliance_check_result`. **Read the compliance
result** — a successful call does not mean the subscription cleared.
## 5. Track subscriptions and allocations
- `list_subscriptions(page, limit, status, deal_id)` — across all deals.
- `get_deal_allocations(deal_id, include_tranche)` — per-tranche
`allocated_amount`, `pct_fill`, `settlement_date`, `confirmation_id`. Set
`include_tranche=True` for full tranche details.
Allocations are the final word on what was actually filled. **Expect them to
differ from the subscribed amount** and report the fill percentage, not just
the amount.
## 6. Portfolio
- `get_portfolio_summary()` — total invested, NAV, returns, allocation
breakdown.
- `get_portfolio_positions()` — individual deal holdings and valuations.
- `get_portfolio_reports()` — holdings snapshot, trade history, tranche
performance, pool-level metrics.
For a portfolio review, start with `get_portfolio_summary`, then pull
`get_portfolio_positions` for detail. In `get_portfolio_reports`, performance
sections are **omitted** when no calculation job has run yet — an absent
section means "not calculated", not "zero".
## Watching for events
- `get_alerts(status="UNREAD")` — allocation finalization, pricing changes, and
document updates surface here. Filter further by `type`, `category`, or
`priority`.
- `get_alert(alert_id)` — a single alert.
For push notifications instead of polling, see **9squid-mcp-webhooks**
(`deal.priced`, `subscription.confirmed`, `allocation.finalized`).
9squid-mcp-webhooks/SKILL.md
9squid-mcp-webhooks/SKILL.md
9squid-mcp-webhooks/SKILL.md
---
name: 9squid-mcp-webhooks
description: Manage and debug 9squid webhook subscriptions through the MCP server — register endpoints for platform events, send test pings, inspect the delivery log, diagnose failed deliveries, and replay them. Use whenever a 9squid task involves webhooks, event subscriptions, missed or failing deliveries, or integration debugging.
---
# 9squid webhooks (MCP)
Read **9squid-mcp** first for registration, roles, error handling, and the
safety rules. Webhook tools are visible to **both** originator and investor
registrations.
## Register a subscription
`create_webhook(url, events, secret, description, active, headers)`
- `url` and `events` are required; `events` is a list of event-type strings.
- `secret` (**min 16 characters**) signs delivery payloads so the receiver can
verify them. Always set one for a real endpoint. Take it from the human's
secret store or an environment variable — **never generate one and never
echo it back** into chat, logs, or a commit.
- `headers` is an optional dict of custom headers sent with every delivery
(e.g. `{"X-Tenant": "acme"}`).
- `active` defaults to `true`.
## Event catalogue
Subscribe narrowly to the events the integration actually handles rather than
to everything:
- **loans**: `loan.ingested`, `loan.qc.passed`, `loan.qc.failed`,
`loan.qc.exception`, `loan.updated`, `loan.repurchase.demanded`
- **pools**: `pool.created`, `pool.finalized`, `pool.stratification.complete`,
`pool.constraint.violated`
- **deals**: `deal.created`, `deal.structured`, `deal.priced`, `deal.closed`,
`deal.amended`, `deal.called`
- **orders**: `ioi.received`, `subscription.submitted`,
`subscription.confirmed`, `allocation.finalized`
- **reporting**: `report.generated`, `report.alm.ready`,
`report.exception.flagged`, `remittance.received`, `performance.updated`,
`delinquency.threshold.breached`, `modification.reported`
- **partners**: `partner.loan.forwarded`, `partner.loan.accepted`,
`partner.loan.rejected`, `partner.loan.securitized`
- **documents**: `document.uploaded`, `document.signed`, `document.expired`
- **account**: `user.created`, `user.deactivated`, `api_key.rotated`,
`api_key.revoked`
The tool's own schema is the authoritative list. An event name not in it is
rejected at validation time.
## Inspect and update
- `list_webhooks(page, limit, is_active)` — all subscriptions, optionally
filtered by active status.
- `get_webhook(webhook_id, include_deliveries)` — one subscription, optionally
with its delivery history.
- `update_webhook(webhook_id, url, events, active, headers)` — a **PATCH**:
only the fields passed are changed.
**Passing `events` replaces the whole list rather than adding to it.** Read the
current events with `get_webhook` first and send the full intended set.
To pause a subscription without losing it, call
`update_webhook(webhook_id, active=False)`.
## Test an endpoint
`test_webhook(webhook_id, event_type)` sends a test ping. Use it right after
`create_webhook` to prove the endpoint is reachable, instead of waiting on real
traffic to find out.
## Debug deliveries
- `get_webhook_deliveries(webhook_id, ...)` — delivery history for one
subscription.
- `get_global_webhook_deliveries(...)` — the log across all subscriptions;
accepts an optional `webhook_id` filter.
Both accept `status` (`pending`, `succeeded`, `failed`, `retrying`),
`event_type`, `date_from` / `date_to` (**ISO 8601 UTC** strings), `page`, and
`limit`.
## Replay a failed delivery
`replay_webhook_delivery(webhook_id, delivery_id)` re-sends the original
payload. Take `delivery_id` from a deliveries listing — never construct it.
**If the receiver is not idempotent, a replay double-processes the event.**
Confirm with the human before replaying, and especially before replaying in
bulk.
## Diagnostic sequence
When someone reports "we're not getting events":
1. `list_webhooks()` — does the subscription exist, and is it active?
2. `get_webhook(id)` — is the event actually in its `events` list, and is the
URL right?
3. `get_webhook_deliveries(id, status="failed")` — was it attempted and
rejected?
4. `test_webhook(id, event_type)` — is the endpoint reachable *now*?
5. Once the receiver is fixed, replay the failed deliveries.
State which of the two failure modes it is:
- **No delivery attempts** → a subscription or configuration problem: wrong
events, inactive subscription, wrong URL.
- **Failed attempts** → the receiver rejected them; the status codes and error
detail are in the delivery log.
## Deleting
`delete_webhook(webhook_id)` is **permanent and takes the delivery history with
it.** Confirm with the human first, and when they only want to pause a webhook,
set `active=False` instead.
## Alerts as an alternative
When an integration endpoint is not available, the same lifecycle events are
visible by polling `get_alerts(status="UNREAD")`. Webhooks are the push path;
alerts are the pull path.
Command-line equivalent
The same 53 operations ship as the9squid CLI, which has a parallel skill set: 9squid-cli, 9squid-cli-loan-pipeline, 9squid-cli-investor-desk, and 9squid-cli-webhooks. The two families use distinct names, so you can install both for an agent that has access to both.