feat: webhook system with retain.completed event, UI, and docs (#487)

* doc: update cookbook

* fix(cookbook): preserve tag keys during sync, strip local .md links

- Fix extract_tags_from_readme/notebook to return dict[str,str] preserving
  sdk/topic keys instead of bare values, preventing topics like
  "Customer Service" from being misclassified as SDK
- Add strip_local_md_links() to remove relative .md references that
  would cause broken link errors in Docusaurus build

* ci: run test-doc-examples independently without waiting for test-rust-cli

Build the CLI directly in the job instead of downloading the artifact,
so test-doc-examples can start at the beginning in parallel with all other jobs.

* feat: webhook system with task-owned retry, retain.completed event, and UI

- New webhook system: register per-bank webhooks with HMAC signing, configurable
  HTTP method/timeout/headers/params (http_config JSONB), and PATCH support
- Webhook deliveries run as async_operations (webhook_delivery type) with
  task-owned retry via RetryTaskAt exception and exponential backoff
  (60s / 5m / 30m / 2h / 8h, max 6 attempts)
- New retain.completed event fires per-document for both sync and async retain
- Delivery debug info (status code, response body) stored in result_metadata
- Control plane UI: webhooks tab per bank with create/edit/delete and a
  deliveries table with cursor pagination and expandable response details
- 28 webhook tests covering HMAC signing, delivery retries, CRUD endpoints,
  PATCH update, and retain.completed queuing
- Docs page at developer/api/webhooks documenting event payloads and delivery
- OpenAPI spec and all client SDKs (Python, TypeScript, Rust, Go) regenerated

* fix: update tests for task-owned retry model and guard _webhook_manager attribute

- test_worker.py: test_executor_exception_triggers_retry now raises RetryTaskAt
  (plain exceptions are immediate failures in the new system); rename
  test_executor_exception_marks_failed_after_max_retries to
  test_executor_exception_marks_failed_immediately to reflect new semantics
- test_batch_api.py: remove max_retries kwarg from WorkerPoller constructor
- memory_engine.py: use getattr for _webhook_manager in _fire_retain_webhook
  to avoid AttributeError when engine is created without __init__ (tests)

* fix: remove max_retries from benchmark WorkerPoller call

* fix(webhooks): transactional outbox, observations_deleted tracking, sidebar

- Queue webhook delivery rows atomically with the primary operation using the
  transactional outbox pattern — prevents lost events on process crash:
  - Retain (sync + async): outbox_callback passed into orchestrator.retain_batch
    and called inside the DB transaction, replacing the post-commit fire call
  - Consolidation: new _mark_operation_completed_and_fire_webhook combines the
    status UPDATE and webhook INSERT in one transaction
  - Added fire_event_with_conn() to WebhookManager for in-connection delivery

- Track observations_deleted count in consolidation stats and expose it in the
  consolidation.completed webhook payload (was always None)

- Add Webhooks page to docs sidebar

- Document at-least-once delivery guarantee with operation_id dedup guidance

* fix(ui): add retain.completed to available webhook event types

* feat(ui): add delete confirmation dialog for webhooks

* fix(webhooks): include operation_id in task_payload so delivery is marked completed

The task_payload JSON was missing the operation_id field, causing execute_task
to see operation_id=None and skip _mark_operation_completed — leaving every
delivery row stuck in 'pending' forever.

Added a test that inserts a real async_operations row and verifies the status
transitions to 'completed' after a successful execute_task call.

* style: fix prettier formatting in webhooks-view
This commit is contained in:
Nicolò Boschi 2026-03-04 14:17:01 +01:00 committed by GitHub
parent 51d2fc5309
commit abbf874d84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 10727 additions and 80 deletions

View file

@ -0,0 +1,62 @@
"""Add webhooks table and next_retry_at to async_operations.
Webhook deliveries are handled as async_operations tasks (operation_type='webhook_delivery')
rather than a dedicated webhook_deliveries table.
Revision ID: e4f5a6b7c8d9
Revises: d2e3f4a5b6c7
Create Date: 2026-03-04
"""
from collections.abc import Sequence
from alembic import context, op
revision: str = "e4f5a6b7c8d9"
down_revision: str | Sequence[str] | None = "d2e3f4a5b6c7"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _get_schema_prefix() -> str:
schema = context.config.get_main_option("target_schema")
return f'"{schema}".' if schema else ""
def upgrade() -> None:
schema = _get_schema_prefix()
op.execute(
f"""
CREATE TABLE IF NOT EXISTS {schema}webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bank_id TEXT,
url TEXT NOT NULL,
secret TEXT,
event_types TEXT[] NOT NULL DEFAULT '{{}}',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""
)
# Index for bank-scoped webhook lookup
op.execute(f"CREATE INDEX IF NOT EXISTS idx_webhooks_bank_id ON {schema}webhooks(bank_id)")
# Add next_retry_at to async_operations for task-owned retry scheduling
op.execute(f"ALTER TABLE {schema}async_operations ADD COLUMN IF NOT EXISTS next_retry_at TIMESTAMPTZ NULL")
# Index for polling: status + next_retry_at
op.execute(
f"CREATE INDEX IF NOT EXISTS idx_async_operations_status_retry "
f"ON {schema}async_operations(status, next_retry_at)"
)
def downgrade() -> None:
schema = _get_schema_prefix()
op.execute(f"DROP INDEX IF EXISTS {schema}idx_async_operations_status_retry")
op.execute(f"ALTER TABLE {schema}async_operations DROP COLUMN IF EXISTS next_retry_at")
op.execute(f"DROP INDEX IF EXISTS {schema}idx_webhooks_bank_id")
op.execute(f"DROP TABLE IF EXISTS {schema}webhooks")

View file

@ -0,0 +1,33 @@
"""Add http_config JSONB column to webhooks table.
Stores HTTP delivery configuration (method, timeout, headers, params) as a
single JSONB column rather than separate columns.
Revision ID: f7g8h9i0j1k2
Revises: e4f5a6b7c8d9
Create Date: 2026-03-04
"""
from collections.abc import Sequence
from alembic import context, op
revision: str = "f7g8h9i0j1k2"
down_revision: str | Sequence[str] | None = "e4f5a6b7c8d9"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _get_schema_prefix() -> str:
schema = context.config.get_main_option("target_schema")
return f'"{schema}".' if schema else ""
def upgrade() -> None:
schema = _get_schema_prefix()
op.execute(f"ALTER TABLE {schema}webhooks ADD COLUMN IF NOT EXISTS http_config JSONB NOT NULL DEFAULT '{{}}'")
def downgrade() -> None:
schema = _get_schema_prefix()
op.execute(f"ALTER TABLE {schema}webhooks DROP COLUMN IF EXISTS http_config")

View file

@ -1627,6 +1627,123 @@ class VersionResponse(BaseModel):
features: FeaturesInfo = Field(description="Enabled feature flags")
# =========================================================================
# Webhook Models
# =========================================================================
from hindsight_api.webhooks.models import WebhookHttpConfig
class CreateWebhookRequest(BaseModel):
"""Request model for registering a webhook."""
url: str = Field(description="HTTP(S) endpoint URL to deliver events to")
secret: str | None = Field(default=None, description="HMAC-SHA256 signing secret (optional)")
event_types: list[str] = Field(
default=["consolidation.completed"],
description="List of event types to deliver. Currently supported: 'consolidation.completed'",
)
enabled: bool = Field(default=True, description="Whether this webhook is active")
http_config: WebhookHttpConfig = Field(
default_factory=WebhookHttpConfig,
description="HTTP delivery configuration (method, timeout, headers, params)",
)
class WebhookResponse(BaseModel):
"""Response model for a webhook."""
id: str
bank_id: str | None
url: str
secret: str | None = Field(default=None, description="Signing secret (redacted in responses)")
event_types: list[str]
enabled: bool
http_config: WebhookHttpConfig = Field(default_factory=WebhookHttpConfig)
created_at: str | None = None
updated_at: str | None = None
class UpdateWebhookRequest(BaseModel):
"""Request model for updating a webhook. Only provided fields are updated."""
url: str | None = Field(default=None, description="HTTP(S) endpoint URL")
secret: str | None = Field(
default=None, description="HMAC-SHA256 signing secret. Omit to keep existing; send null to clear."
)
event_types: list[str] | None = Field(default=None, description="List of event types")
enabled: bool | None = Field(default=None, description="Whether this webhook is active")
http_config: WebhookHttpConfig | None = Field(default=None, description="HTTP delivery configuration")
class WebhookListResponse(BaseModel):
"""Response model for listing webhooks."""
items: list[WebhookResponse]
class WebhookDeliveryResponse(BaseModel):
"""Response model for a webhook delivery record."""
id: str
webhook_id: str | None
url: str
event_type: str
status: str
attempts: int
next_retry_at: str | None = None
last_error: str | None = None
last_response_status: int | None = None
last_response_body: str | None = None
last_attempt_at: str | None = None
created_at: str | None = None
updated_at: str | None = None
@classmethod
def from_async_operation_row(cls, row: dict) -> "WebhookDeliveryResponse":
import json as _json
raw = row["task_payload"]
if isinstance(raw, str):
task_payload = _json.loads(raw)
elif isinstance(raw, dict):
task_payload = raw
else:
task_payload = {}
raw_meta = row.get("result_metadata")
if isinstance(raw_meta, str):
result_metadata = _json.loads(raw_meta) if raw_meta else {}
elif isinstance(raw_meta, dict):
result_metadata = raw_meta
else:
result_metadata = {}
return cls(
id=str(row["operation_id"]),
webhook_id=task_payload.get("webhook_id"),
url=task_payload.get("url", ""),
event_type=task_payload.get("event_type", ""),
status=row["status"],
attempts=row["retry_count"] + 1,
next_retry_at=row["next_retry_at"],
last_error=row["error_message"],
last_response_status=result_metadata.get("last_status_code"),
last_response_body=result_metadata.get("last_response_body"),
last_attempt_at=result_metadata.get("last_attempt_at"),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
class WebhookDeliveryListResponse(BaseModel):
"""Response model for listing webhook deliveries."""
items: list[WebhookDeliveryResponse]
next_cursor: str | None = None
def create_app(
memory: MemoryEngine,
initialize_memory: bool = True,
@ -1726,7 +1843,6 @@ def create_app(
worker_id=worker_id,
executor=memory.execute_task,
poll_interval_ms=config.worker_poll_interval_ms,
max_retries=config.worker_max_retries,
schema=schema,
tenant_extension=memory._tenant_extension,
max_slots=config.worker_max_slots,
@ -3756,6 +3872,318 @@ def _register_routes(app: FastAPI):
logger.error(f"Error in POST /v1/default/banks/{bank_id}/consolidate: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
# =========================================================================
# Webhook Endpoints
# =========================================================================
@app.post(
"/v1/default/banks/{bank_id}/webhooks",
response_model=WebhookResponse,
summary="Register webhook",
description="Register a webhook endpoint to receive event notifications for this bank.",
operation_id="create_webhook",
tags=["Webhooks"],
status_code=201,
)
async def api_create_webhook(
bank_id: str,
request: CreateWebhookRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""Register a webhook for a bank."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
webhook_id = uuid.uuid4()
now = datetime.utcnow().isoformat() + "Z"
row = await pool.fetchrow(
f"""
INSERT INTO {fq_table("webhooks")}
(id, bank_id, url, secret, event_types, enabled, http_config, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, NOW(), NOW())
RETURNING id, bank_id, url, secret, event_types, enabled,
http_config::text, created_at::text, updated_at::text
""",
webhook_id,
bank_id,
request.url,
request.secret,
request.event_types,
request.enabled,
request.http_config.model_dump_json(),
)
return WebhookResponse(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=None, # Never return secret in responses
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=WebhookHttpConfig.model_validate_json(row["http_config"])
if row["http_config"]
else WebhookHttpConfig(),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in POST /v1/default/banks/{bank_id}/webhooks: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/banks/{bank_id}/webhooks",
response_model=WebhookListResponse,
summary="List webhooks",
description="List all webhooks registered for a bank.",
operation_id="list_webhooks",
tags=["Webhooks"],
)
async def api_list_webhooks(
bank_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""List webhooks for a bank."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
rows = await pool.fetch(
f"""
SELECT id, bank_id, url, secret, event_types, enabled,
http_config::text, created_at::text, updated_at::text
FROM {fq_table("webhooks")}
WHERE bank_id = $1
ORDER BY created_at
""",
bank_id,
)
return WebhookListResponse(
items=[
WebhookResponse(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=None, # Never return secret in responses
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=WebhookHttpConfig.model_validate_json(row["http_config"])
if row["http_config"]
else WebhookHttpConfig(),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
for row in rows
]
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in GET /v1/default/banks/{bank_id}/webhooks: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
response_model=DeleteResponse,
summary="Delete webhook",
description="Remove a registered webhook.",
operation_id="delete_webhook",
tags=["Webhooks"],
)
async def api_delete_webhook(
bank_id: str,
webhook_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""Delete a webhook."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
result = await pool.execute(
f"DELETE FROM {fq_table('webhooks')} WHERE id = $1 AND bank_id = $2",
uuid.UUID(webhook_id),
bank_id,
)
deleted = int(result.split()[-1]) if result else 0
if deleted == 0:
raise HTTPException(status_code=404, detail="Webhook not found")
return DeleteResponse(success=True)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/webhooks/{webhook_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.patch(
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
response_model=WebhookResponse,
summary="Update webhook",
description="Update one or more fields of a registered webhook. Only provided fields are changed.",
operation_id="update_webhook",
tags=["Webhooks"],
)
async def api_update_webhook(
bank_id: str,
webhook_id: str,
request: UpdateWebhookRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""Update a webhook's fields (PATCH semantics — only sent fields are updated)."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
set_clauses: list[str] = []
params: list = [uuid.UUID(webhook_id), bank_id]
fields = request.model_fields_set
if "url" in fields:
params.append(request.url)
set_clauses.append(f"url = ${len(params)}")
if "secret" in fields:
params.append(request.secret)
set_clauses.append(f"secret = ${len(params)}")
if "event_types" in fields:
params.append(request.event_types)
set_clauses.append(f"event_types = ${len(params)}")
if "enabled" in fields:
params.append(request.enabled)
set_clauses.append(f"enabled = ${len(params)}")
if "http_config" in fields:
params.append(request.http_config.model_dump_json())
set_clauses.append(f"http_config = ${len(params)}::jsonb")
if not set_clauses:
raise HTTPException(status_code=422, detail="No fields provided to update")
set_clauses.append("updated_at = NOW()")
row = await pool.fetchrow(
f"""
UPDATE {fq_table("webhooks")}
SET {", ".join(set_clauses)}
WHERE id = $1 AND bank_id = $2
RETURNING id, bank_id, url, secret, event_types, enabled,
http_config::text, created_at::text, updated_at::text
""",
*params,
)
if not row:
raise HTTPException(status_code=404, detail="Webhook not found")
return WebhookResponse(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=None,
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=WebhookHttpConfig.model_validate_json(row["http_config"])
if row["http_config"]
else WebhookHttpConfig(),
created_at=row["created_at"],
updated_at=row["updated_at"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/webhooks/{webhook_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries",
response_model=WebhookDeliveryListResponse,
summary="List webhook deliveries",
description="Inspect delivery history for a webhook (useful for debugging).",
operation_id="list_webhook_deliveries",
tags=["Webhooks"],
)
async def api_list_webhook_deliveries(
bank_id: str,
webhook_id: str,
limit: int = Query(default=50, le=200, description="Maximum number of deliveries to return"),
cursor: str | None = Query(default=None, description="Pagination cursor (created_at of last item)"),
request_context: RequestContext = Depends(get_request_context),
):
"""List deliveries for a specific webhook, newest first. Use next_cursor for pagination."""
try:
pool = await app.state.memory._get_pool()
from hindsight_api.engine.memory_engine import fq_table
# Verify webhook belongs to this bank
webhook_row = await pool.fetchrow(
f"SELECT id FROM {fq_table('webhooks')} WHERE id = $1 AND bank_id = $2",
uuid.UUID(webhook_id),
bank_id,
)
if not webhook_row:
raise HTTPException(status_code=404, detail="Webhook not found")
# Fetch limit+1 to detect if there's a next page
fetch_limit = limit + 1
if cursor:
rows = await pool.fetch(
f"""
SELECT operation_id, status, retry_count, next_retry_at::text,
error_message, task_payload, result_metadata::text, created_at::text, updated_at::text
FROM {fq_table("async_operations")}
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'webhook_id' = $2
AND created_at < $3::timestamptz
ORDER BY created_at DESC
LIMIT $4
""",
bank_id,
webhook_id,
cursor,
fetch_limit,
)
else:
rows = await pool.fetch(
f"""
SELECT operation_id, status, retry_count, next_retry_at::text,
error_message, task_payload, result_metadata::text, created_at::text, updated_at::text
FROM {fq_table("async_operations")}
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'webhook_id' = $2
ORDER BY created_at DESC
LIMIT $3
""",
bank_id,
webhook_id,
fetch_limit,
)
has_more = len(rows) > limit
page = rows[:limit]
next_cursor = page[-1]["created_at"] if has_more and page else None
return WebhookDeliveryListResponse(
items=[WebhookDeliveryResponse.from_async_operation_row(dict(row)) for row in page],
next_cursor=next_cursor,
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in GET /v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/v1/default/banks/{bank_id}/memories",
response_model=RetainResponse,
@ -3848,6 +4276,12 @@ def _register_routes(app: FastAPI):
document_tags=request.document_tags,
request_context=request_context,
return_usage=True,
outbox_callback=app.state.memory._build_retain_outbox_callback(
bank_id=bank_id,
contents=contents,
operation_id=None,
schema=request_context.tenant_id,
),
)
return RetainResponse.model_validate(

View file

@ -294,6 +294,12 @@ ENV_CONSOLIDATION_LLM_BATCH_SIZE = "HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE"
ENV_CONSOLIDATION_MAX_TOKENS = "HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS"
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
# Webhook configuration (global, static - server-level only)
ENV_WEBHOOK_URL = "HINDSIGHT_API_WEBHOOK_URL"
ENV_WEBHOOK_SECRET = "HINDSIGHT_API_WEBHOOK_SECRET"
ENV_WEBHOOK_EVENT_TYPES = "HINDSIGHT_API_WEBHOOK_EVENT_TYPES"
ENV_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS = "HINDSIGHT_API_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS"
# Optimization flags
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
@ -497,6 +503,12 @@ Use this tool PROACTIVELY to:
# Default embedding dimension (used by initial migration, adjusted at runtime)
EMBEDDING_DIMENSION = DEFAULT_EMBEDDING_DIMENSION
# Webhook configuration defaults
DEFAULT_WEBHOOK_URL = None # None = no global webhook configured
DEFAULT_WEBHOOK_SECRET = None # None = no signing
DEFAULT_WEBHOOK_EVENT_TYPES = "consolidation.completed" # Comma-separated; default = all supported events
DEFAULT_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS = 30 # How often to poll for pending deliveries
class JsonFormatter(logging.Formatter):
"""JSON formatter for structured logging.
@ -750,6 +762,12 @@ class HindsightConfig:
otel_service_name: str
otel_deployment_environment: str
# Webhook configuration (static - server-level only, not per-bank)
webhook_url: str | None # Global webhook URL (None = disabled)
webhook_secret: str | None # HMAC signing secret (None = unsigned)
webhook_event_types: list[str] # Event types to deliver globally
webhook_delivery_poll_interval_seconds: int # How often the delivery worker polls
# Class-level sets for configuration categorization
# CREDENTIAL_FIELDS: Never exposed via API, never configurable per-tenant/bank
@ -1187,6 +1205,20 @@ class HindsightConfig:
otel_exporter_otlp_headers=os.getenv(ENV_OTEL_EXPORTER_OTLP_HEADERS) or None,
otel_service_name=os.getenv(ENV_OTEL_SERVICE_NAME, DEFAULT_OTEL_SERVICE_NAME),
otel_deployment_environment=os.getenv(ENV_OTEL_DEPLOYMENT_ENVIRONMENT, DEFAULT_OTEL_DEPLOYMENT_ENVIRONMENT),
# Webhook configuration (static, server-level only)
webhook_url=os.getenv(ENV_WEBHOOK_URL) or DEFAULT_WEBHOOK_URL,
webhook_secret=os.getenv(ENV_WEBHOOK_SECRET) or DEFAULT_WEBHOOK_SECRET,
webhook_event_types=[
t.strip()
for t in os.getenv(ENV_WEBHOOK_EVENT_TYPES, DEFAULT_WEBHOOK_EVENT_TYPES).split(",")
if t.strip()
],
webhook_delivery_poll_interval_seconds=int(
os.getenv(
ENV_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS,
str(DEFAULT_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS),
)
),
)
config.validate()
return config

View file

@ -180,11 +180,12 @@ async def run_consolidation_job(
perf.log(f"[1] Found {total_count} pending memories to consolidate")
# Process each memory with individual commits for crash recovery
stats = {
stats: dict[str, int] = {
"memories_processed": 0,
"observations_created": 0,
"observations_updated": 0,
"observations_merged": 0,
"observations_deleted": 0,
"actions_executed": 0,
"skipped": 0,
}
@ -273,11 +274,12 @@ async def run_consolidation_job(
# explicit list[list[str]]
obs_tags_list = _obs_parsed
batch_deleted: int = 0
if obs_tags_list:
# Multi-pass: run one observation consolidation pass per tag set
results = []
for obs_tags in obs_tags_list:
pass_results = await _process_memory_batch(
pass_results, pass_deleted = await _process_memory_batch(
conn=conn,
memory_engine=memory_engine,
llm_config=llm_config,
@ -288,6 +290,7 @@ async def run_consolidation_job(
config=config,
obs_tags_override=obs_tags,
)
batch_deleted += pass_deleted
# Merge results: prefer non-skipped actions
if not results:
results = pass_results
@ -315,7 +318,7 @@ async def run_consolidation_job(
}
else:
# Normal single pass using the memory's own tags
results = await _process_memory_batch(
results, batch_deleted = await _process_memory_batch(
conn=conn,
memory_engine=memory_engine,
llm_config=llm_config,
@ -325,6 +328,7 @@ async def run_consolidation_job(
perf=perf,
config=config,
)
stats["observations_deleted"] += batch_deleted
await conn.executemany(
f"UPDATE {fq_table('memory_units')} SET consolidated_at = NOW() WHERE id = $1",
@ -521,7 +525,7 @@ async def _process_memory_batch(
perf: ConsolidationPerfLog | None = None,
config: Any = None,
obs_tags_override: list[str] | None = None,
) -> list[dict[str, Any]]:
) -> tuple[list[dict[str, Any]], int]:
"""
Process a batch of memories in a single LLM call.
@ -656,6 +660,7 @@ async def _process_memory_batch(
for m in source_mems:
per_memory_updated.add(str(m["id"]))
deleted_count = 0
for delete in llm_result.deletes:
# Security: the observation must be present in the unioned recall
if not any(str(obs.id) == delete.observation_id for obs in union_observations):
@ -664,6 +669,7 @@ async def _process_memory_batch(
)
continue
await _execute_delete_action(conn=conn, bank_id=bank_id, observation_id=delete.observation_id)
deleted_count += 1
# Build per-memory result dicts for the stats tracker in the outer loop
results: list[dict[str, Any]] = []
@ -680,7 +686,7 @@ async def _process_memory_batch(
else:
results.append({"action": "skipped", "reason": "no_durable_knowledge"})
return results
return results, deleted_count
def _min_date(dates: "Any") -> "datetime | None":

View file

@ -15,15 +15,19 @@ import json
import logging
import time
import uuid
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any
import asyncpg
import httpx
import tiktoken
from ..config import get_config
from ..metrics import get_metrics_collector
from ..tracing import create_operation_span
from ..utils import mask_network_location
from ..worker.exceptions import RetryTaskAt
from .db_budget import budgeted_operation
from .operation_metadata import (
BatchRetainChildMetadata,
@ -359,6 +363,10 @@ class MemoryEngine(MemoryEngineInterface):
self._run_migrations = run_migrations
self._retain_entity_lookup = config.retain_entity_lookup
# Webhook manager (will be created in initialize() after pool is ready)
self._webhook_manager = None
self._http_client: httpx.AsyncClient | None = None
# Initialize entity resolver (will be created in initialize())
self.entity_resolver = None
@ -582,6 +590,12 @@ class MemoryEngine(MemoryEngineInterface):
document_tags=document_tags,
request_context=context,
operation_id=operation_id,
outbox_callback=self._build_retain_outbox_callback(
bank_id=bank_id,
contents=contents,
operation_id=operation_id,
schema=context.tenant_id,
),
)
# If this retain was triggered by file conversion, update document with file metadata
@ -778,6 +792,7 @@ class MemoryEngine(MemoryEngineInterface):
)
logger.info(f"[CONSOLIDATION] bank={bank_id} completed: {result.get('memories_processed', 0)} processed")
return result
async def _handle_refresh_mental_model(self, task_dict: dict[str, Any]):
"""
@ -949,15 +964,18 @@ class MemoryEngine(MemoryEngineInterface):
logger.error(f"Failed to check operation status {operation_id}: {e}")
# Continue with processing if we can't check status
consolidation_result: dict | None = None
try:
if task_type == "batch_retain":
await self._handle_batch_retain(task_dict)
elif task_type == "file_convert_retain":
await self._handle_file_convert_retain(task_dict)
elif task_type == "consolidation":
await self._handle_consolidation(task_dict)
consolidation_result = await self._handle_consolidation(task_dict)
elif task_type == "refresh_mental_model":
await self._handle_refresh_mental_model(task_dict)
elif task_type == "webhook_delivery":
await self._handle_webhook_delivery(task_dict)
else:
logger.error(f"Unknown task type: {task_type}")
# Don't retry unknown task types
@ -967,9 +985,22 @@ class MemoryEngine(MemoryEngineInterface):
# Task succeeded - mark operation as completed
# file_convert_retain marks itself as completed in a transaction, skip double-marking
if operation_id and task_type != "file_convert_retain":
await self._mark_operation_completed(operation_id)
if operation_id and task_type not in ("file_convert_retain",):
if task_type == "consolidation":
# Atomically mark completed AND queue webhook delivery in one transaction
await self._mark_operation_completed_and_fire_webhook(
operation_id=operation_id,
bank_id=task_dict.get("bank_id", ""),
status="completed",
result=consolidation_result,
schema=schema,
)
else:
await self._mark_operation_completed(operation_id)
except RetryTaskAt:
# Task-owned retry: let the poller handle scheduling
raise
except Exception as e:
logger.error(f"Task execution failed: {task_type}, error: {e}")
import traceback
@ -984,10 +1015,193 @@ class MemoryEngine(MemoryEngineInterface):
if operation_id:
await self._mark_operation_failed(operation_id, str(e), error_traceback)
else:
# Retryable: re-raise so the worker poller handles retry/fail via _retry_or_fail,
# which correctly resets status='pending' and increments the DB retry_count.
if task_type == "consolidation" and operation_id:
# Fire failure webhook (non-transactional — operation not yet marked failed;
# poller will mark it failed after this raise)
await self._fire_consolidation_webhook(
bank_id=task_dict.get("bank_id", ""),
operation_id=operation_id,
status="failed",
result=None,
error_message=str(e),
schema=schema,
)
# Retryable: use RetryTaskAt if under the retry limit, else re-raise (poller marks failed)
retry_count = task_dict.get("_retry_count", 0)
if retry_count < 3:
raise RetryTaskAt(retry_at=datetime.now(UTC) + timedelta(seconds=60), message=str(e))
raise
async def _fire_consolidation_webhook(
self,
bank_id: str,
operation_id: str,
status: str,
result: dict | None,
error_message: str | None = None,
schema: str | None = None,
) -> None:
"""Fire a consolidation webhook event. Non-fatal - logs errors but does not raise."""
if not self._webhook_manager:
return
try:
from datetime import datetime, timezone
from ..webhooks.models import ConsolidationEventData, WebhookEvent, WebhookEventType
data = ConsolidationEventData(
observations_created=result.get("observations_created") if result else None,
observations_updated=result.get("observations_updated") if result else None,
observations_deleted=result.get("observations_deleted") if result else None,
error_message=error_message,
)
event = WebhookEvent(
event=WebhookEventType.CONSOLIDATION_COMPLETED,
bank_id=bank_id,
operation_id=operation_id,
status=status,
timestamp=datetime.now(timezone.utc),
data=data,
)
await self._webhook_manager.fire_event(event, schema=schema)
except Exception as e:
logger.error(f"Failed to fire consolidation webhook for operation {operation_id}: {e}")
def _build_retain_outbox_callback(
self,
bank_id: str,
contents: list[dict],
operation_id: str | None,
schema: str | None = None,
) -> "Callable[[asyncpg.Connection], Awaitable[None]] | None":
"""Build a transactional outbox callback for retain.completed webhook events.
Returns a coroutine function that queues one webhook delivery row per content
item using the provided connection (inside the retain transaction). Returns None
if no webhook manager is configured.
"""
webhook_manager = getattr(self, "_webhook_manager", None)
if not webhook_manager:
return None
from ..webhooks.models import RetainEventData, WebhookEvent, WebhookEventType
now = datetime.now(UTC)
op_id = operation_id or uuid.uuid4().hex
events = []
for content in contents:
doc_id = content.get("document_id")
tags = content.get("tags")
data = RetainEventData(
document_id=doc_id,
tags=tags if isinstance(tags, list) else None,
)
events.append(
WebhookEvent(
event=WebhookEventType.RETAIN_COMPLETED,
bank_id=bank_id,
operation_id=op_id,
status="completed",
timestamp=now,
data=data,
)
)
async def _callback(conn: asyncpg.Connection) -> None:
for event in events:
await webhook_manager.fire_event_with_conn(event, conn, schema=schema)
return _callback
async def _update_webhook_delivery_metadata(
self, operation_id: str, status_code: int | None, response_body: str | None
) -> None:
"""Persist last HTTP attempt info into async_operations.result_metadata."""
try:
pool = await self._get_pool()
meta = json.dumps(
{
"last_status_code": status_code,
"last_response_body": (response_body or "")[:2048],
"last_attempt_at": datetime.now(UTC).isoformat(),
}
)
async with acquire_with_retry(pool) as conn:
await conn.execute(
f"UPDATE {fq_table('async_operations')} SET result_metadata = $2::jsonb, updated_at = now() WHERE operation_id = $1",
uuid.UUID(operation_id),
meta,
)
except Exception as meta_err:
logger.debug(f"Failed to update webhook delivery metadata: {meta_err}")
async def _handle_webhook_delivery(self, task_dict: dict[str, Any]) -> None:
"""Deliver a webhook event via HTTP.
Raises RetryTaskAt to schedule a retry on failure (up to MAX_ATTEMPTS).
Raises the original exception when retries are exhausted (poller marks failed).
Response status code and body are stored in result_metadata for debugging.
"""
from ..webhooks.manager import MAX_ATTEMPTS, RETRY_DELAYS
from ..webhooks.models import WebhookHttpConfig
url = task_dict["url"]
secret = task_dict.get("secret")
event_type = task_dict["event_type"]
raw_payload = task_dict["payload"]
retry_count = task_dict.get("_retry_count", 0)
operation_id: str | None = task_dict.get("_operation_id")
http_config = WebhookHttpConfig.model_validate(task_dict.get("http_config") or {})
if isinstance(raw_payload, dict):
payload_bytes = json.dumps(raw_payload).encode()
else:
payload_bytes = str(raw_payload).encode()
headers: dict[str, str] = {
"Content-Type": "application/json",
"X-Hindsight-Event": event_type,
**http_config.headers,
}
if secret and self._webhook_manager:
headers["X-Hindsight-Signature"] = self._webhook_manager._sign_payload(secret, payload_bytes)
if self._http_client is None:
raise RuntimeError("HTTP client not initialized")
response = None
try:
request_kwargs: dict[str, Any] = {
"headers": headers,
"params": http_config.params if http_config.params else None,
"timeout": http_config.timeout_seconds,
}
if http_config.method.upper() == "GET":
response = await self._http_client.get(url, **request_kwargs)
else:
response = await self._http_client.post(url, content=payload_bytes, **request_kwargs)
response.raise_for_status()
if operation_id:
await self._update_webhook_delivery_metadata(operation_id, response.status_code, response.text)
except Exception as e:
status_code = response.status_code if response is not None else None
response_body = response.text if response is not None else None
if operation_id:
await self._update_webhook_delivery_metadata(operation_id, status_code, response_body)
if retry_count >= MAX_ATTEMPTS - 1:
logger.error(
f"webhook_delivery permanently_failed url={url} attempts={retry_count + 1} "
f"status_code={status_code} error={e}"
)
raise
delay = RETRY_DELAYS[retry_count] if retry_count < len(RETRY_DELAYS) else RETRY_DELAYS[-1]
retry_at = datetime.now(UTC) + timedelta(seconds=delay)
logger.warning(
f"webhook_delivery failed url={url} attempt={retry_count + 1}/{MAX_ATTEMPTS} "
f"status_code={status_code} retry_in={delay}s error={e}"
)
raise RetryTaskAt(retry_at=retry_at, message=str(e))
async def _delete_operation_record(self, operation_id: str):
"""Helper to delete an operation record from the database."""
try:
@ -1058,6 +1272,58 @@ class MemoryEngine(MemoryEngineInterface):
except Exception as e:
logger.error(f"Failed to mark operation as completed {operation_id}: {e}")
async def _mark_operation_completed_and_fire_webhook(
self,
operation_id: str,
bank_id: str,
status: str,
result: dict | None,
schema: str | None = None,
error_message: str | None = None,
) -> None:
"""Mark an operation as completed and queue webhook deliveries in a single transaction.
Uses the transactional outbox pattern: the webhook delivery row is inserted in the
same database transaction as the status update. This guarantees at-least-once delivery
even if the process crashes immediately after committing.
"""
from ..webhooks.models import ConsolidationEventData, WebhookEvent, WebhookEventType
try:
pool = await self._get_pool()
async with acquire_with_retry(pool) as conn:
async with conn.transaction():
await conn.execute(
f"""
UPDATE {fq_table("async_operations")}
SET status = 'completed', updated_at = NOW(), completed_at = NOW()
WHERE operation_id = $1
""",
uuid.UUID(operation_id),
)
logger.info(f"Marked async operation as completed: {operation_id}")
await self._maybe_update_parent_operation(operation_id, conn)
# Queue webhook deliveries inside the same transaction
if self._webhook_manager:
data = ConsolidationEventData(
observations_created=result.get("observations_created") if result else None,
observations_updated=result.get("observations_updated") if result else None,
observations_deleted=result.get("observations_deleted") if result else None,
error_message=error_message,
)
event = WebhookEvent(
event=WebhookEventType.CONSOLIDATION_COMPLETED,
bank_id=bank_id,
operation_id=operation_id,
status=status,
timestamp=datetime.now(UTC),
data=data,
)
await self._webhook_manager.fire_event_with_conn(event, conn, schema=schema)
except Exception as e:
logger.error(f"Failed to mark operation completed and fire webhook {operation_id}: {e}")
async def _maybe_update_parent_operation(self, child_operation_id: str, conn):
"""Check if this is a child operation and update parent status if all siblings are done.
@ -1381,6 +1647,32 @@ class MemoryEngine(MemoryEngineInterface):
else:
logger.debug("Iris parser not registered (VECTORIZE_TOKEN or VECTORIZE_ORG_ID not set)")
# Initialize webhook manager
from ..webhooks import WebhookManager
from ..webhooks.models import WebhookConfig
webhook_global: list[WebhookConfig] = []
if config.webhook_url:
webhook_global = [
WebhookConfig(
id="", # No DB row for env-configured global webhook
bank_id=None,
url=config.webhook_url,
secret=config.webhook_secret,
event_types=config.webhook_event_types,
enabled=True,
)
]
self._webhook_manager = WebhookManager(
pool=self._pool,
global_webhooks=webhook_global,
tenant_extension=self._tenant_extension,
)
logger.debug("Webhook manager initialized")
# Long-lived HTTP client for webhook delivery tasks
self._http_client = httpx.AsyncClient(timeout=30.0)
# Set executor for task backend and initialize
self._task_backend.set_executor(self.execute_task)
await self._task_backend.initialize()
@ -1440,6 +1732,11 @@ class MemoryEngine(MemoryEngineInterface):
# Shutdown task backend
await self._task_backend.shutdown()
# Close HTTP client used for webhook delivery
if self._http_client is not None:
await self._http_client.aclose()
self._http_client = None
# Close pool
if self._pool is not None:
self._pool.terminate()
@ -1580,6 +1877,7 @@ class MemoryEngine(MemoryEngineInterface):
document_tags: list[str] | None = None,
return_usage: bool = False,
operation_id: str | None = None,
outbox_callback: "Callable[[asyncpg.Connection], Awaitable[None]] | None" = None,
):
"""
Store multiple content items as memory units in ONE batch operation.
@ -1728,6 +2026,9 @@ class MemoryEngine(MemoryEngineInterface):
confidence_score=confidence_score,
document_tags=document_tags,
operation_id=operation_id,
# Outbox callback runs inside the last sub-batch's transaction so the
# webhook delivery row is committed atomically with the final retain data.
outbox_callback=outbox_callback if i == len(sub_batches) else None,
)
all_results.extend(sub_results)
total_usage = total_usage + sub_usage
@ -1749,6 +2050,7 @@ class MemoryEngine(MemoryEngineInterface):
confidence_score=confidence_score,
document_tags=document_tags,
operation_id=operation_id,
outbox_callback=outbox_callback,
)
# Call post-operation hook if validator is configured
@ -1799,6 +2101,7 @@ class MemoryEngine(MemoryEngineInterface):
confidence_score: float | None = None,
document_tags: list[str] | None = None,
operation_id: str | None = None,
outbox_callback: "Callable[[asyncpg.Connection], Awaitable[None]] | None" = None,
) -> tuple[list[list[str]], "TokenUsage"]:
"""
Internal method for batch processing without chunking logic.
@ -1849,6 +2152,7 @@ class MemoryEngine(MemoryEngineInterface):
config=resolved_config,
operation_id=operation_id,
schema=request_context.tenant_id if request_context else None,
outbox_callback=outbox_callback,
)
def recall(

View file

@ -7,6 +7,7 @@ Coordinates all retain pipeline modules to store memories efficiently.
import logging
import time
import uuid
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
from typing import Any
@ -52,6 +53,8 @@ def parse_datetime_flexible(value: Any) -> datetime:
raise TypeError(f"Expected datetime or string, got {type(value).__name__}")
import asyncpg
from ..response_models import TokenUsage
from . import (
chunk_storage,
@ -82,6 +85,7 @@ async def retain_batch(
document_tags: list[str] | None = None,
operation_id: str | None = None,
schema: str | None = None,
outbox_callback: Callable[["asyncpg.Connection"], Awaitable[None]] | None = None,
) -> tuple[list[list[str]], TokenUsage]:
"""
Process a batch of content through the retain pipeline.
@ -484,6 +488,11 @@ async def retain_batch(
# Map results back to original content items
result_unit_ids = _map_results_to_contents(contents, extracted_facts, unit_ids)
# Transactional outbox: queue any side-effect tasks (e.g. webhook deliveries)
# inside the same transaction so they are atomically committed with the retain data.
if outbox_callback:
await outbox_callback(conn)
# Flush entity stats (mention_count / last_seen) now that the transaction
# has committed. Uses a fresh pool connection — no locks held.
await entity_resolver.flush_pending_stats()

View file

@ -307,6 +307,10 @@ def main():
otel_exporter_otlp_headers=config.otel_exporter_otlp_headers,
otel_service_name=config.otel_service_name,
otel_deployment_environment=config.otel_deployment_environment,
webhook_url=config.webhook_url,
webhook_secret=config.webhook_secret,
webhook_event_types=config.webhook_event_types,
webhook_delivery_poll_interval_seconds=config.webhook_delivery_poll_interval_seconds,
)
config.configure_logging()
if not args.daemon:

View file

@ -0,0 +1,13 @@
"""Webhook system for Hindsight API event notifications."""
from .manager import WebhookManager
from .models import ConsolidationEventData, RetainEventData, WebhookConfig, WebhookEvent, WebhookEventType
__all__ = [
"WebhookManager",
"WebhookConfig",
"WebhookEvent",
"WebhookEventType",
"ConsolidationEventData",
"RetainEventData",
]

View file

@ -0,0 +1,238 @@
"""Webhook manager for delivering event notifications."""
import hashlib
import hmac
import json
import logging
import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING
import asyncpg
from .models import WebhookConfig, WebhookEvent, WebhookHttpConfig
if TYPE_CHECKING:
from hindsight_api.extensions.tenant import TenantExtension
logger = logging.getLogger(__name__)
# Retry delay schedule in seconds: 5 retries after the first attempt.
# Fast early retries catch transient failures; later retries handle longer outages.
RETRY_DELAYS = [5, 300, 1800, 7200, 18000]
MAX_ATTEMPTS = len(RETRY_DELAYS) + 1 # first attempt + len(RETRY_DELAYS) retries
def _fq_table(table: str, schema: str | None = None) -> str:
"""Get fully-qualified table name with optional schema prefix."""
if schema:
return f'"{schema}".{table}'
return table
def _parse_http_config(value: str | dict | None) -> WebhookHttpConfig:
"""Parse http_config column value (JSONB returned as text or dict) into a model."""
if value is None:
return WebhookHttpConfig()
if isinstance(value, str):
return WebhookHttpConfig.model_validate_json(value)
return WebhookHttpConfig.model_validate(value)
class WebhookManager:
"""
Manages webhook registration and event firing.
Supports both global webhooks (configured via env vars) and per-bank
webhooks stored in the database. Deliveries are queued as async_operations
tasks (operation_type='webhook_delivery') and picked up by the worker poller.
"""
def __init__(
self,
pool: asyncpg.Pool,
global_webhooks: list[WebhookConfig],
tenant_extension: "TenantExtension | None" = None,
):
self._pool = pool
self._global_webhooks = global_webhooks
self._tenant_extension = tenant_extension
def _sign_payload(self, secret: str, payload_bytes: bytes) -> str:
"""Compute HMAC-SHA256 signature for a payload."""
return "sha256=" + hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
async def fire_event(self, event: WebhookEvent, schema: str | None = None) -> None:
"""
Queue webhook deliveries for an event as async_operations tasks.
Loads per-bank and global webhooks, inserts pending webhook_delivery tasks for
any webhook whose event_types list matches the fired event type. The worker
poller picks these up and calls MemoryEngine._handle_webhook_delivery().
Args:
event: The event to deliver.
schema: Database schema (for multi-tenant). None = default schema.
"""
webhook_table = _fq_table("webhooks", schema)
ops_table = _fq_table("async_operations", schema)
now = datetime.now(timezone.utc)
payload_str = event.model_dump_json()
try:
# Load per-bank webhooks from DB (bank-specific + global NULL rows)
rows = await self._pool.fetch(
f"""
SELECT id, bank_id, url, secret, event_types, enabled, http_config::text
FROM {webhook_table}
WHERE (bank_id = $1 OR bank_id IS NULL) AND enabled = true
""",
event.bank_id,
)
db_webhooks = [
WebhookConfig(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=row["secret"],
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=_parse_http_config(row["http_config"]),
)
for row in rows
]
# Merge with global webhooks from env config
all_webhooks = self._global_webhooks + db_webhooks
matched = 0
for webhook in all_webhooks:
if not webhook.enabled:
continue
if event.event.value not in webhook.event_types:
continue
operation_id = uuid.uuid4()
webhook_id = webhook.id if webhook.id else None
task_payload = json.dumps(
{
"type": "webhook_delivery",
"operation_id": str(operation_id),
"bank_id": event.bank_id,
"url": webhook.url,
"secret": webhook.secret,
"event_type": event.event.value,
"payload": payload_str,
"webhook_id": webhook_id,
"http_config": webhook.http_config.model_dump(),
}
)
await self._pool.execute(
f"""
INSERT INTO {ops_table}
(operation_id, bank_id, operation_type, status, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'pending', $3::jsonb, '{{}}'::jsonb, $4, $4)
""",
operation_id,
event.bank_id,
task_payload,
now,
)
matched += 1
logger.debug(f"Fired webhook event {event.event} for bank {event.bank_id}: {matched} delivery(ies) queued")
except Exception as e:
logger.error(f"Failed to queue webhook deliveries for event {event.event}: {e}")
async def fire_event_with_conn(
self, event: WebhookEvent, conn: asyncpg.Connection, schema: str | None = None
) -> None:
"""
Queue webhook deliveries within an existing database connection/transaction.
Identical to fire_event() but uses the provided connection instead of acquiring
one from the pool. Use this to atomically insert delivery tasks in the same
transaction as the primary operation (transactional outbox pattern).
Args:
event: The event to deliver.
conn: Existing asyncpg connection (may be inside an active transaction).
schema: Database schema (for multi-tenant). None = default schema.
"""
webhook_table = _fq_table("webhooks", schema)
ops_table = _fq_table("async_operations", schema)
now = datetime.now(timezone.utc)
payload_str = event.model_dump_json()
try:
rows = await conn.fetch(
f"""
SELECT id, bank_id, url, secret, event_types, enabled, http_config::text
FROM {webhook_table}
WHERE (bank_id = $1 OR bank_id IS NULL) AND enabled = true
""",
event.bank_id,
)
db_webhooks = [
WebhookConfig(
id=str(row["id"]),
bank_id=row["bank_id"],
url=row["url"],
secret=row["secret"],
event_types=list(row["event_types"]) if row["event_types"] else [],
enabled=row["enabled"],
http_config=_parse_http_config(row["http_config"]),
)
for row in rows
]
all_webhooks = self._global_webhooks + db_webhooks
matched = 0
for webhook in all_webhooks:
if not webhook.enabled:
continue
if event.event.value not in webhook.event_types:
continue
operation_id = uuid.uuid4()
webhook_id = webhook.id if webhook.id else None
task_payload = json.dumps(
{
"type": "webhook_delivery",
"operation_id": str(operation_id),
"bank_id": event.bank_id,
"url": webhook.url,
"secret": webhook.secret,
"event_type": event.event.value,
"payload": payload_str,
"webhook_id": webhook_id,
"http_config": webhook.http_config.model_dump(),
}
)
await conn.execute(
f"""
INSERT INTO {ops_table}
(operation_id, bank_id, operation_type, status, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'pending', $3::jsonb, '{{}}'::jsonb, $4, $4)
""",
operation_id,
event.bank_id,
task_payload,
now,
)
matched += 1
logger.debug(
f"Fired webhook event {event.event} for bank {event.bank_id}: {matched} delivery(ies) queued (in-transaction)"
)
except Exception as e:
logger.error(f"Failed to queue webhook deliveries (in-transaction) for event {event.event}: {e}")

View file

@ -0,0 +1,51 @@
"""Pydantic models for the webhook system."""
from datetime import datetime
from enum import StrEnum
from pydantic import BaseModel, Field
class WebhookEventType(StrEnum):
CONSOLIDATION_COMPLETED = "consolidation.completed"
RETAIN_COMPLETED = "retain.completed"
class ConsolidationEventData(BaseModel):
observations_created: int | None = None
observations_updated: int | None = None
observations_deleted: int | None = None
error_message: str | None = None
class RetainEventData(BaseModel):
document_id: str | None = None
tags: list[str] | None = None
class WebhookEvent(BaseModel):
event: WebhookEventType
bank_id: str
operation_id: str
status: str # "completed" or "failed"
timestamp: datetime
data: ConsolidationEventData | RetainEventData
class WebhookHttpConfig(BaseModel):
"""HTTP delivery configuration for a webhook."""
method: str = Field(default="POST", description="HTTP method: GET or POST")
timeout_seconds: int = Field(default=30, description="HTTP request timeout in seconds")
headers: dict[str, str] = Field(default_factory=dict, description="Custom HTTP headers")
params: dict[str, str] = Field(default_factory=dict, description="Custom HTTP query parameters")
class WebhookConfig(BaseModel):
id: str
bank_id: str | None
url: str
secret: str | None
event_types: list[str]
enabled: bool
http_config: WebhookHttpConfig = Field(default_factory=WebhookHttpConfig)

View file

@ -0,0 +1,9 @@
from datetime import datetime
class RetryTaskAt(Exception):
"""Raise from a task handler to schedule a retry at a specific time."""
def __init__(self, retry_at: datetime, message: str = ""):
self.retry_at = retry_at
super().__init__(message)

View file

@ -219,7 +219,6 @@ def main():
worker_id=args.worker_id,
executor=memory.execute_task,
poll_interval_ms=args.poll_interval,
max_retries=args.max_retries,
schema=schema,
tenant_extension=tenant_extension,
max_slots=config.worker_max_slots,

View file

@ -14,6 +14,8 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from .exceptions import RetryTaskAt
if TYPE_CHECKING:
import asyncpg
@ -57,7 +59,6 @@ class WorkerPoller:
worker_id: str,
executor: Callable[[dict[str, Any]], Awaitable[None]],
poll_interval_ms: int = 500,
max_retries: int = 3,
schema: str | None = None,
tenant_extension: "TenantExtension | None" = None,
max_slots: int = 10,
@ -71,7 +72,6 @@ class WorkerPoller:
worker_id: Unique identifier for this worker
executor: Async function to execute tasks (typically MemoryEngine.execute_task)
poll_interval_ms: Interval between polls when no tasks found (milliseconds)
max_retries: Maximum retry attempts before marking task as failed
schema: Database schema for single-tenant support (deprecated, use tenant_extension)
tenant_extension: Extension for dynamic multi-tenant discovery. If None, creates a
DefaultTenantExtension with the configured schema.
@ -82,7 +82,6 @@ class WorkerPoller:
self._worker_id = worker_id
self._executor = executor
self._poll_interval_ms = poll_interval_ms
self._max_retries = max_retries
self._schema = schema
# Always set tenant extension (use DefaultTenantExtension if none provided)
if tenant_extension is None:
@ -218,11 +217,12 @@ class WorkerPoller:
# 1. Claim non-consolidation tasks (up to limit)
non_consolidation_rows = await conn.fetch(
f"""
SELECT operation_id, task_payload
SELECT operation_id, task_payload, retry_count
FROM {table}
WHERE status = 'pending'
AND task_payload IS NOT NULL
AND operation_type != 'consolidation'
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
ORDER BY created_at
LIMIT $1
FOR UPDATE SKIP LOCKED
@ -238,11 +238,12 @@ class WorkerPoller:
if consolidation_limit > 0 and remaining_limit > 0:
consolidation_rows = await conn.fetch(
f"""
SELECT operation_id, task_payload
SELECT operation_id, task_payload, retry_count
FROM {table} AS pending
WHERE status = 'pending'
AND task_payload IS NOT NULL
AND operation_type = 'consolidation'
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
AND NOT EXISTS (
SELECT 1 FROM {table} AS processing
WHERE processing.bank_id = pending.bank_id
@ -274,14 +275,19 @@ class WorkerPoller:
)
# Parse and return task payloads with schema context
return [
ClaimedTask(
operation_id=str(row["operation_id"]),
task_dict=json.loads(row["task_payload"]),
schema=schema,
result = []
for row in all_rows:
task_dict = json.loads(row["task_payload"])
task_dict["_retry_count"] = row["retry_count"]
task_dict["_operation_id"] = str(row["operation_id"])
result.append(
ClaimedTask(
operation_id=str(row["operation_id"]),
task_dict=task_dict,
schema=schema,
)
)
for row in all_rows
]
return result
async def _mark_completed(self, operation_id: str, schema: str | None):
"""Mark a task as completed."""
@ -310,40 +316,22 @@ class WorkerPoller:
error_message,
)
async def _retry_or_fail(self, operation_id: str, error_message: str, schema: str | None):
"""Increment retry count or mark as failed if max retries exceeded."""
async def _schedule_retry(self, operation_id: str, retry_at: "Any", error_message: str, schema: str | None):
"""Reset task to pending with a future retry timestamp."""
table = fq_table("async_operations", schema)
# Get current retry count
row = await self._pool.fetchrow(
f"SELECT retry_count FROM {table} WHERE operation_id = $1",
error_message = error_message[:5000] if len(error_message) > 5000 else error_message
await self._pool.execute(
f"""
UPDATE {table}
SET status = 'pending', next_retry_at = $2, worker_id = NULL, claimed_at = NULL,
retry_count = retry_count + 1, error_message = $3, updated_at = now()
WHERE operation_id = $1
""",
operation_id,
retry_at,
error_message,
)
if row is None:
logger.warning(f"Operation {operation_id} not found, cannot retry")
return
retry_count = row["retry_count"]
if retry_count >= self._max_retries:
# Max retries exceeded, mark as failed
await self._mark_failed(
operation_id, f"Max retries ({self._max_retries}) exceeded. Last error: {error_message}", schema
)
logger.error(f"Task {operation_id} failed after {retry_count} retries")
else:
# Increment retry and reset to pending
await self._pool.execute(
f"""
UPDATE {table}
SET status = 'pending', worker_id = NULL, claimed_at = NULL,
retry_count = retry_count + 1, updated_at = now()
WHERE operation_id = $1
""",
operation_id,
)
logger.warning(f"Task {operation_id} failed, will retry (attempt {retry_count + 1}/{self._max_retries})")
logger.warning(f"Task {operation_id} scheduled for retry at {retry_at}: {error_message}")
async def execute_task(self, task: ClaimedTask):
"""Execute a single task as a background job (fire-and-forget)."""
@ -378,11 +366,10 @@ class WorkerPoller:
async def _execute_task_inner(self, task: ClaimedTask):
"""Inner task execution with retry/fail handling.
Retryable task failures are re-raised by the executor (MemoryEngine.execute_task)
and handled here via _retry_or_fail, which resets status='pending' (or marks as
'failed' after max retries). Non-retryable failures (e.g., file_convert_retain) are
handled by the executor internally it marks the operation as failed and returns
normally, so no exception reaches here.
Tasks that want to be retried raise RetryTaskAt; the poller sets next_retry_at
and resets status to 'pending'. All other exceptions are marked as failed immediately.
Non-retryable failures (e.g., file_convert_retain) are handled by the executor
internally it marks the operation as failed and returns normally.
"""
task_type = task.task_dict.get("type", "unknown")
bank_id = task.task_dict.get("bank_id", "unknown")
@ -394,10 +381,12 @@ class WorkerPoller:
task.task_dict["_schema"] = task.schema
await self._executor(task.task_dict)
logger.debug(f"Task {task.operation_id} execution finished")
except RetryTaskAt as e:
await self._schedule_retry(task.operation_id, e.retry_at, str(e), task.schema)
except Exception as e:
logger.error(f"Task {task.operation_id} failed: {e}")
traceback.print_exc()
await self._retry_or_fail(task.operation_id, str(e), task.schema)
await self._mark_failed(task.operation_id, str(e), task.schema)
async def recover_own_tasks(self) -> int:
"""

View file

@ -413,7 +413,6 @@ async def test_worker_batch_recovery(memory, request_context):
worker_id="test_worker_recovery",
executor=memory,
poll_interval_ms=100,
max_retries=3,
schema=schema,
tenant_extension=tenant_extension,
max_slots=5,

View file

@ -0,0 +1,780 @@
"""Tests for the webhook system.
Covers:
- Unit tests for HMAC signing and retry constants (no DB required)
- Integration tests for fire_event() using a real DB (inserts into async_operations)
- Integration tests for _handle_webhook_delivery() on the memory engine
- HTTP API integration tests for CRUD and delivery listing endpoints
"""
import json
import uuid
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
import pytest_asyncio
from hindsight_api.api import create_app
from hindsight_api.engine.memory_engine import MemoryEngine
from hindsight_api.webhooks.manager import MAX_ATTEMPTS, RETRY_DELAYS, WebhookManager
from hindsight_api.webhooks.models import (
ConsolidationEventData,
RetainEventData,
WebhookConfig,
WebhookEvent,
WebhookEventType,
)
from hindsight_api.worker.exceptions import RetryTaskAt
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_event(bank_id: str = "bank-1") -> WebhookEvent:
return WebhookEvent(
event=WebhookEventType.CONSOLIDATION_COMPLETED,
bank_id=bank_id,
operation_id=uuid.uuid4().hex,
status="completed",
timestamp=datetime.now(timezone.utc),
data=ConsolidationEventData(observations_created=1),
)
def _make_delivery_task(
bank_id: str = "bank-1",
url: str = "https://example.com/hook",
retry_count: int = 0,
webhook_id: str | None = None,
) -> dict:
return {
"type": "webhook_delivery",
"bank_id": bank_id,
"url": url,
"secret": None,
"event_type": "consolidation.completed",
"payload": '{"event":"consolidation.completed"}',
"webhook_id": webhook_id,
"_retry_count": retry_count,
}
# ---------------------------------------------------------------------------
# Unit tests (no DB)
# ---------------------------------------------------------------------------
class TestHmacSigning:
"""Unit tests for WebhookManager._sign_payload()."""
def _make_manager(self) -> WebhookManager:
"""Create a WebhookManager with a dummy pool (not used for signing)."""
pool = MagicMock()
return WebhookManager(pool=pool, global_webhooks=[])
def test_hmac_signing_format(self):
"""_sign_payload should return a string starting with 'sha256='."""
manager = self._make_manager()
sig = manager._sign_payload("my-secret", b"hello world")
assert sig.startswith("sha256="), f"Expected 'sha256=' prefix, got: {sig!r}"
hex_part = sig[len("sha256="):]
# SHA-256 hex digest is always 64 characters
assert len(hex_part) == 64
# Hex characters only
assert all(c in "0123456789abcdef" for c in hex_part)
def test_hmac_signing_is_deterministic(self):
"""Same secret + payload always produces the same signature."""
manager = self._make_manager()
payload = b'{"event":"consolidation.completed"}'
sig1 = manager._sign_payload("secret-key", payload)
sig2 = manager._sign_payload("secret-key", payload)
assert sig1 == sig2
def test_hmac_signing_differs_with_different_secret(self):
"""Different secrets must produce different signatures."""
manager = self._make_manager()
payload = b"payload"
sig1 = manager._sign_payload("secret-a", payload)
sig2 = manager._sign_payload("secret-b", payload)
assert sig1 != sig2
def test_hmac_signing_differs_with_different_payload(self):
"""Different payloads must produce different signatures."""
manager = self._make_manager()
sig1 = manager._sign_payload("secret", b"payload-one")
sig2 = manager._sign_payload("secret", b"payload-two")
assert sig1 != sig2
class TestRetryConstants:
"""Unit tests to verify retry schedule constants."""
def test_retry_delays_values(self):
"""RETRY_DELAYS must match the documented schedule."""
assert RETRY_DELAYS == [5, 300, 1800, 7200, 18000]
def test_max_attempts(self):
"""MAX_ATTEMPTS should be len(RETRY_DELAYS) + 1."""
assert MAX_ATTEMPTS == 6
assert MAX_ATTEMPTS == len(RETRY_DELAYS) + 1
# ---------------------------------------------------------------------------
# DB integration tests
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def webhook_manager(memory: MemoryEngine) -> WebhookManager:
"""Return a WebhookManager backed by the test pool with no global webhooks."""
return WebhookManager(pool=memory._pool, global_webhooks=[])
class TestFireEvent:
"""Integration tests for WebhookManager.fire_event()."""
@pytest.mark.asyncio
async def test_fire_event_creates_delivery(
self, memory: MemoryEngine, webhook_manager: WebhookManager
):
"""fire_event() inserts a pending webhook_delivery task in async_operations."""
bank_id = f"wh-test-{uuid.uuid4().hex[:8]}"
webhook_id = uuid.uuid4()
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO webhooks (id, bank_id, url, secret, event_types, enabled, created_at, updated_at)
VALUES ($1, $2, $3, NULL, $4, true, NOW(), NOW())
""",
webhook_id,
bank_id,
"https://example.com/hook",
["consolidation.completed"],
)
try:
event = _make_event(bank_id)
await webhook_manager.fire_event(event)
async with memory._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT status, task_payload
FROM async_operations
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'webhook_id' = $2
""",
bank_id,
str(webhook_id),
)
assert len(rows) == 1
assert rows[0]["status"] == "pending"
payload = rows[0]["task_payload"]
if isinstance(payload, str):
payload = json.loads(payload)
assert payload["event_type"] == "consolidation.completed"
finally:
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_type = 'webhook_delivery' AND bank_id = $1",
bank_id,
)
await conn.execute("DELETE FROM webhooks WHERE id = $1", webhook_id)
@pytest.mark.asyncio
async def test_fire_event_global_webhook(
self, memory: MemoryEngine
):
"""fire_event() also queues delivery tasks for global webhooks (not stored in DB)."""
bank_id = f"wh-global-{uuid.uuid4().hex[:8]}"
global_webhook = WebhookConfig(
id="", # No DB row
bank_id=None,
url="https://global.example.com/hook",
secret=None,
event_types=["consolidation.completed"],
enabled=True,
)
manager = WebhookManager(pool=memory._pool, global_webhooks=[global_webhook])
event = _make_event(bank_id)
await manager.fire_event(event)
async with memory._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT status, task_payload
FROM async_operations
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'url' = 'https://global.example.com/hook'
ORDER BY created_at DESC
LIMIT 1
"""
,
bank_id,
)
assert len(rows) == 1
assert rows[0]["status"] == "pending"
payload = rows[0]["task_payload"]
if isinstance(payload, str):
payload = json.loads(payload)
assert payload["webhook_id"] is None # global webhook has no DB row
# Cleanup
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_type = 'webhook_delivery' AND bank_id = $1",
bank_id,
)
@pytest.mark.asyncio
async def test_fire_event_no_match_if_event_type_mismatch(
self, memory: MemoryEngine, webhook_manager: WebhookManager
):
"""Webhooks registered for a different event type receive no delivery task."""
bank_id = f"wh-mismatch-{uuid.uuid4().hex[:8]}"
webhook_id = uuid.uuid4()
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO webhooks (id, bank_id, url, secret, event_types, enabled, created_at, updated_at)
VALUES ($1, $2, $3, NULL, $4, true, NOW(), NOW())
""",
webhook_id,
bank_id,
"https://example.com/other-hook",
["other.event"],
)
try:
event = _make_event(bank_id)
await webhook_manager.fire_event(event)
async with memory._pool.acquire() as conn:
count = await conn.fetchval(
"""
SELECT COUNT(*) FROM async_operations
WHERE operation_type = 'webhook_delivery' AND bank_id = $1
""",
bank_id,
)
assert count == 0
finally:
async with memory._pool.acquire() as conn:
await conn.execute("DELETE FROM webhooks WHERE id = $1", webhook_id)
class TestHandleWebhookDelivery:
"""Integration tests for MemoryEngine._handle_webhook_delivery()."""
@pytest.mark.asyncio
async def test_deliver_success(self, memory: MemoryEngine):
"""A successful HTTP POST completes without raising."""
task_dict = _make_delivery_task(retry_count=0)
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
with patch.object(memory._http_client, "post", new=AsyncMock(return_value=mock_response)):
# Should not raise
await memory._handle_webhook_delivery(task_dict)
@pytest.mark.asyncio
async def test_deliver_failure_raises_retry_task_at(self, memory: MemoryEngine):
"""A failed HTTP POST raises RetryTaskAt when retries remain."""
task_dict = _make_delivery_task(retry_count=0)
with patch.object(
memory._http_client, "post", new=AsyncMock(side_effect=Exception("connection refused"))
):
with pytest.raises(RetryTaskAt):
await memory._handle_webhook_delivery(task_dict)
@pytest.mark.asyncio
async def test_deliver_exhausted_retries_raises(self, memory: MemoryEngine):
"""When retry_count reaches MAX_ATTEMPTS-1, a failure raises the original exception."""
task_dict = _make_delivery_task(retry_count=MAX_ATTEMPTS - 1)
with patch.object(
memory._http_client, "post", new=AsyncMock(side_effect=Exception("server error"))
):
with pytest.raises(Exception, match="server error"):
await memory._handle_webhook_delivery(task_dict)
@pytest.mark.asyncio
async def test_deliver_retry_at_uses_delay_schedule(self, memory: MemoryEngine):
"""RetryTaskAt.retry_at is approximately now + RETRY_DELAYS[retry_count]."""
from datetime import timedelta
task_dict = _make_delivery_task(retry_count=1)
with patch.object(
memory._http_client, "post", new=AsyncMock(side_effect=Exception("fail"))
):
before = datetime.now(timezone.utc)
with pytest.raises(RetryTaskAt) as exc_info:
await memory._handle_webhook_delivery(task_dict)
after = datetime.now(timezone.utc)
retry_at = exc_info.value.retry_at
expected_delay = RETRY_DELAYS[1] # retry_count=1
assert retry_at >= before + timedelta(seconds=expected_delay - 2)
assert retry_at <= after + timedelta(seconds=expected_delay + 2)
@pytest.mark.asyncio
async def test_execute_task_marks_operation_completed(self, memory: MemoryEngine):
"""After a successful delivery, execute_task marks the async_operations row as completed."""
operation_id = str(uuid.uuid4())
bank_id = f"wh-exec-{uuid.uuid4().hex[:8]}"
# Insert a real async_operations row so _mark_operation_completed has something to update
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO async_operations
(operation_id, bank_id, operation_type, status, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'processing', '{}'::jsonb, '{}'::jsonb, NOW(), NOW())
""",
uuid.UUID(operation_id),
bank_id,
)
task_dict = {
**_make_delivery_task(bank_id=bank_id, retry_count=0),
"operation_id": operation_id,
}
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
with patch.object(memory._http_client, "post", new=AsyncMock(return_value=mock_response)):
await memory.execute_task(task_dict)
async with memory._pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT status FROM async_operations WHERE operation_id = $1",
uuid.UUID(operation_id),
)
assert row is not None
assert row["status"] == "completed", f"Expected 'completed', got '{row['status']}'"
# Cleanup
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_id = $1",
uuid.UUID(operation_id),
)
# ---------------------------------------------------------------------------
# HTTP API integration tests
# ---------------------------------------------------------------------------
@pytest_asyncio.fixture
async def api_client(memory: MemoryEngine):
"""Async HTTP test client wired to the FastAPI app."""
app = create_app(memory, initialize_memory=False)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
yield client
class TestWebhookHttpApi:
"""HTTP API integration tests for webhook CRUD endpoints."""
@pytest.mark.asyncio
async def test_http_create_webhook(self, api_client: httpx.AsyncClient):
"""POST /webhooks returns 201 and an id."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
response = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={
"url": "https://example.com/create",
"event_types": ["consolidation.completed"],
},
)
assert response.status_code == 201, response.text
data = response.json()
assert "id" in data
assert data["url"] == "https://example.com/create"
assert data["bank_id"] == bank_id
assert data["secret"] is None # secrets are never echoed back
# Cleanup
await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{data['id']}"
)
@pytest.mark.asyncio
async def test_http_list_webhooks(self, api_client: httpx.AsyncClient):
"""GET /webhooks returns the webhooks registered for a bank."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/list", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
list_resp = await api_client.get(f"/v1/default/banks/{bank_id}/webhooks")
assert list_resp.status_code == 200
items = list_resp.json()["items"]
assert any(item["id"] == webhook_id for item in items)
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_delete_webhook(self, api_client: httpx.AsyncClient):
"""DELETE /webhooks/{id} removes the webhook; subsequent list returns empty for that bank."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/delete", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
delete_resp = await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
)
assert delete_resp.status_code == 200
assert delete_resp.json()["success"] is True
list_resp = await api_client.get(f"/v1/default/banks/{bank_id}/webhooks")
assert list_resp.status_code == 200
ids = [item["id"] for item in list_resp.json()["items"]]
assert webhook_id not in ids
@pytest.mark.asyncio
async def test_http_delete_webhook_not_found(self, api_client: httpx.AsyncClient):
"""DELETE with a non-existent webhook id returns 404."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
missing_id = str(uuid.uuid4())
response = await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{missing_id}"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_http_list_deliveries(
self, memory: MemoryEngine, api_client: httpx.AsyncClient
):
"""GET /webhooks/{id}/deliveries returns delivery records for a webhook."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
# Create webhook via HTTP API
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={
"url": "https://example.com/deliveries",
"event_types": ["consolidation.completed"],
},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
# Insert a delivery row directly into async_operations
delivery_id = uuid.uuid4()
now = datetime.now(timezone.utc)
task_payload = json.dumps(
{
"type": "webhook_delivery",
"bank_id": bank_id,
"url": "https://example.com/deliveries",
"secret": None,
"event_type": "consolidation.completed",
"payload": '{"event":"consolidation.completed"}',
"webhook_id": webhook_id,
}
)
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO async_operations
(operation_id, bank_id, operation_type, status, retry_count, task_payload, result_metadata, created_at, updated_at)
VALUES ($1, $2, 'webhook_delivery', 'completed', 0, $3::jsonb, '{}'::jsonb, $4, $4)
""",
delivery_id,
bank_id,
task_payload,
now,
)
try:
deliveries_resp = await api_client.get(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries"
)
assert deliveries_resp.status_code == 200
items = deliveries_resp.json()["items"]
ids = [item["id"] for item in items]
assert str(delivery_id) in ids
# Verify shape of a delivery item
delivery = next(item for item in items if item["id"] == str(delivery_id))
assert delivery["status"] == "completed"
assert delivery["event_type"] == "consolidation.completed"
assert delivery["attempts"] == 1
finally:
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_id = $1", delivery_id
)
await api_client.delete(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
)
@pytest.mark.asyncio
async def test_http_list_deliveries_webhook_not_found(self, api_client: httpx.AsyncClient):
"""GET /webhooks/{id}/deliveries for a non-existent webhook returns 404."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
missing_id = str(uuid.uuid4())
response = await api_client.get(
f"/v1/default/banks/{bank_id}/webhooks/{missing_id}/deliveries"
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_http_update_webhook_url(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} updates only the provided fields."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/original", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={"url": "https://example.com/updated"},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["url"] == "https://example.com/updated"
# event_types should be unchanged
assert "consolidation.completed" in data["event_types"]
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_event_types(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} can update event_types."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={"event_types": ["retain.completed"]},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["event_types"] == ["retain.completed"]
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_enabled(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} can toggle enabled."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
assert create_resp.json()["enabled"] is True
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={"enabled": False},
)
assert patch_resp.status_code == 200
assert patch_resp.json()["enabled"] is False
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_http_config(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} can update http_config."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={
"http_config": {
"method": "POST",
"timeout_seconds": 10,
"headers": {"X-Custom": "value"},
"params": {},
}
},
)
assert patch_resp.status_code == 200
data = patch_resp.json()
assert data["http_config"]["timeout_seconds"] == 10
assert data["http_config"]["headers"] == {"X-Custom": "value"}
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
@pytest.mark.asyncio
async def test_http_update_webhook_not_found(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} returns 404 for a non-existent webhook."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
missing_id = str(uuid.uuid4())
response = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{missing_id}",
json={"url": "https://example.com/new"},
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_http_update_webhook_no_fields(self, api_client: httpx.AsyncClient):
"""PATCH /webhooks/{id} with empty body returns 422."""
bank_id = f"http-wh-{uuid.uuid4().hex[:8]}"
create_resp = await api_client.post(
f"/v1/default/banks/{bank_id}/webhooks",
json={"url": "https://example.com/hook", "event_types": ["consolidation.completed"]},
)
assert create_resp.status_code == 201
webhook_id = create_resp.json()["id"]
patch_resp = await api_client.patch(
f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
json={},
)
assert patch_resp.status_code == 422
# Cleanup
await api_client.delete(f"/v1/default/banks/{bank_id}/webhooks/{webhook_id}")
# ---------------------------------------------------------------------------
# retain.completed webhook tests
# ---------------------------------------------------------------------------
class TestRetainCompletedWebhook:
"""Tests for the retain.completed webhook event."""
def test_retain_event_data_model(self):
"""RetainEventData can be constructed with optional fields."""
data = RetainEventData(document_id="doc-123", tags=["tag1", "tag2"])
assert data.document_id == "doc-123"
assert data.tags == ["tag1", "tag2"]
empty = RetainEventData()
assert empty.document_id is None
assert empty.tags is None
def test_retain_event_type_value(self):
"""WebhookEventType.RETAIN_COMPLETED has the correct string value."""
assert WebhookEventType.RETAIN_COMPLETED == "retain.completed"
@pytest.mark.asyncio
async def test_fire_retain_webhook_queues_per_document(
self, memory: MemoryEngine, webhook_manager: WebhookManager
):
"""_fire_retain_webhook queues one delivery task per content item."""
bank_id = f"wh-retain-{uuid.uuid4().hex[:8]}"
webhook_id = uuid.uuid4()
async with memory._pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO webhooks (id, bank_id, url, secret, event_types, enabled, created_at, updated_at)
VALUES ($1, $2, $3, NULL, $4, true, NOW(), NOW())
""",
webhook_id,
bank_id,
"https://example.com/retain-hook",
["retain.completed"],
)
try:
contents = [
{"content": "Alice works at Google", "document_id": "doc-1"},
{"content": "Bob loves Python", "document_id": "doc-2"},
]
# Temporarily replace webhook manager on memory engine
original_manager = memory._webhook_manager
memory._webhook_manager = webhook_manager
try:
callback = memory._build_retain_outbox_callback(
bank_id=bank_id,
contents=contents,
operation_id="test-op-123",
)
assert callback is not None
async with memory._pool.acquire() as conn:
await callback(conn)
finally:
memory._webhook_manager = original_manager
async with memory._pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT task_payload
FROM async_operations
WHERE operation_type = 'webhook_delivery'
AND bank_id = $1
AND task_payload->>'event_type' = 'retain.completed'
ORDER BY created_at
""",
bank_id,
)
assert len(rows) == 2
payloads = []
for row in rows:
p = row["task_payload"]
if isinstance(p, str):
p = json.loads(p)
payloads.append(p)
doc_ids_in_payloads = [json.loads(p["payload"]).get("data", {}).get("document_id") for p in payloads]
assert "doc-1" in doc_ids_in_payloads
assert "doc-2" in doc_ids_in_payloads
finally:
async with memory._pool.acquire() as conn:
await conn.execute(
"DELETE FROM async_operations WHERE operation_type = 'webhook_delivery' AND bank_id = $1",
bank_id,
)
await conn.execute("DELETE FROM webhooks WHERE id = $1", webhook_id)

View file

@ -294,14 +294,17 @@ class TestWorkerPoller:
payload,
)
from datetime import datetime, timezone
from hindsight_api.worker.exceptions import RetryTaskAt
async def failing_executor(task_dict):
raise ValueError("TimeoutError during recall")
raise RetryTaskAt(retry_at=datetime.now(timezone.utc), message="TimeoutError during recall")
poller = WorkerPoller(
pool=pool,
worker_id="test-worker-1",
executor=failing_executor,
max_retries=3,
)
task_dict = json.loads(payload)
@ -326,39 +329,35 @@ class TestWorkerPoller:
assert row["retry_count"] == 1
@pytest.mark.asyncio
async def test_executor_exception_marks_failed_after_max_retries(self, pool, clean_operations):
"""Test that a task is permanently marked 'failed' once retry_count hits max_retries.
async def test_executor_exception_marks_failed_immediately(self, pool, clean_operations):
"""Test that a plain exception (not RetryTaskAt) permanently marks a task as 'failed'.
After max_retries exhaustion the task must NOT be reset to 'pending' it should
be marked 'failed' with an error message so it stops consuming retry budget.
With the task-owned retry model, plain exceptions are non-retryable the poller
marks them as 'failed' immediately. Tasks that want to be retried must raise RetryTaskAt.
"""
from hindsight_api.worker import WorkerPoller
from hindsight_api.worker.poller import ClaimedTask
max_retries = 3
bank_id = f"test-worker-{uuid.uuid4().hex[:8]}"
op_id = uuid.uuid4()
payload = json.dumps({"type": "consolidation", "operation_id": str(op_id), "bank_id": bank_id})
# Insert with retry_count already at the limit
await pool.execute(
"""
INSERT INTO async_operations (operation_id, bank_id, operation_type, status, task_payload, worker_id, claimed_at, retry_count)
VALUES ($1, $2, 'consolidation', 'processing', $3::jsonb, 'test-worker-1', now(), $4)
VALUES ($1, $2, 'consolidation', 'processing', $3::jsonb, 'test-worker-1', now(), 0)
""",
op_id,
bank_id,
payload,
max_retries,
)
async def failing_executor(task_dict):
raise ValueError("Still failing after all retries")
raise ValueError("Non-retryable error")
poller = WorkerPoller(
pool=pool,
worker_id="test-worker-1",
executor=failing_executor,
max_retries=max_retries,
)
task_dict = json.loads(payload)
@ -373,11 +372,10 @@ class TestWorkerPoller:
op_id,
)
assert row["status"] == "failed", (
f"Expected 'failed' after max retries, got '{row['status']}'"
f"Expected 'failed' for plain exception, got '{row['status']}'"
)
assert row["error_message"] is not None
assert "Max retries" in row["error_message"]
assert row["retry_count"] == max_retries # not incremented further
assert row["retry_count"] == 0 # not incremented; plain exception = immediate fail
@pytest.mark.asyncio
async def test_executor_failed_status_not_overridden(self, pool, clean_operations):

View file

@ -2111,6 +2111,248 @@ paths:
summary: Trigger consolidation
tags:
- Banks
/v1/default/banks/{bank_id}/webhooks:
get:
description: List all webhooks registered for a bank.
operationId: list_webhooks
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookListResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: List webhooks
tags:
- Webhooks
post:
description: Register a webhook endpoint to receive event notifications for
this bank.
operationId: create_webhook
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateWebhookRequest'
required: true
responses:
"201":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Register webhook
tags:
- Webhooks
/v1/default/banks/{bank_id}/webhooks/{webhook_id}:
delete:
description: Remove a registered webhook.
operationId: delete_webhook
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: webhook_id
required: true
schema:
title: Webhook Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Delete webhook
tags:
- Webhooks
patch:
description: Update one or more fields of a registered webhook. Only provided
fields are changed.
operationId: update_webhook
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: webhook_id
required: true
schema:
title: Webhook Id
type: string
style: simple
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateWebhookRequest'
required: true
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Update webhook
tags:
- Webhooks
/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries:
get:
description: Inspect delivery history for a webhook (useful for debugging).
operationId: list_webhook_deliveries
parameters:
- explode: false
in: path
name: bank_id
required: true
schema:
title: Bank Id
type: string
style: simple
- explode: false
in: path
name: webhook_id
required: true
schema:
title: Webhook Id
type: string
style: simple
- description: Maximum number of deliveries to return
explode: true
in: query
name: limit
required: false
schema:
default: 50
description: Maximum number of deliveries to return
maximum: 200
title: Limit
type: integer
style: form
- description: Pagination cursor (created_at of last item)
explode: true
in: query
name: cursor
required: false
schema:
nullable: true
type: string
style: form
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookDeliveryListResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: List webhook deliveries
tags:
- Webhooks
/v1/default/banks/{bank_id}/memories:
delete:
description: "Delete memory units for a memory bank. Optionally filter by type\
@ -2879,6 +3121,47 @@ components:
required:
- operation_id
title: CreateMentalModelResponse
CreateWebhookRequest:
description: Request model for registering a webhook.
example:
event_types:
- event_types
- event_types
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
url:
description: HTTP(S) endpoint URL to deliver events to
title: Url
type: string
secret:
nullable: true
type: string
event_types:
default:
- consolidation.completed
description: "List of event types to deliver. Currently supported: 'consolidation.completed'"
items:
type: string
type: array
enabled:
default: true
description: Whether this webhook is active
title: Enabled
type: boolean
http_config:
$ref: '#/components/schemas/WebhookHttpConfig'
required:
- url
title: CreateWebhookRequest
DeleteDocumentResponse:
description: Response model for delete document endpoint.
example:
@ -4437,6 +4720,41 @@ components:
trigger:
$ref: '#/components/schemas/MentalModelTrigger'
title: UpdateMentalModelRequest
UpdateWebhookRequest:
description: Request model for updating a webhook. Only provided fields are
updated.
example:
event_types:
- event_types
- event_types
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
url:
nullable: true
type: string
secret:
nullable: true
type: string
event_types:
items:
type: string
nullable: true
type: array
enabled:
nullable: true
type: boolean
http_config:
$ref: '#/components/schemas/WebhookHttpConfig'
title: UpdateWebhookRequest
ValidationError:
example:
msg: msg
@ -4481,6 +4799,244 @@ components:
- api_version
- features
title: VersionResponse
WebhookDeliveryListResponse:
description: Response model for listing webhook deliveries.
example:
next_cursor: next_cursor
items:
- last_response_body: last_response_body
last_attempt_at: last_attempt_at
created_at: created_at
last_response_status: 6
url: url
event_type: event_type
updated_at: updated_at
webhook_id: webhook_id
next_retry_at: next_retry_at
id: id
last_error: last_error
status: status
attempts: 0
- last_response_body: last_response_body
last_attempt_at: last_attempt_at
created_at: created_at
last_response_status: 6
url: url
event_type: event_type
updated_at: updated_at
webhook_id: webhook_id
next_retry_at: next_retry_at
id: id
last_error: last_error
status: status
attempts: 0
properties:
items:
items:
$ref: '#/components/schemas/WebhookDeliveryResponse'
type: array
next_cursor:
nullable: true
type: string
required:
- items
title: WebhookDeliveryListResponse
WebhookDeliveryResponse:
description: Response model for a webhook delivery record.
example:
last_response_body: last_response_body
last_attempt_at: last_attempt_at
created_at: created_at
last_response_status: 6
url: url
event_type: event_type
updated_at: updated_at
webhook_id: webhook_id
next_retry_at: next_retry_at
id: id
last_error: last_error
status: status
attempts: 0
properties:
id:
title: Id
type: string
webhook_id:
nullable: true
type: string
url:
title: Url
type: string
event_type:
title: Event Type
type: string
status:
title: Status
type: string
attempts:
title: Attempts
type: integer
next_retry_at:
nullable: true
type: string
last_error:
nullable: true
type: string
last_response_status:
nullable: true
type: integer
last_response_body:
nullable: true
type: string
last_attempt_at:
nullable: true
type: string
created_at:
nullable: true
type: string
updated_at:
nullable: true
type: string
required:
- attempts
- event_type
- id
- status
- url
- webhook_id
title: WebhookDeliveryResponse
WebhookHttpConfig:
description: HTTP delivery configuration for a webhook.
example:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
properties:
method:
default: POST
description: "HTTP method: GET or POST"
title: Method
type: string
timeout_seconds:
default: 30
description: HTTP request timeout in seconds
title: Timeout Seconds
type: integer
headers:
additionalProperties:
type: string
description: Custom HTTP headers
title: Headers
params:
additionalProperties:
type: string
description: Custom HTTP query parameters
title: Params
title: WebhookHttpConfig
WebhookListResponse:
description: Response model for listing webhooks.
example:
items:
- event_types:
- event_types
- event_types
updated_at: updated_at
bank_id: bank_id
created_at: created_at
id: id
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
- event_types:
- event_types
- event_types
updated_at: updated_at
bank_id: bank_id
created_at: created_at
id: id
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
items:
items:
$ref: '#/components/schemas/WebhookResponse'
type: array
required:
- items
title: WebhookListResponse
WebhookResponse:
description: Response model for a webhook.
example:
event_types:
- event_types
- event_types
updated_at: updated_at
bank_id: bank_id
created_at: created_at
id: id
secret: secret
http_config:
headers:
key: headers
method: POST
timeout_seconds: 0
params:
key: params
url: url
enabled: true
properties:
id:
title: Id
type: string
bank_id:
nullable: true
type: string
url:
title: Url
type: string
secret:
nullable: true
type: string
event_types:
items:
type: string
type: array
enabled:
title: Enabled
type: boolean
http_config:
$ref: '#/components/schemas/WebhookHttpConfig'
created_at:
nullable: true
type: string
updated_at:
nullable: true
type: string
required:
- bank_id
- enabled
- event_types
- id
- url
title: WebhookResponse
Timestamp:
anyOf:
- format: date-time

View file

@ -0,0 +1,691 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"bytes"
"context"
"io"
"net/http"
"net/url"
"strings"
)
// WebhooksAPIService WebhooksAPI service
type WebhooksAPIService service
type ApiCreateWebhookRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
createWebhookRequest *CreateWebhookRequest
authorization *string
}
func (r ApiCreateWebhookRequest) CreateWebhookRequest(createWebhookRequest CreateWebhookRequest) ApiCreateWebhookRequest {
r.createWebhookRequest = &createWebhookRequest
return r
}
func (r ApiCreateWebhookRequest) Authorization(authorization string) ApiCreateWebhookRequest {
r.authorization = &authorization
return r
}
func (r ApiCreateWebhookRequest) Execute() (*WebhookResponse, *http.Response, error) {
return r.ApiService.CreateWebhookExecute(r)
}
/*
CreateWebhook Register webhook
Register a webhook endpoint to receive event notifications for this bank.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@return ApiCreateWebhookRequest
*/
func (a *WebhooksAPIService) CreateWebhook(ctx context.Context, bankId string) ApiCreateWebhookRequest {
return ApiCreateWebhookRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
}
}
// Execute executes the request
// @return WebhookResponse
func (a *WebhooksAPIService) CreateWebhookExecute(r ApiCreateWebhookRequest) (*WebhookResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPost
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.CreateWebhook")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.createWebhookRequest == nil {
return localVarReturnValue, nil, reportError("createWebhookRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
// body params
localVarPostBody = r.createWebhookRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiDeleteWebhookRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
webhookId string
authorization *string
}
func (r ApiDeleteWebhookRequest) Authorization(authorization string) ApiDeleteWebhookRequest {
r.authorization = &authorization
return r
}
func (r ApiDeleteWebhookRequest) Execute() (*DeleteResponse, *http.Response, error) {
return r.ApiService.DeleteWebhookExecute(r)
}
/*
DeleteWebhook Delete webhook
Remove a registered webhook.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param webhookId
@return ApiDeleteWebhookRequest
*/
func (a *WebhooksAPIService) DeleteWebhook(ctx context.Context, bankId string, webhookId string) ApiDeleteWebhookRequest {
return ApiDeleteWebhookRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
webhookId: webhookId,
}
}
// Execute executes the request
// @return DeleteResponse
func (a *WebhooksAPIService) DeleteWebhookExecute(r ApiDeleteWebhookRequest) (*DeleteResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodDelete
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *DeleteResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.DeleteWebhook")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"webhook_id"+"}", url.PathEscape(parameterValueToString(r.webhookId, "webhookId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiListWebhookDeliveriesRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
webhookId string
limit *int32
cursor *string
authorization *string
}
// Maximum number of deliveries to return
func (r ApiListWebhookDeliveriesRequest) Limit(limit int32) ApiListWebhookDeliveriesRequest {
r.limit = &limit
return r
}
// Pagination cursor (created_at of last item)
func (r ApiListWebhookDeliveriesRequest) Cursor(cursor string) ApiListWebhookDeliveriesRequest {
r.cursor = &cursor
return r
}
func (r ApiListWebhookDeliveriesRequest) Authorization(authorization string) ApiListWebhookDeliveriesRequest {
r.authorization = &authorization
return r
}
func (r ApiListWebhookDeliveriesRequest) Execute() (*WebhookDeliveryListResponse, *http.Response, error) {
return r.ApiService.ListWebhookDeliveriesExecute(r)
}
/*
ListWebhookDeliveries List webhook deliveries
Inspect delivery history for a webhook (useful for debugging).
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param webhookId
@return ApiListWebhookDeliveriesRequest
*/
func (a *WebhooksAPIService) ListWebhookDeliveries(ctx context.Context, bankId string, webhookId string) ApiListWebhookDeliveriesRequest {
return ApiListWebhookDeliveriesRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
webhookId: webhookId,
}
}
// Execute executes the request
// @return WebhookDeliveryListResponse
func (a *WebhooksAPIService) ListWebhookDeliveriesExecute(r ApiListWebhookDeliveriesRequest) (*WebhookDeliveryListResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookDeliveryListResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.ListWebhookDeliveries")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"webhook_id"+"}", url.PathEscape(parameterValueToString(r.webhookId, "webhookId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.limit != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "")
} else {
var defaultValue int32 = 50
r.limit = &defaultValue
}
if r.cursor != nil {
parameterAddToHeaderOrQuery(localVarQueryParams, "cursor", r.cursor, "form", "")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiListWebhooksRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
authorization *string
}
func (r ApiListWebhooksRequest) Authorization(authorization string) ApiListWebhooksRequest {
r.authorization = &authorization
return r
}
func (r ApiListWebhooksRequest) Execute() (*WebhookListResponse, *http.Response, error) {
return r.ApiService.ListWebhooksExecute(r)
}
/*
ListWebhooks List webhooks
List all webhooks registered for a bank.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@return ApiListWebhooksRequest
*/
func (a *WebhooksAPIService) ListWebhooks(ctx context.Context, bankId string) ApiListWebhooksRequest {
return ApiListWebhooksRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
}
}
// Execute executes the request
// @return WebhookListResponse
func (a *WebhooksAPIService) ListWebhooksExecute(r ApiListWebhooksRequest) (*WebhookListResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookListResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.ListWebhooks")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiUpdateWebhookRequest struct {
ctx context.Context
ApiService *WebhooksAPIService
bankId string
webhookId string
updateWebhookRequest *UpdateWebhookRequest
authorization *string
}
func (r ApiUpdateWebhookRequest) UpdateWebhookRequest(updateWebhookRequest UpdateWebhookRequest) ApiUpdateWebhookRequest {
r.updateWebhookRequest = &updateWebhookRequest
return r
}
func (r ApiUpdateWebhookRequest) Authorization(authorization string) ApiUpdateWebhookRequest {
r.authorization = &authorization
return r
}
func (r ApiUpdateWebhookRequest) Execute() (*WebhookResponse, *http.Response, error) {
return r.ApiService.UpdateWebhookExecute(r)
}
/*
UpdateWebhook Update webhook
Update one or more fields of a registered webhook. Only provided fields are changed.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@param webhookId
@return ApiUpdateWebhookRequest
*/
func (a *WebhooksAPIService) UpdateWebhook(ctx context.Context, bankId string, webhookId string) ApiUpdateWebhookRequest {
return ApiUpdateWebhookRequest{
ApiService: a,
ctx: ctx,
bankId: bankId,
webhookId: webhookId,
}
}
// Execute executes the request
// @return WebhookResponse
func (a *WebhooksAPIService) UpdateWebhookExecute(r ApiUpdateWebhookRequest) (*WebhookResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPatch
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *WebhookResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "WebhooksAPIService.UpdateWebhook")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/webhooks/{webhook_id}"
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
localVarPath = strings.Replace(localVarPath, "{"+"webhook_id"+"}", url.PathEscape(parameterValueToString(r.webhookId, "webhookId")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.updateWebhookRequest == nil {
return localVarReturnValue, nil, reportError("updateWebhookRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
// body params
localVarPostBody = r.updateWebhookRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}

View file

@ -66,6 +66,8 @@ type APIClient struct {
MonitoringAPI *MonitoringAPIService
OperationsAPI *OperationsAPIService
WebhooksAPI *WebhooksAPIService
}
type service struct {
@ -93,6 +95,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
c.MentalModelsAPI = (*MentalModelsAPIService)(&c.common)
c.MonitoringAPI = (*MonitoringAPIService)(&c.common)
c.OperationsAPI = (*OperationsAPIService)(&c.common)
c.WebhooksAPI = (*WebhooksAPIService)(&c.common)
return c
}

View file

@ -0,0 +1,320 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the CreateWebhookRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &CreateWebhookRequest{}
// CreateWebhookRequest Request model for registering a webhook.
type CreateWebhookRequest struct {
// HTTP(S) endpoint URL to deliver events to
Url string `json:"url"`
Secret NullableString `json:"secret,omitempty"`
// List of event types to deliver. Currently supported: 'consolidation.completed'
EventTypes []string `json:"event_types,omitempty"`
// Whether this webhook is active
Enabled *bool `json:"enabled,omitempty"`
// HTTP delivery configuration (method, timeout, headers, params)
HttpConfig *WebhookHttpConfig `json:"http_config,omitempty"`
}
type _CreateWebhookRequest CreateWebhookRequest
// NewCreateWebhookRequest instantiates a new CreateWebhookRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewCreateWebhookRequest(url string) *CreateWebhookRequest {
this := CreateWebhookRequest{}
this.Url = url
var enabled bool = true
this.Enabled = &enabled
return &this
}
// NewCreateWebhookRequestWithDefaults instantiates a new CreateWebhookRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewCreateWebhookRequestWithDefaults() *CreateWebhookRequest {
this := CreateWebhookRequest{}
var enabled bool = true
this.Enabled = &enabled
return &this
}
// GetUrl returns the Url field value
func (o *CreateWebhookRequest) GetUrl() string {
if o == nil {
var ret string
return ret
}
return o.Url
}
// GetUrlOk returns a tuple with the Url field value
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Url, true
}
// SetUrl sets field value
func (o *CreateWebhookRequest) SetUrl(v string) {
o.Url = v
}
// GetSecret returns the Secret field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateWebhookRequest) GetSecret() string {
if o == nil || IsNil(o.Secret.Get()) {
var ret string
return ret
}
return *o.Secret.Get()
}
// GetSecretOk returns a tuple with the Secret field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateWebhookRequest) GetSecretOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Secret.Get(), o.Secret.IsSet()
}
// HasSecret returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasSecret() bool {
if o != nil && o.Secret.IsSet() {
return true
}
return false
}
// SetSecret gets a reference to the given NullableString and assigns it to the Secret field.
func (o *CreateWebhookRequest) SetSecret(v string) {
o.Secret.Set(&v)
}
// SetSecretNil sets the value for Secret to be an explicit nil
func (o *CreateWebhookRequest) SetSecretNil() {
o.Secret.Set(nil)
}
// UnsetSecret ensures that no value is present for Secret, not even an explicit nil
func (o *CreateWebhookRequest) UnsetSecret() {
o.Secret.Unset()
}
// GetEventTypes returns the EventTypes field value if set, zero value otherwise.
func (o *CreateWebhookRequest) GetEventTypes() []string {
if o == nil || IsNil(o.EventTypes) {
var ret []string
return ret
}
return o.EventTypes
}
// GetEventTypesOk returns a tuple with the EventTypes field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetEventTypesOk() ([]string, bool) {
if o == nil || IsNil(o.EventTypes) {
return nil, false
}
return o.EventTypes, true
}
// HasEventTypes returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasEventTypes() bool {
if o != nil && !IsNil(o.EventTypes) {
return true
}
return false
}
// SetEventTypes gets a reference to the given []string and assigns it to the EventTypes field.
func (o *CreateWebhookRequest) SetEventTypes(v []string) {
o.EventTypes = v
}
// GetEnabled returns the Enabled field value if set, zero value otherwise.
func (o *CreateWebhookRequest) GetEnabled() bool {
if o == nil || IsNil(o.Enabled) {
var ret bool
return ret
}
return *o.Enabled
}
// GetEnabledOk returns a tuple with the Enabled field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetEnabledOk() (*bool, bool) {
if o == nil || IsNil(o.Enabled) {
return nil, false
}
return o.Enabled, true
}
// HasEnabled returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasEnabled() bool {
if o != nil && !IsNil(o.Enabled) {
return true
}
return false
}
// SetEnabled gets a reference to the given bool and assigns it to the Enabled field.
func (o *CreateWebhookRequest) SetEnabled(v bool) {
o.Enabled = &v
}
// GetHttpConfig returns the HttpConfig field value if set, zero value otherwise.
func (o *CreateWebhookRequest) GetHttpConfig() WebhookHttpConfig {
if o == nil || IsNil(o.HttpConfig) {
var ret WebhookHttpConfig
return ret
}
return *o.HttpConfig
}
// GetHttpConfigOk returns a tuple with the HttpConfig field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *CreateWebhookRequest) GetHttpConfigOk() (*WebhookHttpConfig, bool) {
if o == nil || IsNil(o.HttpConfig) {
return nil, false
}
return o.HttpConfig, true
}
// HasHttpConfig returns a boolean if a field has been set.
func (o *CreateWebhookRequest) HasHttpConfig() bool {
if o != nil && !IsNil(o.HttpConfig) {
return true
}
return false
}
// SetHttpConfig gets a reference to the given WebhookHttpConfig and assigns it to the HttpConfig field.
func (o *CreateWebhookRequest) SetHttpConfig(v WebhookHttpConfig) {
o.HttpConfig = &v
}
func (o CreateWebhookRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o CreateWebhookRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["url"] = o.Url
if o.Secret.IsSet() {
toSerialize["secret"] = o.Secret.Get()
}
if !IsNil(o.EventTypes) {
toSerialize["event_types"] = o.EventTypes
}
if !IsNil(o.Enabled) {
toSerialize["enabled"] = o.Enabled
}
if !IsNil(o.HttpConfig) {
toSerialize["http_config"] = o.HttpConfig
}
return toSerialize, nil
}
func (o *CreateWebhookRequest) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"url",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varCreateWebhookRequest := _CreateWebhookRequest{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varCreateWebhookRequest)
if err != nil {
return err
}
*o = CreateWebhookRequest(varCreateWebhookRequest)
return err
}
type NullableCreateWebhookRequest struct {
value *CreateWebhookRequest
isSet bool
}
func (v NullableCreateWebhookRequest) Get() *CreateWebhookRequest {
return v.value
}
func (v *NullableCreateWebhookRequest) Set(val *CreateWebhookRequest) {
v.value = val
v.isSet = true
}
func (v NullableCreateWebhookRequest) IsSet() bool {
return v.isSet
}
func (v *NullableCreateWebhookRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableCreateWebhookRequest(val *CreateWebhookRequest) *NullableCreateWebhookRequest {
return &NullableCreateWebhookRequest{value: val, isSet: true}
}
func (v NullableCreateWebhookRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableCreateWebhookRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,311 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
)
// checks if the UpdateWebhookRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UpdateWebhookRequest{}
// UpdateWebhookRequest Request model for updating a webhook. Only provided fields are updated.
type UpdateWebhookRequest struct {
Url NullableString `json:"url,omitempty"`
Secret NullableString `json:"secret,omitempty"`
EventTypes []string `json:"event_types,omitempty"`
Enabled NullableBool `json:"enabled,omitempty"`
HttpConfig NullableWebhookHttpConfig `json:"http_config,omitempty"`
}
// NewUpdateWebhookRequest instantiates a new UpdateWebhookRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUpdateWebhookRequest() *UpdateWebhookRequest {
this := UpdateWebhookRequest{}
return &this
}
// NewUpdateWebhookRequestWithDefaults instantiates a new UpdateWebhookRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUpdateWebhookRequestWithDefaults() *UpdateWebhookRequest {
this := UpdateWebhookRequest{}
return &this
}
// GetUrl returns the Url field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetUrl() string {
if o == nil || IsNil(o.Url.Get()) {
var ret string
return ret
}
return *o.Url.Get()
}
// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Url.Get(), o.Url.IsSet()
}
// HasUrl returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasUrl() bool {
if o != nil && o.Url.IsSet() {
return true
}
return false
}
// SetUrl gets a reference to the given NullableString and assigns it to the Url field.
func (o *UpdateWebhookRequest) SetUrl(v string) {
o.Url.Set(&v)
}
// SetUrlNil sets the value for Url to be an explicit nil
func (o *UpdateWebhookRequest) SetUrlNil() {
o.Url.Set(nil)
}
// UnsetUrl ensures that no value is present for Url, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetUrl() {
o.Url.Unset()
}
// GetSecret returns the Secret field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetSecret() string {
if o == nil || IsNil(o.Secret.Get()) {
var ret string
return ret
}
return *o.Secret.Get()
}
// GetSecretOk returns a tuple with the Secret field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetSecretOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Secret.Get(), o.Secret.IsSet()
}
// HasSecret returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasSecret() bool {
if o != nil && o.Secret.IsSet() {
return true
}
return false
}
// SetSecret gets a reference to the given NullableString and assigns it to the Secret field.
func (o *UpdateWebhookRequest) SetSecret(v string) {
o.Secret.Set(&v)
}
// SetSecretNil sets the value for Secret to be an explicit nil
func (o *UpdateWebhookRequest) SetSecretNil() {
o.Secret.Set(nil)
}
// UnsetSecret ensures that no value is present for Secret, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetSecret() {
o.Secret.Unset()
}
// GetEventTypes returns the EventTypes field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetEventTypes() []string {
if o == nil {
var ret []string
return ret
}
return o.EventTypes
}
// GetEventTypesOk returns a tuple with the EventTypes field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetEventTypesOk() ([]string, bool) {
if o == nil || IsNil(o.EventTypes) {
return nil, false
}
return o.EventTypes, true
}
// HasEventTypes returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasEventTypes() bool {
if o != nil && !IsNil(o.EventTypes) {
return true
}
return false
}
// SetEventTypes gets a reference to the given []string and assigns it to the EventTypes field.
func (o *UpdateWebhookRequest) SetEventTypes(v []string) {
o.EventTypes = v
}
// GetEnabled returns the Enabled field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetEnabled() bool {
if o == nil || IsNil(o.Enabled.Get()) {
var ret bool
return ret
}
return *o.Enabled.Get()
}
// GetEnabledOk returns a tuple with the Enabled field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetEnabledOk() (*bool, bool) {
if o == nil {
return nil, false
}
return o.Enabled.Get(), o.Enabled.IsSet()
}
// HasEnabled returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasEnabled() bool {
if o != nil && o.Enabled.IsSet() {
return true
}
return false
}
// SetEnabled gets a reference to the given NullableBool and assigns it to the Enabled field.
func (o *UpdateWebhookRequest) SetEnabled(v bool) {
o.Enabled.Set(&v)
}
// SetEnabledNil sets the value for Enabled to be an explicit nil
func (o *UpdateWebhookRequest) SetEnabledNil() {
o.Enabled.Set(nil)
}
// UnsetEnabled ensures that no value is present for Enabled, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetEnabled() {
o.Enabled.Unset()
}
// GetHttpConfig returns the HttpConfig field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *UpdateWebhookRequest) GetHttpConfig() WebhookHttpConfig {
if o == nil || IsNil(o.HttpConfig.Get()) {
var ret WebhookHttpConfig
return ret
}
return *o.HttpConfig.Get()
}
// GetHttpConfigOk returns a tuple with the HttpConfig field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *UpdateWebhookRequest) GetHttpConfigOk() (*WebhookHttpConfig, bool) {
if o == nil {
return nil, false
}
return o.HttpConfig.Get(), o.HttpConfig.IsSet()
}
// HasHttpConfig returns a boolean if a field has been set.
func (o *UpdateWebhookRequest) HasHttpConfig() bool {
if o != nil && o.HttpConfig.IsSet() {
return true
}
return false
}
// SetHttpConfig gets a reference to the given NullableWebhookHttpConfig and assigns it to the HttpConfig field.
func (o *UpdateWebhookRequest) SetHttpConfig(v WebhookHttpConfig) {
o.HttpConfig.Set(&v)
}
// SetHttpConfigNil sets the value for HttpConfig to be an explicit nil
func (o *UpdateWebhookRequest) SetHttpConfigNil() {
o.HttpConfig.Set(nil)
}
// UnsetHttpConfig ensures that no value is present for HttpConfig, not even an explicit nil
func (o *UpdateWebhookRequest) UnsetHttpConfig() {
o.HttpConfig.Unset()
}
func (o UpdateWebhookRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UpdateWebhookRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if o.Url.IsSet() {
toSerialize["url"] = o.Url.Get()
}
if o.Secret.IsSet() {
toSerialize["secret"] = o.Secret.Get()
}
if o.EventTypes != nil {
toSerialize["event_types"] = o.EventTypes
}
if o.Enabled.IsSet() {
toSerialize["enabled"] = o.Enabled.Get()
}
if o.HttpConfig.IsSet() {
toSerialize["http_config"] = o.HttpConfig.Get()
}
return toSerialize, nil
}
type NullableUpdateWebhookRequest struct {
value *UpdateWebhookRequest
isSet bool
}
func (v NullableUpdateWebhookRequest) Get() *UpdateWebhookRequest {
return v.value
}
func (v *NullableUpdateWebhookRequest) Set(val *UpdateWebhookRequest) {
v.value = val
v.isSet = true
}
func (v NullableUpdateWebhookRequest) IsSet() bool {
return v.isSet
}
func (v *NullableUpdateWebhookRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUpdateWebhookRequest(val *UpdateWebhookRequest) *NullableUpdateWebhookRequest {
return &NullableUpdateWebhookRequest{value: val, isSet: true}
}
func (v NullableUpdateWebhookRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUpdateWebhookRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,204 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookDeliveryListResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookDeliveryListResponse{}
// WebhookDeliveryListResponse Response model for listing webhook deliveries.
type WebhookDeliveryListResponse struct {
Items []WebhookDeliveryResponse `json:"items"`
NextCursor NullableString `json:"next_cursor,omitempty"`
}
type _WebhookDeliveryListResponse WebhookDeliveryListResponse
// NewWebhookDeliveryListResponse instantiates a new WebhookDeliveryListResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookDeliveryListResponse(items []WebhookDeliveryResponse) *WebhookDeliveryListResponse {
this := WebhookDeliveryListResponse{}
this.Items = items
return &this
}
// NewWebhookDeliveryListResponseWithDefaults instantiates a new WebhookDeliveryListResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookDeliveryListResponseWithDefaults() *WebhookDeliveryListResponse {
this := WebhookDeliveryListResponse{}
return &this
}
// GetItems returns the Items field value
func (o *WebhookDeliveryListResponse) GetItems() []WebhookDeliveryResponse {
if o == nil {
var ret []WebhookDeliveryResponse
return ret
}
return o.Items
}
// GetItemsOk returns a tuple with the Items field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryListResponse) GetItemsOk() ([]WebhookDeliveryResponse, bool) {
if o == nil {
return nil, false
}
return o.Items, true
}
// SetItems sets field value
func (o *WebhookDeliveryListResponse) SetItems(v []WebhookDeliveryResponse) {
o.Items = v
}
// GetNextCursor returns the NextCursor field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryListResponse) GetNextCursor() string {
if o == nil || IsNil(o.NextCursor.Get()) {
var ret string
return ret
}
return *o.NextCursor.Get()
}
// GetNextCursorOk returns a tuple with the NextCursor field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryListResponse) GetNextCursorOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.NextCursor.Get(), o.NextCursor.IsSet()
}
// HasNextCursor returns a boolean if a field has been set.
func (o *WebhookDeliveryListResponse) HasNextCursor() bool {
if o != nil && o.NextCursor.IsSet() {
return true
}
return false
}
// SetNextCursor gets a reference to the given NullableString and assigns it to the NextCursor field.
func (o *WebhookDeliveryListResponse) SetNextCursor(v string) {
o.NextCursor.Set(&v)
}
// SetNextCursorNil sets the value for NextCursor to be an explicit nil
func (o *WebhookDeliveryListResponse) SetNextCursorNil() {
o.NextCursor.Set(nil)
}
// UnsetNextCursor ensures that no value is present for NextCursor, not even an explicit nil
func (o *WebhookDeliveryListResponse) UnsetNextCursor() {
o.NextCursor.Unset()
}
func (o WebhookDeliveryListResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookDeliveryListResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["items"] = o.Items
if o.NextCursor.IsSet() {
toSerialize["next_cursor"] = o.NextCursor.Get()
}
return toSerialize, nil
}
func (o *WebhookDeliveryListResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"items",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookDeliveryListResponse := _WebhookDeliveryListResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookDeliveryListResponse)
if err != nil {
return err
}
*o = WebhookDeliveryListResponse(varWebhookDeliveryListResponse)
return err
}
type NullableWebhookDeliveryListResponse struct {
value *WebhookDeliveryListResponse
isSet bool
}
func (v NullableWebhookDeliveryListResponse) Get() *WebhookDeliveryListResponse {
return v.value
}
func (v *NullableWebhookDeliveryListResponse) Set(val *WebhookDeliveryListResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookDeliveryListResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookDeliveryListResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookDeliveryListResponse(val *WebhookDeliveryListResponse) *NullableWebhookDeliveryListResponse {
return &NullableWebhookDeliveryListResponse{value: val, isSet: true}
}
func (v NullableWebhookDeliveryListResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookDeliveryListResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,622 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookDeliveryResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookDeliveryResponse{}
// WebhookDeliveryResponse Response model for a webhook delivery record.
type WebhookDeliveryResponse struct {
Id string `json:"id"`
WebhookId NullableString `json:"webhook_id"`
Url string `json:"url"`
EventType string `json:"event_type"`
Status string `json:"status"`
Attempts int32 `json:"attempts"`
NextRetryAt NullableString `json:"next_retry_at,omitempty"`
LastError NullableString `json:"last_error,omitempty"`
LastResponseStatus NullableInt32 `json:"last_response_status,omitempty"`
LastResponseBody NullableString `json:"last_response_body,omitempty"`
LastAttemptAt NullableString `json:"last_attempt_at,omitempty"`
CreatedAt NullableString `json:"created_at,omitempty"`
UpdatedAt NullableString `json:"updated_at,omitempty"`
}
type _WebhookDeliveryResponse WebhookDeliveryResponse
// NewWebhookDeliveryResponse instantiates a new WebhookDeliveryResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookDeliveryResponse(id string, webhookId NullableString, url string, eventType string, status string, attempts int32) *WebhookDeliveryResponse {
this := WebhookDeliveryResponse{}
this.Id = id
this.WebhookId = webhookId
this.Url = url
this.EventType = eventType
this.Status = status
this.Attempts = attempts
return &this
}
// NewWebhookDeliveryResponseWithDefaults instantiates a new WebhookDeliveryResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookDeliveryResponseWithDefaults() *WebhookDeliveryResponse {
this := WebhookDeliveryResponse{}
return &this
}
// GetId returns the Id field value
func (o *WebhookDeliveryResponse) GetId() string {
if o == nil {
var ret string
return ret
}
return o.Id
}
// GetIdOk returns a tuple with the Id field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Id, true
}
// SetId sets field value
func (o *WebhookDeliveryResponse) SetId(v string) {
o.Id = v
}
// GetWebhookId returns the WebhookId field value
// If the value is explicit nil, the zero value for string will be returned
func (o *WebhookDeliveryResponse) GetWebhookId() string {
if o == nil || o.WebhookId.Get() == nil {
var ret string
return ret
}
return *o.WebhookId.Get()
}
// GetWebhookIdOk returns a tuple with the WebhookId field value
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetWebhookIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.WebhookId.Get(), o.WebhookId.IsSet()
}
// SetWebhookId sets field value
func (o *WebhookDeliveryResponse) SetWebhookId(v string) {
o.WebhookId.Set(&v)
}
// GetUrl returns the Url field value
func (o *WebhookDeliveryResponse) GetUrl() string {
if o == nil {
var ret string
return ret
}
return o.Url
}
// GetUrlOk returns a tuple with the Url field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Url, true
}
// SetUrl sets field value
func (o *WebhookDeliveryResponse) SetUrl(v string) {
o.Url = v
}
// GetEventType returns the EventType field value
func (o *WebhookDeliveryResponse) GetEventType() string {
if o == nil {
var ret string
return ret
}
return o.EventType
}
// GetEventTypeOk returns a tuple with the EventType field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetEventTypeOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.EventType, true
}
// SetEventType sets field value
func (o *WebhookDeliveryResponse) SetEventType(v string) {
o.EventType = v
}
// GetStatus returns the Status field value
func (o *WebhookDeliveryResponse) GetStatus() string {
if o == nil {
var ret string
return ret
}
return o.Status
}
// GetStatusOk returns a tuple with the Status field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetStatusOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Status, true
}
// SetStatus sets field value
func (o *WebhookDeliveryResponse) SetStatus(v string) {
o.Status = v
}
// GetAttempts returns the Attempts field value
func (o *WebhookDeliveryResponse) GetAttempts() int32 {
if o == nil {
var ret int32
return ret
}
return o.Attempts
}
// GetAttemptsOk returns a tuple with the Attempts field value
// and a boolean to check if the value has been set.
func (o *WebhookDeliveryResponse) GetAttemptsOk() (*int32, bool) {
if o == nil {
return nil, false
}
return &o.Attempts, true
}
// SetAttempts sets field value
func (o *WebhookDeliveryResponse) SetAttempts(v int32) {
o.Attempts = v
}
// GetNextRetryAt returns the NextRetryAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetNextRetryAt() string {
if o == nil || IsNil(o.NextRetryAt.Get()) {
var ret string
return ret
}
return *o.NextRetryAt.Get()
}
// GetNextRetryAtOk returns a tuple with the NextRetryAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetNextRetryAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.NextRetryAt.Get(), o.NextRetryAt.IsSet()
}
// HasNextRetryAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasNextRetryAt() bool {
if o != nil && o.NextRetryAt.IsSet() {
return true
}
return false
}
// SetNextRetryAt gets a reference to the given NullableString and assigns it to the NextRetryAt field.
func (o *WebhookDeliveryResponse) SetNextRetryAt(v string) {
o.NextRetryAt.Set(&v)
}
// SetNextRetryAtNil sets the value for NextRetryAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetNextRetryAtNil() {
o.NextRetryAt.Set(nil)
}
// UnsetNextRetryAt ensures that no value is present for NextRetryAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetNextRetryAt() {
o.NextRetryAt.Unset()
}
// GetLastError returns the LastError field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastError() string {
if o == nil || IsNil(o.LastError.Get()) {
var ret string
return ret
}
return *o.LastError.Get()
}
// GetLastErrorOk returns a tuple with the LastError field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastErrorOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.LastError.Get(), o.LastError.IsSet()
}
// HasLastError returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastError() bool {
if o != nil && o.LastError.IsSet() {
return true
}
return false
}
// SetLastError gets a reference to the given NullableString and assigns it to the LastError field.
func (o *WebhookDeliveryResponse) SetLastError(v string) {
o.LastError.Set(&v)
}
// SetLastErrorNil sets the value for LastError to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastErrorNil() {
o.LastError.Set(nil)
}
// UnsetLastError ensures that no value is present for LastError, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastError() {
o.LastError.Unset()
}
// GetLastResponseStatus returns the LastResponseStatus field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastResponseStatus() int32 {
if o == nil || IsNil(o.LastResponseStatus.Get()) {
var ret int32
return ret
}
return *o.LastResponseStatus.Get()
}
// GetLastResponseStatusOk returns a tuple with the LastResponseStatus field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastResponseStatusOk() (*int32, bool) {
if o == nil {
return nil, false
}
return o.LastResponseStatus.Get(), o.LastResponseStatus.IsSet()
}
// HasLastResponseStatus returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastResponseStatus() bool {
if o != nil && o.LastResponseStatus.IsSet() {
return true
}
return false
}
// SetLastResponseStatus gets a reference to the given NullableInt32 and assigns it to the LastResponseStatus field.
func (o *WebhookDeliveryResponse) SetLastResponseStatus(v int32) {
o.LastResponseStatus.Set(&v)
}
// SetLastResponseStatusNil sets the value for LastResponseStatus to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastResponseStatusNil() {
o.LastResponseStatus.Set(nil)
}
// UnsetLastResponseStatus ensures that no value is present for LastResponseStatus, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastResponseStatus() {
o.LastResponseStatus.Unset()
}
// GetLastResponseBody returns the LastResponseBody field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastResponseBody() string {
if o == nil || IsNil(o.LastResponseBody.Get()) {
var ret string
return ret
}
return *o.LastResponseBody.Get()
}
// GetLastResponseBodyOk returns a tuple with the LastResponseBody field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastResponseBodyOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.LastResponseBody.Get(), o.LastResponseBody.IsSet()
}
// HasLastResponseBody returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastResponseBody() bool {
if o != nil && o.LastResponseBody.IsSet() {
return true
}
return false
}
// SetLastResponseBody gets a reference to the given NullableString and assigns it to the LastResponseBody field.
func (o *WebhookDeliveryResponse) SetLastResponseBody(v string) {
o.LastResponseBody.Set(&v)
}
// SetLastResponseBodyNil sets the value for LastResponseBody to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastResponseBodyNil() {
o.LastResponseBody.Set(nil)
}
// UnsetLastResponseBody ensures that no value is present for LastResponseBody, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastResponseBody() {
o.LastResponseBody.Unset()
}
// GetLastAttemptAt returns the LastAttemptAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetLastAttemptAt() string {
if o == nil || IsNil(o.LastAttemptAt.Get()) {
var ret string
return ret
}
return *o.LastAttemptAt.Get()
}
// GetLastAttemptAtOk returns a tuple with the LastAttemptAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetLastAttemptAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.LastAttemptAt.Get(), o.LastAttemptAt.IsSet()
}
// HasLastAttemptAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasLastAttemptAt() bool {
if o != nil && o.LastAttemptAt.IsSet() {
return true
}
return false
}
// SetLastAttemptAt gets a reference to the given NullableString and assigns it to the LastAttemptAt field.
func (o *WebhookDeliveryResponse) SetLastAttemptAt(v string) {
o.LastAttemptAt.Set(&v)
}
// SetLastAttemptAtNil sets the value for LastAttemptAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetLastAttemptAtNil() {
o.LastAttemptAt.Set(nil)
}
// UnsetLastAttemptAt ensures that no value is present for LastAttemptAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetLastAttemptAt() {
o.LastAttemptAt.Unset()
}
// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetCreatedAt() string {
if o == nil || IsNil(o.CreatedAt.Get()) {
var ret string
return ret
}
return *o.CreatedAt.Get()
}
// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetCreatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.CreatedAt.Get(), o.CreatedAt.IsSet()
}
// HasCreatedAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasCreatedAt() bool {
if o != nil && o.CreatedAt.IsSet() {
return true
}
return false
}
// SetCreatedAt gets a reference to the given NullableString and assigns it to the CreatedAt field.
func (o *WebhookDeliveryResponse) SetCreatedAt(v string) {
o.CreatedAt.Set(&v)
}
// SetCreatedAtNil sets the value for CreatedAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetCreatedAtNil() {
o.CreatedAt.Set(nil)
}
// UnsetCreatedAt ensures that no value is present for CreatedAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetCreatedAt() {
o.CreatedAt.Unset()
}
// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookDeliveryResponse) GetUpdatedAt() string {
if o == nil || IsNil(o.UpdatedAt.Get()) {
var ret string
return ret
}
return *o.UpdatedAt.Get()
}
// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookDeliveryResponse) GetUpdatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.UpdatedAt.Get(), o.UpdatedAt.IsSet()
}
// HasUpdatedAt returns a boolean if a field has been set.
func (o *WebhookDeliveryResponse) HasUpdatedAt() bool {
if o != nil && o.UpdatedAt.IsSet() {
return true
}
return false
}
// SetUpdatedAt gets a reference to the given NullableString and assigns it to the UpdatedAt field.
func (o *WebhookDeliveryResponse) SetUpdatedAt(v string) {
o.UpdatedAt.Set(&v)
}
// SetUpdatedAtNil sets the value for UpdatedAt to be an explicit nil
func (o *WebhookDeliveryResponse) SetUpdatedAtNil() {
o.UpdatedAt.Set(nil)
}
// UnsetUpdatedAt ensures that no value is present for UpdatedAt, not even an explicit nil
func (o *WebhookDeliveryResponse) UnsetUpdatedAt() {
o.UpdatedAt.Unset()
}
func (o WebhookDeliveryResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookDeliveryResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["id"] = o.Id
toSerialize["webhook_id"] = o.WebhookId.Get()
toSerialize["url"] = o.Url
toSerialize["event_type"] = o.EventType
toSerialize["status"] = o.Status
toSerialize["attempts"] = o.Attempts
if o.NextRetryAt.IsSet() {
toSerialize["next_retry_at"] = o.NextRetryAt.Get()
}
if o.LastError.IsSet() {
toSerialize["last_error"] = o.LastError.Get()
}
if o.LastResponseStatus.IsSet() {
toSerialize["last_response_status"] = o.LastResponseStatus.Get()
}
if o.LastResponseBody.IsSet() {
toSerialize["last_response_body"] = o.LastResponseBody.Get()
}
if o.LastAttemptAt.IsSet() {
toSerialize["last_attempt_at"] = o.LastAttemptAt.Get()
}
if o.CreatedAt.IsSet() {
toSerialize["created_at"] = o.CreatedAt.Get()
}
if o.UpdatedAt.IsSet() {
toSerialize["updated_at"] = o.UpdatedAt.Get()
}
return toSerialize, nil
}
func (o *WebhookDeliveryResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"id",
"webhook_id",
"url",
"event_type",
"status",
"attempts",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookDeliveryResponse := _WebhookDeliveryResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookDeliveryResponse)
if err != nil {
return err
}
*o = WebhookDeliveryResponse(varWebhookDeliveryResponse)
return err
}
type NullableWebhookDeliveryResponse struct {
value *WebhookDeliveryResponse
isSet bool
}
func (v NullableWebhookDeliveryResponse) Get() *WebhookDeliveryResponse {
return v.value
}
func (v *NullableWebhookDeliveryResponse) Set(val *WebhookDeliveryResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookDeliveryResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookDeliveryResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookDeliveryResponse(val *WebhookDeliveryResponse) *NullableWebhookDeliveryResponse {
return &NullableWebhookDeliveryResponse{value: val, isSet: true}
}
func (v NullableWebhookDeliveryResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookDeliveryResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,246 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
)
// checks if the WebhookHttpConfig type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookHttpConfig{}
// WebhookHttpConfig HTTP delivery configuration for a webhook.
type WebhookHttpConfig struct {
// HTTP method: GET or POST
Method *string `json:"method,omitempty"`
// HTTP request timeout in seconds
TimeoutSeconds *int32 `json:"timeout_seconds,omitempty"`
// Custom HTTP headers
Headers map[string]string `json:"headers,omitempty"`
// Custom HTTP query parameters
Params map[string]string `json:"params,omitempty"`
}
// NewWebhookHttpConfig instantiates a new WebhookHttpConfig object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookHttpConfig() *WebhookHttpConfig {
this := WebhookHttpConfig{}
var method string = "POST"
this.Method = &method
var timeoutSeconds int32 = 30
this.TimeoutSeconds = &timeoutSeconds
return &this
}
// NewWebhookHttpConfigWithDefaults instantiates a new WebhookHttpConfig object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookHttpConfigWithDefaults() *WebhookHttpConfig {
this := WebhookHttpConfig{}
var method string = "POST"
this.Method = &method
var timeoutSeconds int32 = 30
this.TimeoutSeconds = &timeoutSeconds
return &this
}
// GetMethod returns the Method field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetMethod() string {
if o == nil || IsNil(o.Method) {
var ret string
return ret
}
return *o.Method
}
// GetMethodOk returns a tuple with the Method field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetMethodOk() (*string, bool) {
if o == nil || IsNil(o.Method) {
return nil, false
}
return o.Method, true
}
// HasMethod returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasMethod() bool {
if o != nil && !IsNil(o.Method) {
return true
}
return false
}
// SetMethod gets a reference to the given string and assigns it to the Method field.
func (o *WebhookHttpConfig) SetMethod(v string) {
o.Method = &v
}
// GetTimeoutSeconds returns the TimeoutSeconds field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetTimeoutSeconds() int32 {
if o == nil || IsNil(o.TimeoutSeconds) {
var ret int32
return ret
}
return *o.TimeoutSeconds
}
// GetTimeoutSecondsOk returns a tuple with the TimeoutSeconds field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetTimeoutSecondsOk() (*int32, bool) {
if o == nil || IsNil(o.TimeoutSeconds) {
return nil, false
}
return o.TimeoutSeconds, true
}
// HasTimeoutSeconds returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasTimeoutSeconds() bool {
if o != nil && !IsNil(o.TimeoutSeconds) {
return true
}
return false
}
// SetTimeoutSeconds gets a reference to the given int32 and assigns it to the TimeoutSeconds field.
func (o *WebhookHttpConfig) SetTimeoutSeconds(v int32) {
o.TimeoutSeconds = &v
}
// GetHeaders returns the Headers field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetHeaders() map[string]string {
if o == nil || IsNil(o.Headers) {
var ret map[string]string
return ret
}
return o.Headers
}
// GetHeadersOk returns a tuple with the Headers field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetHeadersOk() (map[string]string, bool) {
if o == nil || IsNil(o.Headers) {
return map[string]string{}, false
}
return o.Headers, true
}
// HasHeaders returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasHeaders() bool {
if o != nil && !IsNil(o.Headers) {
return true
}
return false
}
// SetHeaders gets a reference to the given map[string]string and assigns it to the Headers field.
func (o *WebhookHttpConfig) SetHeaders(v map[string]string) {
o.Headers = v
}
// GetParams returns the Params field value if set, zero value otherwise.
func (o *WebhookHttpConfig) GetParams() map[string]string {
if o == nil || IsNil(o.Params) {
var ret map[string]string
return ret
}
return o.Params
}
// GetParamsOk returns a tuple with the Params field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookHttpConfig) GetParamsOk() (map[string]string, bool) {
if o == nil || IsNil(o.Params) {
return map[string]string{}, false
}
return o.Params, true
}
// HasParams returns a boolean if a field has been set.
func (o *WebhookHttpConfig) HasParams() bool {
if o != nil && !IsNil(o.Params) {
return true
}
return false
}
// SetParams gets a reference to the given map[string]string and assigns it to the Params field.
func (o *WebhookHttpConfig) SetParams(v map[string]string) {
o.Params = v
}
func (o WebhookHttpConfig) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookHttpConfig) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if !IsNil(o.Method) {
toSerialize["method"] = o.Method
}
if !IsNil(o.TimeoutSeconds) {
toSerialize["timeout_seconds"] = o.TimeoutSeconds
}
if !IsNil(o.Headers) {
toSerialize["headers"] = o.Headers
}
if !IsNil(o.Params) {
toSerialize["params"] = o.Params
}
return toSerialize, nil
}
type NullableWebhookHttpConfig struct {
value *WebhookHttpConfig
isSet bool
}
func (v NullableWebhookHttpConfig) Get() *WebhookHttpConfig {
return v.value
}
func (v *NullableWebhookHttpConfig) Set(val *WebhookHttpConfig) {
v.value = val
v.isSet = true
}
func (v NullableWebhookHttpConfig) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookHttpConfig) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookHttpConfig(val *WebhookHttpConfig) *NullableWebhookHttpConfig {
return &NullableWebhookHttpConfig{value: val, isSet: true}
}
func (v NullableWebhookHttpConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookHttpConfig) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,158 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookListResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookListResponse{}
// WebhookListResponse Response model for listing webhooks.
type WebhookListResponse struct {
Items []WebhookResponse `json:"items"`
}
type _WebhookListResponse WebhookListResponse
// NewWebhookListResponse instantiates a new WebhookListResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookListResponse(items []WebhookResponse) *WebhookListResponse {
this := WebhookListResponse{}
this.Items = items
return &this
}
// NewWebhookListResponseWithDefaults instantiates a new WebhookListResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookListResponseWithDefaults() *WebhookListResponse {
this := WebhookListResponse{}
return &this
}
// GetItems returns the Items field value
func (o *WebhookListResponse) GetItems() []WebhookResponse {
if o == nil {
var ret []WebhookResponse
return ret
}
return o.Items
}
// GetItemsOk returns a tuple with the Items field value
// and a boolean to check if the value has been set.
func (o *WebhookListResponse) GetItemsOk() ([]WebhookResponse, bool) {
if o == nil {
return nil, false
}
return o.Items, true
}
// SetItems sets field value
func (o *WebhookListResponse) SetItems(v []WebhookResponse) {
o.Items = v
}
func (o WebhookListResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookListResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["items"] = o.Items
return toSerialize, nil
}
func (o *WebhookListResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"items",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookListResponse := _WebhookListResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookListResponse)
if err != nil {
return err
}
*o = WebhookListResponse(varWebhookListResponse)
return err
}
type NullableWebhookListResponse struct {
value *WebhookListResponse
isSet bool
}
func (v NullableWebhookListResponse) Get() *WebhookListResponse {
return v.value
}
func (v *NullableWebhookListResponse) Set(val *WebhookListResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookListResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookListResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookListResponse(val *WebhookListResponse) *NullableWebhookListResponse {
return &NullableWebhookListResponse{value: val, isSet: true}
}
func (v NullableWebhookListResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookListResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -0,0 +1,446 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.4.15
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the WebhookResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &WebhookResponse{}
// WebhookResponse Response model for a webhook.
type WebhookResponse struct {
Id string `json:"id"`
BankId NullableString `json:"bank_id"`
Url string `json:"url"`
Secret NullableString `json:"secret,omitempty"`
EventTypes []string `json:"event_types"`
Enabled bool `json:"enabled"`
HttpConfig *WebhookHttpConfig `json:"http_config,omitempty"`
CreatedAt NullableString `json:"created_at,omitempty"`
UpdatedAt NullableString `json:"updated_at,omitempty"`
}
type _WebhookResponse WebhookResponse
// NewWebhookResponse instantiates a new WebhookResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewWebhookResponse(id string, bankId NullableString, url string, eventTypes []string, enabled bool) *WebhookResponse {
this := WebhookResponse{}
this.Id = id
this.BankId = bankId
this.Url = url
this.EventTypes = eventTypes
this.Enabled = enabled
return &this
}
// NewWebhookResponseWithDefaults instantiates a new WebhookResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewWebhookResponseWithDefaults() *WebhookResponse {
this := WebhookResponse{}
return &this
}
// GetId returns the Id field value
func (o *WebhookResponse) GetId() string {
if o == nil {
var ret string
return ret
}
return o.Id
}
// GetIdOk returns a tuple with the Id field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Id, true
}
// SetId sets field value
func (o *WebhookResponse) SetId(v string) {
o.Id = v
}
// GetBankId returns the BankId field value
// If the value is explicit nil, the zero value for string will be returned
func (o *WebhookResponse) GetBankId() string {
if o == nil || o.BankId.Get() == nil {
var ret string
return ret
}
return *o.BankId.Get()
}
// GetBankIdOk returns a tuple with the BankId field value
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetBankIdOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.BankId.Get(), o.BankId.IsSet()
}
// SetBankId sets field value
func (o *WebhookResponse) SetBankId(v string) {
o.BankId.Set(&v)
}
// GetUrl returns the Url field value
func (o *WebhookResponse) GetUrl() string {
if o == nil {
var ret string
return ret
}
return o.Url
}
// GetUrlOk returns a tuple with the Url field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetUrlOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Url, true
}
// SetUrl sets field value
func (o *WebhookResponse) SetUrl(v string) {
o.Url = v
}
// GetSecret returns the Secret field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookResponse) GetSecret() string {
if o == nil || IsNil(o.Secret.Get()) {
var ret string
return ret
}
return *o.Secret.Get()
}
// GetSecretOk returns a tuple with the Secret field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetSecretOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.Secret.Get(), o.Secret.IsSet()
}
// HasSecret returns a boolean if a field has been set.
func (o *WebhookResponse) HasSecret() bool {
if o != nil && o.Secret.IsSet() {
return true
}
return false
}
// SetSecret gets a reference to the given NullableString and assigns it to the Secret field.
func (o *WebhookResponse) SetSecret(v string) {
o.Secret.Set(&v)
}
// SetSecretNil sets the value for Secret to be an explicit nil
func (o *WebhookResponse) SetSecretNil() {
o.Secret.Set(nil)
}
// UnsetSecret ensures that no value is present for Secret, not even an explicit nil
func (o *WebhookResponse) UnsetSecret() {
o.Secret.Unset()
}
// GetEventTypes returns the EventTypes field value
func (o *WebhookResponse) GetEventTypes() []string {
if o == nil {
var ret []string
return ret
}
return o.EventTypes
}
// GetEventTypesOk returns a tuple with the EventTypes field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetEventTypesOk() ([]string, bool) {
if o == nil {
return nil, false
}
return o.EventTypes, true
}
// SetEventTypes sets field value
func (o *WebhookResponse) SetEventTypes(v []string) {
o.EventTypes = v
}
// GetEnabled returns the Enabled field value
func (o *WebhookResponse) GetEnabled() bool {
if o == nil {
var ret bool
return ret
}
return o.Enabled
}
// GetEnabledOk returns a tuple with the Enabled field value
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetEnabledOk() (*bool, bool) {
if o == nil {
return nil, false
}
return &o.Enabled, true
}
// SetEnabled sets field value
func (o *WebhookResponse) SetEnabled(v bool) {
o.Enabled = v
}
// GetHttpConfig returns the HttpConfig field value if set, zero value otherwise.
func (o *WebhookResponse) GetHttpConfig() WebhookHttpConfig {
if o == nil || IsNil(o.HttpConfig) {
var ret WebhookHttpConfig
return ret
}
return *o.HttpConfig
}
// GetHttpConfigOk returns a tuple with the HttpConfig field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *WebhookResponse) GetHttpConfigOk() (*WebhookHttpConfig, bool) {
if o == nil || IsNil(o.HttpConfig) {
return nil, false
}
return o.HttpConfig, true
}
// HasHttpConfig returns a boolean if a field has been set.
func (o *WebhookResponse) HasHttpConfig() bool {
if o != nil && !IsNil(o.HttpConfig) {
return true
}
return false
}
// SetHttpConfig gets a reference to the given WebhookHttpConfig and assigns it to the HttpConfig field.
func (o *WebhookResponse) SetHttpConfig(v WebhookHttpConfig) {
o.HttpConfig = &v
}
// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookResponse) GetCreatedAt() string {
if o == nil || IsNil(o.CreatedAt.Get()) {
var ret string
return ret
}
return *o.CreatedAt.Get()
}
// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetCreatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.CreatedAt.Get(), o.CreatedAt.IsSet()
}
// HasCreatedAt returns a boolean if a field has been set.
func (o *WebhookResponse) HasCreatedAt() bool {
if o != nil && o.CreatedAt.IsSet() {
return true
}
return false
}
// SetCreatedAt gets a reference to the given NullableString and assigns it to the CreatedAt field.
func (o *WebhookResponse) SetCreatedAt(v string) {
o.CreatedAt.Set(&v)
}
// SetCreatedAtNil sets the value for CreatedAt to be an explicit nil
func (o *WebhookResponse) SetCreatedAtNil() {
o.CreatedAt.Set(nil)
}
// UnsetCreatedAt ensures that no value is present for CreatedAt, not even an explicit nil
func (o *WebhookResponse) UnsetCreatedAt() {
o.CreatedAt.Unset()
}
// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *WebhookResponse) GetUpdatedAt() string {
if o == nil || IsNil(o.UpdatedAt.Get()) {
var ret string
return ret
}
return *o.UpdatedAt.Get()
}
// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *WebhookResponse) GetUpdatedAtOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.UpdatedAt.Get(), o.UpdatedAt.IsSet()
}
// HasUpdatedAt returns a boolean if a field has been set.
func (o *WebhookResponse) HasUpdatedAt() bool {
if o != nil && o.UpdatedAt.IsSet() {
return true
}
return false
}
// SetUpdatedAt gets a reference to the given NullableString and assigns it to the UpdatedAt field.
func (o *WebhookResponse) SetUpdatedAt(v string) {
o.UpdatedAt.Set(&v)
}
// SetUpdatedAtNil sets the value for UpdatedAt to be an explicit nil
func (o *WebhookResponse) SetUpdatedAtNil() {
o.UpdatedAt.Set(nil)
}
// UnsetUpdatedAt ensures that no value is present for UpdatedAt, not even an explicit nil
func (o *WebhookResponse) UnsetUpdatedAt() {
o.UpdatedAt.Unset()
}
func (o WebhookResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o WebhookResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["id"] = o.Id
toSerialize["bank_id"] = o.BankId.Get()
toSerialize["url"] = o.Url
if o.Secret.IsSet() {
toSerialize["secret"] = o.Secret.Get()
}
toSerialize["event_types"] = o.EventTypes
toSerialize["enabled"] = o.Enabled
if !IsNil(o.HttpConfig) {
toSerialize["http_config"] = o.HttpConfig
}
if o.CreatedAt.IsSet() {
toSerialize["created_at"] = o.CreatedAt.Get()
}
if o.UpdatedAt.IsSet() {
toSerialize["updated_at"] = o.UpdatedAt.Get()
}
return toSerialize, nil
}
func (o *WebhookResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"id",
"bank_id",
"url",
"event_types",
"enabled",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varWebhookResponse := _WebhookResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varWebhookResponse)
if err != nil {
return err
}
*o = WebhookResponse(varWebhookResponse)
return err
}
type NullableWebhookResponse struct {
value *WebhookResponse
isSet bool
}
func (v NullableWebhookResponse) Get() *WebhookResponse {
return v.value
}
func (v *NullableWebhookResponse) Set(val *WebhookResponse) {
v.value = val
v.isSet = true
}
func (v NullableWebhookResponse) IsSet() bool {
return v.isSet
}
func (v *NullableWebhookResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableWebhookResponse(val *WebhookResponse) *NullableWebhookResponse {
return &NullableWebhookResponse{value: val, isSet: true}
}
func (v NullableWebhookResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableWebhookResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View file

@ -9,6 +9,7 @@ hindsight_client_api/api/memory_api.py
hindsight_client_api/api/mental_models_api.py
hindsight_client_api/api/monitoring_api.py
hindsight_client_api/api/operations_api.py
hindsight_client_api/api/webhooks_api.py
hindsight_client_api/api_client.py
hindsight_client_api/api_response.py
hindsight_client_api/configuration.py
@ -35,6 +36,7 @@ hindsight_client_api/models/create_bank_request.py
hindsight_client_api/models/create_directive_request.py
hindsight_client_api/models/create_mental_model_request.py
hindsight_client_api/models/create_mental_model_response.py
hindsight_client_api/models/create_webhook_request.py
hindsight_client_api/models/delete_document_response.py
hindsight_client_api/models/delete_response.py
hindsight_client_api/models/directive_list_response.py
@ -87,8 +89,14 @@ hindsight_client_api/models/tool_calls_include_options.py
hindsight_client_api/models/update_directive_request.py
hindsight_client_api/models/update_disposition_request.py
hindsight_client_api/models/update_mental_model_request.py
hindsight_client_api/models/update_webhook_request.py
hindsight_client_api/models/validation_error.py
hindsight_client_api/models/validation_error_loc_inner.py
hindsight_client_api/models/version_response.py
hindsight_client_api/models/webhook_delivery_list_response.py
hindsight_client_api/models/webhook_delivery_response.py
hindsight_client_api/models/webhook_http_config.py
hindsight_client_api/models/webhook_list_response.py
hindsight_client_api/models/webhook_response.py
hindsight_client_api/rest.py
hindsight_client_api_README.md

View file

@ -26,6 +26,7 @@ from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.mental_models_api import MentalModelsApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.webhooks_api import WebhooksApi
# import ApiClient
from hindsight_client_api.api_response import ApiResponse
@ -60,6 +61,7 @@ from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
from hindsight_client_api.models.create_webhook_request import CreateWebhookRequest
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.directive_list_response import DirectiveListResponse
@ -112,6 +114,12 @@ from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncl
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.version_response import VersionResponse
from hindsight_client_api.models.webhook_delivery_list_response import WebhookDeliveryListResponse
from hindsight_client_api.models.webhook_delivery_response import WebhookDeliveryResponse
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from hindsight_client_api.models.webhook_list_response import WebhookListResponse
from hindsight_client_api.models.webhook_response import WebhookResponse

View file

@ -10,4 +10,5 @@ from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.mental_models_api import MentalModelsApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.webhooks_api import WebhooksApi

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@ from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
from hindsight_client_api.models.create_webhook_request import CreateWebhookRequest
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.directive_list_response import DirectiveListResponse
@ -87,6 +88,12 @@ from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncl
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.version_response import VersionResponse
from hindsight_client_api.models.webhook_delivery_list_response import WebhookDeliveryListResponse
from hindsight_client_api.models.webhook_delivery_response import WebhookDeliveryResponse
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from hindsight_client_api.models.webhook_list_response import WebhookListResponse
from hindsight_client_api.models.webhook_response import WebhookResponse

View file

@ -0,0 +1,104 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from typing import Optional, Set
from typing_extensions import Self
class CreateWebhookRequest(BaseModel):
"""
Request model for registering a webhook.
""" # noqa: E501
url: StrictStr = Field(description="HTTP(S) endpoint URL to deliver events to")
secret: Optional[StrictStr] = None
event_types: Optional[List[StrictStr]] = Field(default=None, description="List of event types to deliver. Currently supported: 'consolidation.completed'")
enabled: Optional[StrictBool] = Field(default=True, description="Whether this webhook is active")
http_config: Optional[WebhookHttpConfig] = Field(default=None, description="HTTP delivery configuration (method, timeout, headers, params)")
__properties: ClassVar[List[str]] = ["url", "secret", "event_types", "enabled", "http_config"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of CreateWebhookRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of http_config
if self.http_config:
_dict['http_config'] = self.http_config.to_dict()
# set to None if secret (nullable) is None
# and model_fields_set contains the field
if self.secret is None and "secret" in self.model_fields_set:
_dict['secret'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of CreateWebhookRequest from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"url": obj.get("url"),
"secret": obj.get("secret"),
"event_types": obj.get("event_types"),
"enabled": obj.get("enabled") if obj.get("enabled") is not None else True,
"http_config": WebhookHttpConfig.from_dict(obj["http_config"]) if obj.get("http_config") is not None else None
})
return _obj

View file

@ -0,0 +1,124 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from typing import Optional, Set
from typing_extensions import Self
class UpdateWebhookRequest(BaseModel):
"""
Request model for updating a webhook. Only provided fields are updated.
""" # noqa: E501
url: Optional[StrictStr] = None
secret: Optional[StrictStr] = None
event_types: Optional[List[StrictStr]] = None
enabled: Optional[StrictBool] = None
http_config: Optional[WebhookHttpConfig] = None
__properties: ClassVar[List[str]] = ["url", "secret", "event_types", "enabled", "http_config"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of UpdateWebhookRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of http_config
if self.http_config:
_dict['http_config'] = self.http_config.to_dict()
# set to None if url (nullable) is None
# and model_fields_set contains the field
if self.url is None and "url" in self.model_fields_set:
_dict['url'] = None
# set to None if secret (nullable) is None
# and model_fields_set contains the field
if self.secret is None and "secret" in self.model_fields_set:
_dict['secret'] = None
# set to None if event_types (nullable) is None
# and model_fields_set contains the field
if self.event_types is None and "event_types" in self.model_fields_set:
_dict['event_types'] = None
# set to None if enabled (nullable) is None
# and model_fields_set contains the field
if self.enabled is None and "enabled" in self.model_fields_set:
_dict['enabled'] = None
# set to None if http_config (nullable) is None
# and model_fields_set contains the field
if self.http_config is None and "http_config" in self.model_fields_set:
_dict['http_config'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of UpdateWebhookRequest from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"url": obj.get("url"),
"secret": obj.get("secret"),
"event_types": obj.get("event_types"),
"enabled": obj.get("enabled"),
"http_config": WebhookHttpConfig.from_dict(obj["http_config"]) if obj.get("http_config") is not None else None
})
return _obj

View file

@ -0,0 +1,102 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_delivery_response import WebhookDeliveryResponse
from typing import Optional, Set
from typing_extensions import Self
class WebhookDeliveryListResponse(BaseModel):
"""
Response model for listing webhook deliveries.
""" # noqa: E501
items: List[WebhookDeliveryResponse]
next_cursor: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["items", "next_cursor"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookDeliveryListResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of each item in items (list)
_items = []
if self.items:
for _item_items in self.items:
if _item_items:
_items.append(_item_items.to_dict())
_dict['items'] = _items
# set to None if next_cursor (nullable) is None
# and model_fields_set contains the field
if self.next_cursor is None and "next_cursor" in self.model_fields_set:
_dict['next_cursor'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookDeliveryListResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"items": [WebhookDeliveryResponse.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None,
"next_cursor": obj.get("next_cursor")
})
return _obj

View file

@ -0,0 +1,151 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class WebhookDeliveryResponse(BaseModel):
"""
Response model for a webhook delivery record.
""" # noqa: E501
id: StrictStr
webhook_id: Optional[StrictStr]
url: StrictStr
event_type: StrictStr
status: StrictStr
attempts: StrictInt
next_retry_at: Optional[StrictStr] = None
last_error: Optional[StrictStr] = None
last_response_status: Optional[StrictInt] = None
last_response_body: Optional[StrictStr] = None
last_attempt_at: Optional[StrictStr] = None
created_at: Optional[StrictStr] = None
updated_at: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["id", "webhook_id", "url", "event_type", "status", "attempts", "next_retry_at", "last_error", "last_response_status", "last_response_body", "last_attempt_at", "created_at", "updated_at"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookDeliveryResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# set to None if webhook_id (nullable) is None
# and model_fields_set contains the field
if self.webhook_id is None and "webhook_id" in self.model_fields_set:
_dict['webhook_id'] = None
# set to None if next_retry_at (nullable) is None
# and model_fields_set contains the field
if self.next_retry_at is None and "next_retry_at" in self.model_fields_set:
_dict['next_retry_at'] = None
# set to None if last_error (nullable) is None
# and model_fields_set contains the field
if self.last_error is None and "last_error" in self.model_fields_set:
_dict['last_error'] = None
# set to None if last_response_status (nullable) is None
# and model_fields_set contains the field
if self.last_response_status is None and "last_response_status" in self.model_fields_set:
_dict['last_response_status'] = None
# set to None if last_response_body (nullable) is None
# and model_fields_set contains the field
if self.last_response_body is None and "last_response_body" in self.model_fields_set:
_dict['last_response_body'] = None
# set to None if last_attempt_at (nullable) is None
# and model_fields_set contains the field
if self.last_attempt_at is None and "last_attempt_at" in self.model_fields_set:
_dict['last_attempt_at'] = None
# set to None if created_at (nullable) is None
# and model_fields_set contains the field
if self.created_at is None and "created_at" in self.model_fields_set:
_dict['created_at'] = None
# set to None if updated_at (nullable) is None
# and model_fields_set contains the field
if self.updated_at is None and "updated_at" in self.model_fields_set:
_dict['updated_at'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookDeliveryResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"id": obj.get("id"),
"webhook_id": obj.get("webhook_id"),
"url": obj.get("url"),
"event_type": obj.get("event_type"),
"status": obj.get("status"),
"attempts": obj.get("attempts"),
"next_retry_at": obj.get("next_retry_at"),
"last_error": obj.get("last_error"),
"last_response_status": obj.get("last_response_status"),
"last_response_body": obj.get("last_response_body"),
"last_attempt_at": obj.get("last_attempt_at"),
"created_at": obj.get("created_at"),
"updated_at": obj.get("updated_at")
})
return _obj

View file

@ -0,0 +1,93 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class WebhookHttpConfig(BaseModel):
"""
HTTP delivery configuration for a webhook.
""" # noqa: E501
method: Optional[StrictStr] = Field(default='POST', description="HTTP method: GET or POST")
timeout_seconds: Optional[StrictInt] = Field(default=30, description="HTTP request timeout in seconds")
headers: Optional[Dict[str, StrictStr]] = Field(default=None, description="Custom HTTP headers")
params: Optional[Dict[str, StrictStr]] = Field(default=None, description="Custom HTTP query parameters")
__properties: ClassVar[List[str]] = ["method", "timeout_seconds", "headers", "params"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookHttpConfig from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookHttpConfig from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"method": obj.get("method") if obj.get("method") is not None else 'POST',
"timeout_seconds": obj.get("timeout_seconds") if obj.get("timeout_seconds") is not None else 30,
"headers": obj.get("headers"),
"params": obj.get("params")
})
return _obj

View file

@ -0,0 +1,95 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict
from typing import Any, ClassVar, Dict, List
from hindsight_client_api.models.webhook_response import WebhookResponse
from typing import Optional, Set
from typing_extensions import Self
class WebhookListResponse(BaseModel):
"""
Response model for listing webhooks.
""" # noqa: E501
items: List[WebhookResponse]
__properties: ClassVar[List[str]] = ["items"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookListResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of each item in items (list)
_items = []
if self.items:
for _item_items in self.items:
if _item_items:
_items.append(_item_items.to_dict())
_dict['items'] = _items
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookListResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"items": [WebhookResponse.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None
})
return _obj

View file

@ -0,0 +1,127 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.4.15
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.webhook_http_config import WebhookHttpConfig
from typing import Optional, Set
from typing_extensions import Self
class WebhookResponse(BaseModel):
"""
Response model for a webhook.
""" # noqa: E501
id: StrictStr
bank_id: Optional[StrictStr]
url: StrictStr
secret: Optional[StrictStr] = None
event_types: List[StrictStr]
enabled: StrictBool
http_config: Optional[WebhookHttpConfig] = None
created_at: Optional[StrictStr] = None
updated_at: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["id", "bank_id", "url", "secret", "event_types", "enabled", "http_config", "created_at", "updated_at"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of WebhookResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of http_config
if self.http_config:
_dict['http_config'] = self.http_config.to_dict()
# set to None if bank_id (nullable) is None
# and model_fields_set contains the field
if self.bank_id is None and "bank_id" in self.model_fields_set:
_dict['bank_id'] = None
# set to None if secret (nullable) is None
# and model_fields_set contains the field
if self.secret is None and "secret" in self.model_fields_set:
_dict['secret'] = None
# set to None if created_at (nullable) is None
# and model_fields_set contains the field
if self.created_at is None and "created_at" in self.model_fields_set:
_dict['created_at'] = None
# set to None if updated_at (nullable) is None
# and model_fields_set contains the field
if self.updated_at is None and "updated_at" in self.model_fields_set:
_dict['updated_at'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of WebhookResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"id": obj.get("id"),
"bank_id": obj.get("bank_id"),
"url": obj.get("url"),
"secret": obj.get("secret"),
"event_types": obj.get("event_types"),
"enabled": obj.get("enabled"),
"http_config": WebhookHttpConfig.from_dict(obj["http_config"]) if obj.get("http_config") is not None else None,
"created_at": obj.get("created_at"),
"updated_at": obj.get("updated_at")
})
return _obj

View file

@ -32,6 +32,9 @@ import type {
CreateOrUpdateBankData,
CreateOrUpdateBankErrors,
CreateOrUpdateBankResponses,
CreateWebhookData,
CreateWebhookErrors,
CreateWebhookResponses,
DeleteBankData,
DeleteBankErrors,
DeleteBankResponses,
@ -44,6 +47,9 @@ import type {
DeleteMentalModelData,
DeleteMentalModelErrors,
DeleteMentalModelResponses,
DeleteWebhookData,
DeleteWebhookErrors,
DeleteWebhookResponses,
FileRetainData,
FileRetainErrors,
FileRetainResponses,
@ -108,6 +114,12 @@ import type {
ListTagsData,
ListTagsErrors,
ListTagsResponses,
ListWebhookDeliveriesData,
ListWebhookDeliveriesErrors,
ListWebhookDeliveriesResponses,
ListWebhooksData,
ListWebhooksErrors,
ListWebhooksResponses,
MetricsEndpointMetricsGetData,
MetricsEndpointMetricsGetResponses,
RecallMemoriesData,
@ -146,6 +158,9 @@ import type {
UpdateMentalModelData,
UpdateMentalModelErrors,
UpdateMentalModelResponses,
UpdateWebhookData,
UpdateWebhookErrors,
UpdateWebhookResponses,
} from "./types.gen";
export type Options<
@ -912,6 +927,93 @@ export const triggerConsolidation = <ThrowOnError extends boolean = false>(
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/consolidate", ...options });
/**
* List webhooks
*
* List all webhooks registered for a bank.
*/
export const listWebhooks = <ThrowOnError extends boolean = false>(
options: Options<ListWebhooksData, ThrowOnError>,
) =>
(options.client ?? client).get<
ListWebhooksResponses,
ListWebhooksErrors,
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/webhooks", ...options });
/**
* Register webhook
*
* Register a webhook endpoint to receive event notifications for this bank.
*/
export const createWebhook = <ThrowOnError extends boolean = false>(
options: Options<CreateWebhookData, ThrowOnError>,
) =>
(options.client ?? client).post<
CreateWebhookResponses,
CreateWebhookErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/webhooks",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
/**
* Delete webhook
*
* Remove a registered webhook.
*/
export const deleteWebhook = <ThrowOnError extends boolean = false>(
options: Options<DeleteWebhookData, ThrowOnError>,
) =>
(options.client ?? client).delete<
DeleteWebhookResponses,
DeleteWebhookErrors,
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}", ...options });
/**
* Update webhook
*
* Update one or more fields of a registered webhook. Only provided fields are changed.
*/
export const updateWebhook = <ThrowOnError extends boolean = false>(
options: Options<UpdateWebhookData, ThrowOnError>,
) =>
(options.client ?? client).patch<
UpdateWebhookResponses,
UpdateWebhookErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
/**
* List webhook deliveries
*
* Inspect delivery history for a webhook (useful for debugging).
*/
export const listWebhookDeliveries = <ThrowOnError extends boolean = false>(
options: Options<ListWebhookDeliveriesData, ThrowOnError>,
) =>
(options.client ?? client).get<
ListWebhookDeliveriesResponses,
ListWebhookDeliveriesErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries",
...options,
});
/**
* Clear memory bank memories
*

View file

@ -618,6 +618,42 @@ export type CreateMentalModelResponse = {
operation_id: string;
};
/**
* CreateWebhookRequest
*
* Request model for registering a webhook.
*/
export type CreateWebhookRequest = {
/**
* Url
*
* HTTP(S) endpoint URL to deliver events to
*/
url: string;
/**
* Secret
*
* HMAC-SHA256 signing secret (optional)
*/
secret?: string | null;
/**
* Event Types
*
* List of event types to deliver. Currently supported: 'consolidation.completed'
*/
event_types?: Array<string>;
/**
* Enabled
*
* Whether this webhook is active
*/
enabled?: boolean;
/**
* HTTP delivery configuration (method, timeout, headers, params)
*/
http_config?: WebhookHttpConfig;
};
/**
* DeleteDocumentResponse
*
@ -2075,6 +2111,42 @@ export type UpdateMentalModelRequest = {
trigger?: MentalModelTrigger | null;
};
/**
* UpdateWebhookRequest
*
* Request model for updating a webhook. Only provided fields are updated.
*/
export type UpdateWebhookRequest = {
/**
* Url
*
* HTTP(S) endpoint URL
*/
url?: string | null;
/**
* Secret
*
* HMAC-SHA256 signing secret. Omit to keep existing; send null to clear.
*/
secret?: string | null;
/**
* Event Types
*
* List of event types
*/
event_types?: Array<string> | null;
/**
* Enabled
*
* Whether this webhook is active
*/
enabled?: boolean | null;
/**
* HTTP delivery configuration
*/
http_config?: WebhookHttpConfig | null;
};
/**
* ValidationError
*/
@ -2111,6 +2183,173 @@ export type VersionResponse = {
features: FeaturesInfo;
};
/**
* WebhookDeliveryListResponse
*
* Response model for listing webhook deliveries.
*/
export type WebhookDeliveryListResponse = {
/**
* Items
*/
items: Array<WebhookDeliveryResponse>;
/**
* Next Cursor
*/
next_cursor?: string | null;
};
/**
* WebhookDeliveryResponse
*
* Response model for a webhook delivery record.
*/
export type WebhookDeliveryResponse = {
/**
* Id
*/
id: string;
/**
* Webhook Id
*/
webhook_id: string | null;
/**
* Url
*/
url: string;
/**
* Event Type
*/
event_type: string;
/**
* Status
*/
status: string;
/**
* Attempts
*/
attempts: number;
/**
* Next Retry At
*/
next_retry_at?: string | null;
/**
* Last Error
*/
last_error?: string | null;
/**
* Last Response Status
*/
last_response_status?: number | null;
/**
* Last Response Body
*/
last_response_body?: string | null;
/**
* Last Attempt At
*/
last_attempt_at?: string | null;
/**
* Created At
*/
created_at?: string | null;
/**
* Updated At
*/
updated_at?: string | null;
};
/**
* WebhookHttpConfig
*
* HTTP delivery configuration for a webhook.
*/
export type WebhookHttpConfig = {
/**
* Method
*
* HTTP method: GET or POST
*/
method?: string;
/**
* Timeout Seconds
*
* HTTP request timeout in seconds
*/
timeout_seconds?: number;
/**
* Headers
*
* Custom HTTP headers
*/
headers?: {
[key: string]: string;
};
/**
* Params
*
* Custom HTTP query parameters
*/
params?: {
[key: string]: string;
};
};
/**
* WebhookListResponse
*
* Response model for listing webhooks.
*/
export type WebhookListResponse = {
/**
* Items
*/
items: Array<WebhookResponse>;
};
/**
* WebhookResponse
*
* Response model for a webhook.
*/
export type WebhookResponse = {
/**
* Id
*/
id: string;
/**
* Bank Id
*/
bank_id: string | null;
/**
* Url
*/
url: string;
/**
* Secret
*
* Signing secret (redacted in responses)
*/
secret?: string | null;
/**
* Event Types
*/
event_types: Array<string>;
/**
* Enabled
*/
enabled: boolean;
http_config?: WebhookHttpConfig;
/**
* Created At
*/
created_at?: string | null;
/**
* Updated At
*/
updated_at?: string | null;
};
export type HealthEndpointHealthGetData = {
body?: never;
path?: never;
@ -3899,6 +4138,217 @@ export type TriggerConsolidationResponses = {
export type TriggerConsolidationResponse =
TriggerConsolidationResponses[keyof TriggerConsolidationResponses];
export type ListWebhooksData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks";
};
export type ListWebhooksErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListWebhooksError = ListWebhooksErrors[keyof ListWebhooksErrors];
export type ListWebhooksResponses = {
/**
* Successful Response
*/
200: WebhookListResponse;
};
export type ListWebhooksResponse =
ListWebhooksResponses[keyof ListWebhooksResponses];
export type CreateWebhookData = {
body: CreateWebhookRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks";
};
export type CreateWebhookErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateWebhookError = CreateWebhookErrors[keyof CreateWebhookErrors];
export type CreateWebhookResponses = {
/**
* Successful Response
*/
201: WebhookResponse;
};
export type CreateWebhookResponse =
CreateWebhookResponses[keyof CreateWebhookResponses];
export type DeleteWebhookData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Webhook Id
*/
webhook_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}";
};
export type DeleteWebhookErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeleteWebhookError = DeleteWebhookErrors[keyof DeleteWebhookErrors];
export type DeleteWebhookResponses = {
/**
* Successful Response
*/
200: DeleteResponse;
};
export type DeleteWebhookResponse =
DeleteWebhookResponses[keyof DeleteWebhookResponses];
export type UpdateWebhookData = {
body: UpdateWebhookRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Webhook Id
*/
webhook_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}";
};
export type UpdateWebhookErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UpdateWebhookError = UpdateWebhookErrors[keyof UpdateWebhookErrors];
export type UpdateWebhookResponses = {
/**
* Successful Response
*/
200: WebhookResponse;
};
export type UpdateWebhookResponse =
UpdateWebhookResponses[keyof UpdateWebhookResponses];
export type ListWebhookDeliveriesData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path: {
/**
* Bank Id
*/
bank_id: string;
/**
* Webhook Id
*/
webhook_id: string;
};
query?: {
/**
* Limit
*
* Maximum number of deliveries to return
*/
limit?: number;
/**
* Cursor
*
* Pagination cursor (created_at of last item)
*/
cursor?: string | null;
};
url: "/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries";
};
export type ListWebhookDeliveriesErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListWebhookDeliveriesError =
ListWebhookDeliveriesErrors[keyof ListWebhookDeliveriesErrors];
export type ListWebhookDeliveriesResponses = {
/**
* Successful Response
*/
200: WebhookDeliveryListResponse;
};
export type ListWebhookDeliveriesResponse =
ListWebhookDeliveriesResponses[keyof ListWebhookDeliveriesResponses];
export type ClearBankMemoriesData = {
body?: never;
headers?: {

View file

@ -0,0 +1,23 @@
import { NextResponse } from "next/server";
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function GET(
request: Request,
{ params }: { params: Promise<{ bankId: string; webhookId: string }> }
) {
const { bankId, webhookId } = await params;
const { searchParams } = new URL(request.url);
const limit = searchParams.get("limit") || "50";
const cursor = searchParams.get("cursor");
const qs = new URLSearchParams({ limit });
if (cursor) qs.set("cursor", cursor);
const res = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks/${webhookId}/deliveries?${qs}`,
{
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
}
);
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}

View file

@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ bankId: string; webhookId: string }> }
) {
const { bankId, webhookId } = await params;
const body = await request.json();
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks/${webhookId}`, {
method: "PATCH",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string; webhookId: string }> }
) {
const { bankId, webhookId } = await params;
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks/${webhookId}`, {
method: "DELETE",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}

View file

@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
const { bankId } = await params;
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks`, {
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data);
}
export async function POST(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
const { bankId } = await params;
const body = await request.json();
const res = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/webhooks`, {
method: "POST",
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) return NextResponse.json({ error: data.detail || "Failed" }, { status: res.status });
return NextResponse.json(data, { status: 201 });
}

View file

@ -15,6 +15,7 @@ import { BankConfigView } from "@/components/bank-config-view";
import { BankStatsView } from "@/components/bank-stats-view";
import { BankOperationsView } from "@/components/bank-operations-view";
import { MentalModelsView } from "@/components/mental-models-view";
import { WebhooksView } from "@/components/webhooks-view";
import { useFeatures } from "@/lib/features-context";
import { useBank } from "@/lib/bank-context";
import { client } from "@/lib/api";
@ -40,7 +41,7 @@ import { Brain, Trash2, Loader2, MoreVertical, Pencil, RotateCcw } from "lucide-
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
type DataSubTab = "world" | "experience" | "observations" | "mental-models";
type BankConfigTab = "general" | "configuration";
type BankConfigTab = "general" | "configuration" | "webhooks";
export default function BankPage() {
const params = useParams();
@ -250,6 +251,19 @@ export default function BankPage() {
)}
</button>
)}
<button
onClick={() => handleBankConfigTabChange("webhooks")}
className={`px-6 py-3 font-semibold text-sm transition-all relative ${
bankConfigTab === "webhooks"
? "text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
Webhooks
{bankConfigTab === "webhooks" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
</div>
</div>
@ -272,6 +286,15 @@ export default function BankPage() {
<BankConfigView />
</div>
)}
{bankConfigTab === "webhooks" && (
<div>
<p className="text-sm text-muted-foreground mb-4">
Manage webhook endpoints to receive event notifications from this memory
bank.
</p>
<WebhooksView />
</div>
)}
</div>
</div>
)}

View file

@ -11,7 +11,6 @@ import {
Users,
ChevronLeft,
ChevronRight,
Box,
Settings,
} from "lucide-react";
import { cn } from "@/lib/utils";

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,40 @@
import { toast } from "sonner";
export interface WebhookHttpConfig {
method: string;
timeout_seconds: number;
headers: Record<string, string>;
params: Record<string, string>;
}
export interface Webhook {
id: string;
bank_id: string | null;
url: string;
event_types: string[];
enabled: boolean;
http_config: WebhookHttpConfig;
created_at: string | null;
updated_at: string | null;
}
export interface WebhookDelivery {
id: string;
webhook_id: string | null;
url: string;
event_type: string;
status: string;
attempts: number;
next_retry_at: string | null;
last_error: string | null;
last_response_status: number | null;
last_response_body: string | null;
last_attempt_at: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface MentalModel {
id: string;
bank_id: string;
@ -858,6 +892,79 @@ export class ControlPlaneClient {
method: "DELETE",
});
}
/**
* List webhooks for a bank
*/
async listWebhooks(bankId: string): Promise<{ items: Webhook[] }> {
return this.fetchApi<{ items: Webhook[] }>(`/api/banks/${bankId}/webhooks`);
}
/**
* Create a webhook
*/
async createWebhook(
bankId: string,
params: {
url: string;
secret?: string;
event_types?: string[];
enabled?: boolean;
http_config?: WebhookHttpConfig;
}
): Promise<Webhook> {
return this.fetchApi<Webhook>(`/api/banks/${bankId}/webhooks`, {
method: "POST",
body: JSON.stringify(params),
});
}
/**
* Update a webhook (PATCH only provided fields are changed)
*/
async updateWebhook(
bankId: string,
webhookId: string,
params: {
url?: string;
secret?: string | null;
event_types?: string[];
enabled?: boolean;
http_config?: WebhookHttpConfig;
}
): Promise<Webhook> {
return this.fetchApi<Webhook>(`/api/banks/${bankId}/webhooks/${webhookId}`, {
method: "PATCH",
body: JSON.stringify(params),
});
}
/**
* Delete a webhook
*/
async deleteWebhook(bankId: string, webhookId: string): Promise<{ success: boolean }> {
return this.fetchApi<{ success: boolean }>(`/api/banks/${bankId}/webhooks/${webhookId}`, {
method: "DELETE",
});
}
/**
* List webhook deliveries
*/
async listWebhookDeliveries(
bankId: string,
webhookId: string,
limit?: number,
cursor?: string
): Promise<{ items: WebhookDelivery[]; next_cursor: string | null }> {
const params = new URLSearchParams();
if (limit) params.append("limit", limit.toString());
if (cursor) params.append("cursor", cursor);
const query = params.toString();
return this.fetchApi<{ items: WebhookDelivery[]; next_cursor: string | null }>(
`/api/banks/${bankId}/webhooks/${webhookId}/deliveries${query ? `?${query}` : ""}`
);
}
}
// Export singleton instance

View file

@ -654,7 +654,6 @@ async def cmd_generate(bank_id: str, scale: str, workers: int = 16) -> None:
poll_interval_ms=200,
max_slots=workers,
consolidation_max_slots=0,
max_retries=20,
)
poller_task = asyncio.create_task(poller.run())
console.print(" Worker : started\n")

View file

@ -0,0 +1,96 @@
---
sidebar_position: 10
---
# Webhooks
Hindsight can notify your application in real-time when memory events occur by sending HTTP POST requests to a URL you configure.
## Delivery and Retries
Webhooks are registered per memory bank and fire automatically when matching events occur. Each delivery attempt is tracked, and failed deliveries are retried with exponential backoff:
| Attempt | Delay after failure |
|---------|---------------------|
| 1 | 5 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6 | Permanent failure |
A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within the configured timeout (default 30 seconds). After 6 failed attempts, the delivery is marked as permanently failed and no further retries are made.
:::info At-least-once delivery
Webhook delivery tasks are queued in the same database transaction as the primary operation (e.g. the retain or consolidation write). This means if the server crashes after committing but before sending, the delivery task survives and will be retried. As a result, **your endpoint may receive the same event more than once** — use the `operation_id` field to deduplicate if needed.
:::
## Event Types
### `consolidation.completed`
Fired after Hindsight finishes consolidating new memories into observations for a bank.
**Payload:**
```json
{
"event": "consolidation.completed",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "completed",
"timestamp": "2026-03-04T12:00:00Z",
"data": {
"observations_created": 3,
"observations_updated": 1,
"observations_deleted": null,
"error_message": null
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `observations_created` | `integer \| null` | Number of new observations created |
| `observations_updated` | `integer \| null` | Number of existing observations updated |
| `observations_deleted` | `integer \| null` | Number of observations deleted |
| `error_message` | `string \| null` | Set when `status` is `"failed"` |
**`status` values:** `"completed"` or `"failed"`
---
### `retain.completed`
Fired once per document after a retain operation completes (both synchronous and asynchronous). When retaining a batch of N documents, N separate events are fired.
**Payload:**
```json
{
"event": "retain.completed",
"bank_id": "my-bank",
"operation_id": "a1b2c3d4e5f6",
"status": "completed",
"timestamp": "2026-03-04T12:00:01Z",
"data": {
"document_id": "doc-abc123",
"tags": ["meeting", "q1-2026"]
}
}
```
**`data` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `document_id` | `string \| null` | The document ID if one was provided in the retain request |
| `tags` | `string[] \| null` | Document-level tags applied during retain |
**Notes:**
- For async retain (`async: true`), `operation_id` matches the `operation_id` returned by the retain API.
- For sync retain, `operation_id` is a generated identifier for tracing purposes.
- One event is fired per content item in the retain request.

View file

@ -99,6 +99,11 @@ const sidebars: SidebarsConfig = {
id: 'developer/api/operations',
label: 'Operations',
},
{
type: 'doc',
id: 'developer/api/webhooks',
label: 'Webhooks',
},
],
},
{

View file

@ -3141,6 +3141,375 @@
}
}
},
"/v1/default/banks/{bank_id}/webhooks": {
"post": {
"tags": [
"Webhooks"
],
"summary": "Register webhook",
"description": "Register a webhook endpoint to receive event notifications for this bank.",
"operationId": "create_webhook",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWebhookRequest"
}
}
}
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"get": {
"tags": [
"Webhooks"
],
"summary": "List webhooks",
"description": "List all webhooks registered for a bank.",
"operationId": "list_webhooks",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookListResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}": {
"delete": {
"tags": [
"Webhooks"
],
"summary": "Delete webhook",
"description": "Remove a registered webhook.",
"operationId": "delete_webhook",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Webhook Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"patch": {
"tags": [
"Webhooks"
],
"summary": "Update webhook",
"description": "Update one or more fields of a registered webhook. Only provided fields are changed.",
"operationId": "update_webhook",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Webhook Id"
}
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateWebhookRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/default/banks/{bank_id}/webhooks/{webhook_id}/deliveries": {
"get": {
"tags": [
"Webhooks"
],
"summary": "List webhook deliveries",
"description": "Inspect delivery history for a webhook (useful for debugging).",
"operationId": "list_webhook_deliveries",
"parameters": [
{
"name": "bank_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bank Id"
}
},
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Webhook Id"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 200,
"description": "Maximum number of deliveries to return",
"default": 50,
"title": "Limit"
},
"description": "Maximum number of deliveries to return"
},
{
"name": "cursor",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Pagination cursor (created_at of last item)",
"title": "Cursor"
},
"description": "Pagination cursor (created_at of last item)"
},
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookDeliveryListResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/default/banks/{bank_id}/memories": {
"post": {
"tags": [
@ -4348,6 +4717,54 @@
"title": "CreateMentalModelResponse",
"description": "Response model for mental model creation."
},
"CreateWebhookRequest": {
"properties": {
"url": {
"type": "string",
"title": "Url",
"description": "HTTP(S) endpoint URL to deliver events to"
},
"secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secret",
"description": "HMAC-SHA256 signing secret (optional)"
},
"event_types": {
"items": {
"type": "string"
},
"type": "array",
"title": "Event Types",
"description": "List of event types to deliver. Currently supported: 'consolidation.completed'",
"default": [
"consolidation.completed"
]
},
"enabled": {
"type": "boolean",
"title": "Enabled",
"description": "Whether this webhook is active",
"default": true
},
"http_config": {
"$ref": "#/components/schemas/WebhookHttpConfig",
"description": "HTTP delivery configuration (method, timeout, headers, params)"
}
},
"type": "object",
"required": [
"url"
],
"title": "CreateWebhookRequest",
"description": "Request model for registering a webhook."
},
"DeleteDocumentResponse": {
"properties": {
"success": {
@ -7031,6 +7448,75 @@
}
}
},
"UpdateWebhookRequest": {
"properties": {
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Url",
"description": "HTTP(S) endpoint URL"
},
"secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secret",
"description": "HMAC-SHA256 signing secret. Omit to keep existing; send null to clear."
},
"event_types": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Event Types",
"description": "List of event types"
},
"enabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Enabled",
"description": "Whether this webhook is active"
},
"http_config": {
"anyOf": [
{
"$ref": "#/components/schemas/WebhookHttpConfig"
},
{
"type": "null"
}
],
"description": "HTTP delivery configuration"
}
},
"type": "object",
"title": "UpdateWebhookRequest",
"description": "Request model for updating a webhook. Only provided fields are updated."
},
"ValidationError": {
"properties": {
"loc": {
@ -7093,6 +7579,290 @@
"worker": true
}
}
},
"WebhookDeliveryListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/WebhookDeliveryResponse"
},
"type": "array",
"title": "Items"
},
"next_cursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Next Cursor"
}
},
"type": "object",
"required": [
"items"
],
"title": "WebhookDeliveryListResponse",
"description": "Response model for listing webhook deliveries."
},
"WebhookDeliveryResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"webhook_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Webhook Id"
},
"url": {
"type": "string",
"title": "Url"
},
"event_type": {
"type": "string",
"title": "Event Type"
},
"status": {
"type": "string",
"title": "Status"
},
"attempts": {
"type": "integer",
"title": "Attempts"
},
"next_retry_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Next Retry At"
},
"last_error": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Error"
},
"last_response_status": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Last Response Status"
},
"last_response_body": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Response Body"
},
"last_attempt_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Attempt At"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created At"
},
"updated_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Updated At"
}
},
"type": "object",
"required": [
"id",
"webhook_id",
"url",
"event_type",
"status",
"attempts"
],
"title": "WebhookDeliveryResponse",
"description": "Response model for a webhook delivery record."
},
"WebhookHttpConfig": {
"properties": {
"method": {
"type": "string",
"title": "Method",
"description": "HTTP method: GET or POST",
"default": "POST"
},
"timeout_seconds": {
"type": "integer",
"title": "Timeout Seconds",
"description": "HTTP request timeout in seconds",
"default": 30
},
"headers": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"title": "Headers",
"description": "Custom HTTP headers"
},
"params": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"title": "Params",
"description": "Custom HTTP query parameters"
}
},
"type": "object",
"title": "WebhookHttpConfig",
"description": "HTTP delivery configuration for a webhook."
},
"WebhookListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/WebhookResponse"
},
"type": "array",
"title": "Items"
}
},
"type": "object",
"required": [
"items"
],
"title": "WebhookListResponse",
"description": "Response model for listing webhooks."
},
"WebhookResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"bank_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Bank Id"
},
"url": {
"type": "string",
"title": "Url"
},
"secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Secret",
"description": "Signing secret (redacted in responses)"
},
"event_types": {
"items": {
"type": "string"
},
"type": "array",
"title": "Event Types"
},
"enabled": {
"type": "boolean",
"title": "Enabled"
},
"http_config": {
"$ref": "#/components/schemas/WebhookHttpConfig"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created At"
},
"updated_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Updated At"
}
},
"type": "object",
"required": [
"id",
"bank_id",
"url",
"event_types",
"enabled"
],
"title": "WebhookResponse",
"description": "Response model for a webhook."
}
}
}