Sending verifications to claims platforms
Webhooks are great when you control the receiver. They're awkward when the receiver is Guidewire ClaimCenter, Duck Creek, or Mitchell — those systems have specific shapes, auth flows, and idempotency rules. VerifyAI ships an integration framework for this: define an integration once, the dispatcher fires on every verification, and adapter code maps the verification into the destination's domain model.
By the end you'll know:
- How the dispatcher decides what to send.
- The adapter contract you implement to add a new destination.
- The current state of the Guidewire ClaimCenter adapter (preview).
- What's on deck for v2 (Duck Creek, Mitchell).
1. Architecture
Two tables make up the framework (migration
20260532_verify_ai_integrations.sql):
verify_ai_integrations— one row per configured integration. Columns includeid,customer_id,type(e.g.guidewire),name,config(JSONB tenant settings),credentials_ref(Vault path),status(inactive|active|error),last_sync_at,last_error. Disabling =status = 'inactive'; there is no separate soft-delete column.verify_ai_integration_deliveries— every dispatch attempt. Columns:id,integration_id,verification_id,attempt_count,status(pending|sending|delivered|retrying|failed),request_body,response_status,response_body,next_attempt_at,error_message,delivered_at,failed_at,created_at.
The dispatcher (dispatchVerificationEvent in
lib/verify-ai/integrations/dispatcher.ts) runs after every
verification commit (fired from lib/verify-ai/service.ts at the
CLUSTER-3-DISPATCH marker). For each integration on the customer
with status = 'active', it:
- Records a delivery row with
status = 'sending'. - Looks up the adapter in the registry by
type. If no adapter is registered, the delivery row is immediately markedfailedwitherror_message = 'No adapter registered for type "..."'. - Calls
adapter.pushVerification(integration, event)whereeventis anOutboundEventcarrying the verification id, customer, site, policy, compliance verdict, confidence, violation reasons, feedback, image URL, metadata, and timestamp. - On
{ ok: true }, marks the deliverydelivered. On failure, updates the row withretrying(orfailedafter the final attempt) and schedules the next attempt atnow + delay. Retry delays mirror webhook delivery: 1m, 5m, 30m (4 attempts total). A background worker re-attempts retrying rows.
2. The adapter contract
Adapters implement IntegrationAdapter from
lib/verify-ai/integrations/types.ts:
export interface IntegrationAdapter {
/** Unique type identifier — matches verify_ai_integrations.type. */
readonly type: string;
/** Human label shown in the dashboard. */
readonly label: string;
/** Push a verification event to the third-party system. */
pushVerification(
integration: IntegrationConfig,
event: OutboundEvent,
): Promise<DeliveryResult>;
/** Optional connectivity self-check for the dashboard "Test connection" button. */
testConnection?(integration: IntegrationConfig): Promise<DeliveryResult>;
}
export interface DeliveryResult {
ok: boolean;
status?: number;
body?: unknown;
error?: string;
}The dispatcher calls pushVerification for every active
integration on every verification — there is no shouldDispatch
hook. If an adapter wants to skip events (e.g. only push
non-compliant results), it inspects event and returns
{ ok: true } without making a network call. Filtering knobs live
in integration.config.
Register the adapter at module load time. Side-effect imports live in
lib/verify-ai/integrations/index.ts, which keeps registry.ts free
of cycles:
// lib/verify-ai/integrations/index.ts
import './guidewire/adapter';
// import './duck-creek/adapter'; // v2
// import './mitchell/adapter'; // v2The adapter file itself calls registerAdapter(myAdapter) at the
bottom. The dispatcher reads the registry by type; there's no
other wiring.
3. Worked example — Guidewire ClaimCenter (stub)
A stub adapter ships in lib/verify-ai/integrations/guidewire/adapter.ts
under type = 'guidewire', label "Guidewire ClaimCenter". Today both
pushVerification and testConnection short-circuit:
async pushVerification() {
throw new Error(
'Not implemented — requires Guidewire tenant URL + OAuth credentials ' +
'(Insurtech Vanguards enrollment).'
);
}Stub, not preview. Calling the adapter today throws; deliveries end up in the
failedstate. The full implementation is gated on Guidewire Insurtech Vanguards enrollment (~12-week quarterly cohort) for OAuth client credentials.
Once implemented, the planned flow is: OAuth2 client_credentials →
cached bearer → multipart POST to
${tenantUrl}/cc/rest/claim/v1/claims/{claimId}/documents with the
verification image + JSON envelope → surface Guidewire's documentId
back into response_body.
Expected config shape on verify_ai_integrations.config:
{
"tenantUrl": "https://tenant.cc.guidewire.com",
"environment": "production",
"fieldMappings": { "vai_violation_reason": "cc_LossDescription" },
"defaultClaimDisposition": "openInProgress"
}credentials_ref points at a Supabase Vault secret containing
{ clientId, clientSecret, tokenUrl } — never store credentials in
config.
4. Writing a custom adapter
When the built-in adapters don't fit, write your own. The skeleton:
import { registerAdapter } from "@/lib/verify-ai/integrations/registry";
import type {
DeliveryResult,
IntegrationAdapter,
IntegrationConfig,
OutboundEvent,
} from "@/lib/verify-ai/integrations/types";
export const warehouseAdapter: IntegrationAdapter = {
type: "internal_warehouse_api",
label: "Internal Warehouse",
async pushVerification(
integration: IntegrationConfig,
event: OutboundEvent,
): Promise<DeliveryResult> {
// Opt-out example: skip events without a site assignment.
if (event.siteId === null) {
return { ok: true, body: { skipped: "no_site" } };
}
const endpoint = String(integration.config.endpoint);
const token = String(integration.config.token);
const res = await fetch(`${endpoint}/events`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
external_id: event.verificationId,
compliant: event.isCompliant,
site_id: event.siteId,
policy_id: event.policyId,
confidence: event.confidence,
created_at: event.createdAt,
}),
});
if (res.ok) {
return { ok: true, status: res.status, body: await res.json() };
}
return {
ok: false,
status: res.status,
error: `HTTP ${res.status}`,
};
},
};
registerAdapter(warehouseAdapter);Things to keep in mind:
- Idempotency — your destination should treat retries as a no-op.
The dispatcher retries any
{ ok: false }result up to 3 times (1m, 5m, 30m backoff) before marking the deliveryfailed. - Secrets — never store them in
config. Usecredentials_refpointing at a Supabase Vault secret and resolve it inside the adapter.configends up visible in the dashboard. - Latency — adapters run on the verification commit path inside
Promise.all. Aim for sub-second; offload heavy work to your own queue.
5. Roadmap
- Duck Creek (v2 / future) — same shape as ClaimCenter, different auth model. No adapter code yet.
- Mitchell (v2 / future) — claims-system integration for auto / fleet collision workflows. No adapter code yet.
- Bulk replay — re-dispatch deliveries for a date range, e.g. after a destination outage. UI in Dashboard → Operations → Integrations is on the v2 list.
Today the only adapter registered is the Guidewire stub. Anything else (Duck Creek, Mitchell, McLeod, SupplyPike, internal APIs) is custom adapter territory — write it, register it, ship it.
What's next
- Audit log and SOC 2 readiness — integration config changes are audited; delivery attempts live in their own table.
- Triaging low-confidence verifications — adapters can choose to only dispatch resolved exceptions.
- Exporting verification data for BI tools — for warehouses / analytics targets, the BI export is usually a better fit than an adapter.