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:

  1. How the dispatcher decides what to send.
  2. The adapter contract you implement to add a new destination.
  3. The current state of the Guidewire ClaimCenter adapter (preview).
  4. 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 include id, 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:

  1. Records a delivery row with status = 'sending'.
  2. Looks up the adapter in the registry by type. If no adapter is registered, the delivery row is immediately marked failed with error_message = 'No adapter registered for type "..."'.
  3. Calls adapter.pushVerification(integration, event) where event is an OutboundEvent carrying the verification id, customer, site, policy, compliance verdict, confidence, violation reasons, feedback, image URL, metadata, and timestamp.
  4. On { ok: true }, marks the delivery delivered. On failure, updates the row with retrying (or failed after the final attempt) and schedules the next attempt at now + 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:

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:

ts
// lib/verify-ai/integrations/index.ts
import './guidewire/adapter';
// import './duck-creek/adapter';   // v2
// import './mitchell/adapter';     // v2

The 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:

ts
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 failed state. 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:

json
{
  "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:

ts
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 delivery failed.
  • Secrets — never store them in config. Use credentials_ref pointing at a Supabase Vault secret and resolve it inside the adapter. config ends 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

Get in Touch

Questions about pricing, integrations, or custom deployments? We'd love to hear from you.