| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/distill-web/public/ |
Upload File : |
openapi: 3.1.0
info:
title: Distill API
version: 0.1.0
summary: AI-assisted CRM data-cleanup. HubSpot duplicate detection via embeddings + clustering + reversible writes.
description: |
Distill connects to HubSpot, finds duplicate contacts via embeddings +
string-similarity, surfaces them in a human-review queue, and writes
reversible merges back through the HubSpot API. Production-shaped
portfolio demo.
Authentication is via `Authorization: Bearer dst_<prefix>_<secret>` API
keys. Keys carry one of three scopes: `read`, `admin_read`, `admin_write`.
Higher scopes include lower ones.
All errors follow RFC 7807 problem-details. Responses are JSON.
contact:
name: Philip Rehberger
url: https://distill.philiprehberger.com
license:
name: MIT
identifier: MIT
servers:
- url: https://api.distill.philiprehberger.com
description: Production
- url: http://localhost:8000
description: Local development
security:
- bearerAuth: []
paths:
/v1/healthz:
get:
summary: Liveness probe
operationId: healthCheck
security: []
responses:
'200':
description: Healthy
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
/v1/workspaces/current:
get:
summary: Current workspace context
operationId: workspaceCurrent
responses:
'200':
description: Workspace context
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Workspace'
/v1/sync/status:
get:
summary: HubSpot sync status
operationId: syncStatus
responses:
'200':
description: Sync status
content:
application/json:
schema:
$ref: '#/components/schemas/SyncStatus'
/v1/sync/start:
post:
summary: Trigger a full backfill from HubSpot
operationId: syncStart
responses:
'202':
description: Sync queued
content:
application/json:
schema:
$ref: '#/components/schemas/SyncRunRef'
/v1/clusters:
get:
summary: List pending review clusters
operationId: clustersList
parameters:
- in: query
name: status
schema:
type: string
enum: [pending, merged, rejected, skipped]
- in: query
name: min_similarity
schema:
type: number
format: float
responses:
'200':
description: Clusters
content:
application/json:
schema:
$ref: '#/components/schemas/ClusterList'
/v1/clusters/{id}:
get:
summary: Cluster detail with members
operationId: clusterShow
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'200':
description: Cluster detail
content:
application/json:
schema:
type: object
properties:
data:
$ref: '#/components/schemas/Cluster'
/v1/clusters/{id}/merge:
post:
summary: Confirm a merge
operationId: clusterMerge
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MergeRequest'
responses:
'202':
description: Merge queued
/v1/clusters/{id}/reject:
post:
summary: Reject a cluster (not-a-duplicate signal)
operationId: clusterReject
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RejectRequest'
responses:
'200':
description: Rejected
/v1/cluster/run:
post:
summary: Trigger a clustering pass
operationId: clusterRun
responses:
'202':
description: Queued
/v1/merges:
get:
summary: Merge history
operationId: mergesList
responses:
'200':
description: Merge history
/v1/merges/{id}/undo:
post:
summary: Reverse a merge
operationId: mergeUndo
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'202':
description: Undo queued
/v1/audit-log:
get:
summary: Audit trail
operationId: auditLog
responses:
'200':
description: Audit events
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: dst_<prefix>_<secret>
schemas:
Health:
type: object
properties:
healthy:
type: boolean
version:
type: string
checks:
type: object
Workspace:
type: object
properties:
id:
type: string
name:
type: string
slug:
type: string
writes_enabled:
type: boolean
blocking_strategy:
type: string
enum: [domain, phone, name_company, any]
similarity_threshold:
type: number
format: float
auto_merge_enabled:
type: boolean
embedding_model:
type: string
SyncStatus:
type: object
properties:
data:
type: object
properties:
connected:
type: boolean
portal_id:
type: string
nullable: true
last_full_sync_at:
type: string
format: date-time
nullable: true
last_incremental_sync_at:
type: string
format: date-time
nullable: true
webhook_subscribed:
type: boolean
recent_runs:
type: array
items:
$ref: '#/components/schemas/SyncRun'
SyncRun:
type: object
properties:
id:
type: string
kind:
type: string
enum: [full_backfill, incremental, webhook_replay]
status:
type: string
enum: [queued, running, finished, failed]
contacts_seen:
type: integer
contacts_upserted:
type: integer
started_at:
type: string
format: date-time
nullable: true
finished_at:
type: string
format: date-time
nullable: true
SyncRunRef:
type: object
properties:
data:
type: object
properties:
sync_run_id:
type: string
status:
type: string
Cluster:
type: object
properties:
id:
type: string
similarity:
type: number
format: float
member_count:
type: integer
status:
type: string
enum: [pending, merged, rejected, skipped]
blocking_strategy:
type: string
block_key:
type: string
nullable: true
winner_hubspot_id:
type: string
nullable: true
members:
type: array
items:
type: object
properties:
hubspot_id:
type: string
email:
type: string
nullable: true
first_name:
type: string
nullable: true
last_name:
type: string
nullable: true
company_name:
type: string
nullable: true
phone_e164:
type: string
nullable: true
updated_at_source:
type: string
format: date-time
nullable: true
owned_deals_count:
type: integer
owned_emails_count:
type: integer
owned_notes_count:
type: integer
ClusterList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Cluster'
MergeRequest:
type: object
required: [winner_hubspot_id]
properties:
winner_hubspot_id:
type: string
strategy:
type: string
enum: [manual, newest_wins]
RejectRequest:
type: object
required: [reason]
properties:
reason:
type: string
enum: [different_people, intentional_duplicate, needs_more_info, other]
comment:
type: string
nullable: true
Problem:
type: object
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
errors:
type: object
additionalProperties: true