| 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/switchyard-web/public/ |
Upload File : |
openapi: 3.1.0
info:
title: Switchyard API
version: 0.1.0
description: |
Multi-tenant ClickUp lead-intake API.
Captures inquiries from many sources (brand contact forms, Upwork, Fiverr,
direct email, generic webhooks), enriches them with service taxonomy +
portfolio examples + a 0-100 lead score, and lands them as ClickUp tasks
in a tenant's sales-pipeline list. ClickUp status changes mirror back via
webhook so the local pipeline stays in lockstep.
## Authentication
The bearer-API surface (`/v1/*` except `/v1/healthz`) takes an
`Authorization: Bearer swy_…` token issued in the Filament admin. Tokens
are workspace-scoped — every read/write happens against the workspace
that owns the key.
Inbound webhooks (`/webhooks/inbound/{workspace_id}/{source_slug}`) use a
per-source HMAC signature in `X-Switchyard-Signature: t=<unix>,v1=<hex>`
where `v1 = hmac_sha256(secret, "{t}.{body}")`. ClickUp webhooks
(`/webhooks/clickup/{workspace_id}`) use ClickUp's own `X-Signature` HMAC
against the per-workspace `webhook_secret`.
## Errors
All error responses use RFC 7807 problem details (`application/problem+json`):
`{ "type": "about:blank", "title": "...", "status": 4xx, "detail": "..." }`.
contact:
name: Switchyard Support
url: https://switchyard.philiprehberger.com
license:
name: MIT
servers:
- url: https://api.switchyard.philiprehberger.com
description: Production
security:
- bearerAuth: []
tags:
- name: Health
description: Service liveness
- name: Leads
description: Inbox + manual routing
- name: Sources
description: Configured inbound source channels
- name: ClientProjects
description: Phase 2 — client status board (projects, milestones, share tokens)
- name: PublicStatus
description: Phase 2 — public read-only status board (token-authenticated)
- name: Templates
description: Phase 3 — project templates + scaffold runs
- name: Webhooks
description: Signed inbound + ClickUp callback
paths:
/v1/healthz:
get:
tags: [Health]
operationId: healthz
summary: Service healthcheck
security: []
responses:
'200':
description: Healthy
content:
application/json:
schema: { $ref: '#/components/schemas/Health' }
'503':
description: Unhealthy
content:
application/json:
schema: { $ref: '#/components/schemas/Health' }
/v1/leads:
get:
tags: [Leads]
operationId: listLeads
summary: List leads
description: Paginated list of leads in the caller's workspace. Filterable by status, source slug, and minimum score.
parameters:
- { in: query, name: status, schema: { type: string, enum: [received, enriched, routed, won, lost, dismissed] } }
- { in: query, name: source, schema: { type: string }, description: Source slug (e.g. scopeforged-form) }
- { in: query, name: min_score, schema: { type: integer, minimum: 0, maximum: 100 } }
- { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/Lead' } }
meta: { $ref: '#/components/schemas/PaginationMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
post:
tags: [Leads]
operationId: ingestLead
summary: Ingest a lead
description: Submit a raw payload from any configured source. Returns 202 + queues an IngestLeadJob.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [source_slug, payload]
properties:
source_slug: { type: string, description: Slug of a configured LeadSource }
payload: { type: object, additionalProperties: true }
external_ref: { type: string, nullable: true, maxLength: 128 }
responses:
'202':
description: Accepted; job queued
content:
application/json:
schema: { $ref: '#/components/schemas/Accepted' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/Conflict' }
/v1/leads/{id}:
get:
tags: [Leads]
operationId: getLead
summary: Get a lead
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/LeadWithRaw' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/leads/{id}/route:
post:
tags: [Leads]
operationId: routeLead
summary: Manually route a lead to ClickUp
description: Idempotent on `clickup_task_id` — if the lead is already routed, returns the existing task reference without re-dispatching.
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'202':
description: Queued
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/Lead' }
queued: { type: boolean }
'200':
description: Already routed
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/Lead' }
already_routed: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/leads/{id}/dismiss:
post:
tags: [Leads]
operationId: dismissLead
summary: Dismiss a lead
description: Marks the lead as dismissed; the row is preserved for audit.
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'200':
description: Dismissed
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/Lead' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/sources:
get:
tags: [Sources]
operationId: listSources
summary: List configured lead sources
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/Source' } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/client-projects:
get:
tags: [ClientProjects]
operationId: listClientProjects
summary: List client projects (Phase 2)
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/ClientProject' } }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [ClientProjects]
operationId: createClientProject
summary: Create a client project (Phase 2)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [clickup_folder_id, name]
properties:
clickup_folder_id: { type: string, maxLength: 32, example: '901234567' }
name: { type: string, maxLength: 255 }
client_name: { type: string, nullable: true, maxLength: 255 }
summary: { type: string, nullable: true, maxLength: 2000 }
lead_id: { type: string, nullable: true, maxLength: 26 }
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ClientProject' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
/v1/client-projects/{id}:
get:
tags: [ClientProjects]
operationId: getClientProject
summary: Get a client project with milestones (Phase 2)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ClientProjectWithMilestones' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [ClientProjects]
operationId: updateClientProject
summary: Update a client project (Phase 2)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string, maxLength: 255 }
client_name: { type: string, nullable: true, maxLength: 255 }
summary: { type: string, nullable: true, maxLength: 2000 }
is_archived: { type: boolean }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ClientProject' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/client-projects/{id}/sync:
post:
tags: [ClientProjects]
operationId: syncClientProject
summary: Pull milestones from ClickUp into the local mirror (Phase 2)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'200':
description: Synced
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ClientProjectWithMilestones' }
sync:
type: object
properties:
lists: { type: integer }
tasks: { type: integer }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/Conflict' }
'502':
description: ClickUp API rejected the sync
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
/v1/client-projects/{id}/tokens:
post:
tags: [ClientProjects]
operationId: issueShareToken
summary: Issue a public share link for a client project (Phase 2)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
requestBody:
content:
application/json:
schema:
type: object
properties:
label: { type: string, nullable: true, maxLength: 120 }
expires_at: { type: string, format: date-time, nullable: true }
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/IssuedShareToken' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/client-projects/{id}/tokens/{tokenId}:
delete:
tags: [ClientProjects]
operationId: revokeShareToken
summary: Revoke a public share link (Phase 2)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
- { in: path, name: tokenId, required: true, schema: { type: string } }
responses:
'200':
description: Revoked (idempotent)
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id: { type: string }
revoked_at: { type: string, format: date-time, nullable: true }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/templates:
get:
tags: [Templates]
operationId: listTemplates
summary: List project templates (Phase 3)
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/ProjectTemplate' } }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Templates]
operationId: createTemplate
summary: Create a project template (Phase 3)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProjectTemplateInput' }
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ProjectTemplateWithStructure' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/templates/{id}:
get:
tags: [Templates]
operationId: getTemplate
summary: Get a template with structure (Phase 3)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ProjectTemplateWithStructure' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [Templates]
operationId: updateTemplate
summary: Update a project template (Phase 3)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProjectTemplateInput' }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { $ref: '#/components/schemas/ProjectTemplateWithStructure' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [Templates]
operationId: deleteTemplate
summary: Delete a project template (Phase 3)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
responses:
'200':
description: Deleted
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id: { type: string }
deleted: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/leads/{id}/scaffold:
post:
tags: [Templates]
operationId: scaffoldFromLead
summary: Manually scaffold a project for a lead (Phase 3)
parameters:
- { in: path, name: id, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [template_id]
properties:
template_id: { type: string, maxLength: 26 }
responses:
'202':
description: Queued
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
scaffold_run_id: { type: string }
status: { type: string, enum: [queued, running, completed, failed] }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/scaffold-runs:
get:
tags: [Templates]
operationId: listScaffoldRuns
summary: List recent scaffold runs (Phase 3)
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/ScaffoldRun' } }
'401': { $ref: '#/components/responses/Unauthorized' }
/public/status/{token}:
get:
tags: [PublicStatus]
operationId: getPublicStatus
summary: Public read-only client status board (Phase 2)
description: |
Returns the client-facing view of a tracked project: name, client name,
summary, and the visible milestones. No bearer auth — the token itself
is the secret. Per-IP rate-limited at 50/hour. Revoked / expired
tokens return 410.
security: []
parameters:
- { in: path, name: token, required: true, schema: { type: string, example: 'sty_…' } }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/PublicStatusResponse' }
'404':
description: Token unknown or project archived
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
'410':
description: Token revoked or expired
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
'429':
description: Per-IP rate limit exhausted
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
/webhooks/inbound/{workspace_id}/{source_slug}:
post:
tags: [Webhooks]
operationId: inboundWebhook
summary: Signed inbound webhook
description: |
Accept a signed payload from a configured source. The signature is
verified using the source's `inbound_secret` (revealed once in the
Filament admin). Optional `Idempotency-Key` header dedupes retries
for 24 hours.
security: []
parameters:
- { in: path, name: workspace_id, required: true, schema: { type: string } }
- { in: path, name: source_slug, required: true, schema: { type: string } }
- { in: header, name: X-Switchyard-Signature, required: true, schema: { type: string, example: 't=1750000000,v1=abc123…' } }
- { in: header, name: Idempotency-Key, required: false, schema: { type: string, maxLength: 64 } }
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'202':
description: Accepted; ingest job queued
content:
application/json:
schema: { $ref: '#/components/schemas/Accepted' }
'200':
description: Idempotency replay; cached response returned
content:
application/json:
schema: { $ref: '#/components/schemas/Accepted' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'409': { $ref: '#/components/responses/Conflict' }
/webhooks/clickup/{workspace_id}:
post:
tags: [Webhooks]
operationId: clickupWebhook
summary: ClickUp event callback
description: |
Receives task/status events from ClickUp. The signature in
`X-Signature` is verified against the per-workspace `webhook_secret`
on `clickup_connections`. On success, dispatches MirrorClickUpStatusJob
to update the local lead mirror.
security: []
parameters:
- { in: path, name: workspace_id, required: true, schema: { type: string } }
- { in: header, name: X-Signature, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: Acknowledged
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: swy_<prefix>_<secret>
schemas:
Lead:
type: object
properties:
id: { type: string, example: '01HZZ…' }
status: { type: string, enum: [received, enriched, routed, won, lost, dismissed] }
source_id: { type: string }
external_ref: { type: string, nullable: true }
parsed_contact:
type: object
properties:
name: { type: string }
email: { type: string, format: email }
phone: { type: string }
company: { type: string }
linkedin: { type: string, format: uri }
additionalProperties: false
parsed_job:
type: object
properties:
title: { type: string }
body: { type: string }
budget_text: { type: string }
timeline_text: { type: string }
urls: { type: array, items: { type: string, format: uri } }
additionalProperties: false
service_tag: { type: string, nullable: true, example: web-app }
suggested_examples: { type: array, items: { type: string }, example: [highlands-ranch-champ, integridev] }
lead_score: { type: integer, minimum: 0, maximum: 100 }
clickup_task_id: { type: string, nullable: true }
routed_at: { type: string, format: date-time, nullable: true }
won_at: { type: string, format: date-time, nullable: true }
lost_at: { type: string, format: date-time, nullable: true }
dismissed_at: { type: string, format: date-time, nullable: true }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
LeadWithRaw:
allOf:
- $ref: '#/components/schemas/Lead'
- type: object
properties:
raw_payload:
type: object
additionalProperties: true
Source:
type: object
properties:
id: { type: string }
slug: { type: string, example: scopeforged-form }
name: { type: string }
kind: { type: string, enum: [webhook, email, manual] }
enabled: { type: boolean }
created_at: { type: string, format: date-time }
PaginationMeta:
type: object
properties:
total: { type: integer }
per_page: { type: integer }
current_page: { type: integer }
last_page: { type: integer }
Health:
type: object
properties:
healthy: { type: boolean }
version: { type: string }
checks:
type: object
properties:
db: { type: object, properties: { ok: { type: boolean }, error: { type: string } } }
redis: { type: object, properties: { ok: { type: boolean }, error: { type: string } } }
Accepted:
type: object
properties:
ok: { type: boolean }
accepted: { type: boolean }
ClientProject:
type: object
properties:
id: { type: string }
name: { type: string }
client_name: { type: string, nullable: true }
summary: { type: string, nullable: true }
clickup_folder_id: { type: string }
is_archived: { type: boolean }
last_synced_at: { type: string, format: date-time, nullable: true }
created_at: { type: string, format: date-time }
tokens:
type: array
nullable: true
items: { $ref: '#/components/schemas/ShareTokenSummary' }
ClientProjectWithMilestones:
allOf:
- $ref: '#/components/schemas/ClientProject'
- type: object
properties:
milestones:
type: array
items: { $ref: '#/components/schemas/Milestone' }
Milestone:
type: object
properties:
id: { type: string }
name: { type: string }
clickup_task_id: { type: string, nullable: true }
status_name: { type: string, nullable: true }
status_color: { type: string, nullable: true, example: '#10b981' }
status_type: { type: string, nullable: true, enum: [open, closed, custom] }
is_visible: { type: boolean }
is_complete: { type: boolean }
display_order: { type: integer }
due_at: { type: string, format: date-time, nullable: true }
completed_at: { type: string, format: date-time, nullable: true }
ShareTokenSummary:
type: object
properties:
id: { type: string }
label: { type: string, nullable: true }
expires_at: { type: string, format: date-time, nullable: true }
revoked_at: { type: string, format: date-time, nullable: true }
last_viewed_at: { type: string, format: date-time, nullable: true }
view_count: { type: integer }
share_url: { type: string, format: uri, nullable: true }
IssuedShareToken:
type: object
properties:
id: { type: string }
token: { type: string, example: 'sty_…' }
label: { type: string, nullable: true }
expires_at: { type: string, format: date-time, nullable: true }
share_url: { type: string, format: uri }
ProjectTemplate:
type: object
properties:
id: { type: string }
name: { type: string }
service_tag: { type: string, nullable: true }
clickup_space_id: { type: string }
folder_name_pattern: { type: string }
auto_scaffold: { type: boolean }
created_at: { type: string, format: date-time }
ProjectTemplateWithStructure:
allOf:
- $ref: '#/components/schemas/ProjectTemplate'
- type: object
properties:
structure:
type: object
properties:
lists:
type: array
items:
type: object
properties:
name: { type: string }
tasks:
type: array
items:
oneOf:
- { type: string }
- type: object
properties:
name: { type: string }
due_offset_days: { type: integer, nullable: true }
priority: { type: integer, nullable: true }
description: { type: string, nullable: true }
ProjectTemplateInput:
type: object
required: [name, clickup_space_id, structure]
properties:
name: { type: string, maxLength: 120 }
service_tag: { type: string, nullable: true, maxLength: 64 }
clickup_space_id: { type: string, maxLength: 32 }
folder_name_pattern: { type: string, nullable: true, maxLength: 255 }
auto_scaffold: { type: boolean }
structure:
type: object
required: [lists]
properties:
lists:
type: array
minItems: 1
items:
type: object
properties:
name: { type: string }
tasks: { type: array, items: { oneOf: [{ type: string }, { type: object }] } }
ScaffoldRun:
type: object
properties:
id: { type: string }
template_id: { type: string }
lead_id: { type: string, nullable: true }
client_project_id: { type: string, nullable: true }
trigger: { type: string, example: 'lead.status=won' }
status: { type: string, enum: [queued, running, completed, failed] }
clickup_folder_id: { type: string, nullable: true }
error_message: { type: string, nullable: true }
started_at: { type: string, format: date-time, nullable: true }
completed_at: { type: string, format: date-time, nullable: true }
created_at: { type: string, format: date-time }
PublicStatusResponse:
type: object
properties:
data:
type: object
properties:
project:
type: object
properties:
name: { type: string }
client_name: { type: string, nullable: true }
summary: { type: string, nullable: true }
last_synced_at: { type: string, format: date-time, nullable: true }
milestones:
type: array
items:
type: object
properties:
id: { type: string }
name: { type: string }
status_name: { type: string, nullable: true }
status_color: { type: string, nullable: true }
status_type: { type: string, nullable: true }
is_complete: { type: boolean }
due_at: { type: string, format: date-time, nullable: true }
completed_at: { type: string, format: date-time, nullable: true }
Problem:
type: object
description: RFC 7807 problem details
properties:
type: { type: string, format: uri, example: 'about:blank' }
title: { type: string }
status: { type: integer }
detail: { type: string }
responses:
BadRequest:
description: Validation or shape error
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
Unauthorized:
description: Missing / invalid bearer or signature
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
Forbidden:
description: Scope or IP allowlist denied
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
NotFound:
description: Resource not found
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
Conflict:
description: Resource state conflict
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }