openapi.yaml
Rama main · réplica de lectura del repositorio original.
openapi: 3.1.0
info:
title: Homix Marketing Listing API
version: 2.0.0
description: Authenticated listing-marketing operations plus signed public delivery endpoints.
servers:
- url: /api/v2
security:
- cookieAuth: []
tags:
- { name: Auth }
- { name: Users }
- { name: Agents }
- { name: Contacts }
- { name: Imports }
- { name: Reference data }
- { name: Suppressions }
- { name: Listings }
- { name: Audiences }
- { name: Senders }
- { name: Campaigns }
- { name: Analytics }
- { name: Operations }
- { name: OneKey }
- { name: AI }
- { name: Public }
- { name: Portal }
paths:
/api/integrations/homix/v1/campaigns/{id}/stats:
servers: [{ url: / }]
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
tags: [Portal, Analytics]
operationId: getPortalCampaignStats
summary: Read the owned campaign report and current sending state
description: >-
Uses the signed Portal identity and campaign ownership scope. A null reporting value means
the new report has not been computed; legacy numeric zeros do not establish engagement
tracking availability. Historical coverage comes from each accepted message, never the
sender's current preferences. Counts are a persisted snapshot and sending is evaluated
when the response is read.
security: [{ portalBearerAuth: [] }]
responses:
"200":
description: Owned campaign reporting snapshot
content:
application/json:
schema:
type: object
required: [campaign, globalPause]
properties:
globalPause: { type: boolean }
campaign:
type: object
required: [id, stats, reporting]
additionalProperties: true
properties:
id: { type: string, format: uuid }
stats: { $ref: "#/components/schemas/CampaignLegacyStats" }
statsComputedAt: { type: [string, "null"], format: date-time }
reporting:
oneOf:
- $ref: "#/components/schemas/CampaignReporting"
- { type: "null" }
"401": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" }
/auth/me:
get:
{
tags: [Auth],
operationId: getCurrentUser,
summary: Return the authenticated application user,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"401": { $ref: "#/components/responses/Error" },
},
}
/auth/dev-login:
post:
{
tags: [Auth],
operationId: localDevelopmentLogin,
summary: Create a local-only development session,
security: [],
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
},
}
/auth/logout:
post:
{
tags: [Auth],
operationId: logout,
summary: Clear the local session or return the Easy Auth logout URL,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/users:
get:
{
tags: [Users],
operationId: listUsers,
summary: List provisioned users,
responses:
{
"200": { $ref: "#/components/responses/Collection" },
"403": { $ref: "#/components/responses/Error" },
},
}
post:
{
tags: [Users],
operationId: provisionUser,
summary: Pre-provision a user and application role,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"201": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
},
}
/users/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
patch:
{
tags: [Users],
operationId: updateUser,
summary: Change a user role or active state,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/agents:
get:
{
tags: [Agents],
operationId: listAgents,
summary: List broker identities,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
post:
{
tags: [Agents],
operationId: createAgent,
summary: Create a broker identity,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/agents/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Agents],
operationId: getAgent,
summary: Get a broker identity,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"404": { $ref: "#/components/responses/Error" },
},
}
patch:
{
tags: [Agents],
operationId: updateAgent,
summary: Update a broker identity,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
delete:
{
tags: [Agents],
operationId: deactivateAgent,
summary: Soft-deactivate a broker identity,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/contacts:
get:
{
tags: [Contacts],
operationId: listContacts,
summary: Search and page contacts with suppression state,
parameters:
[
{ $ref: "#/components/parameters/Page" },
{ $ref: "#/components/parameters/Limit" },
{ $ref: "#/components/parameters/Cursor" },
{ $ref: "#/components/parameters/Search" },
{ $ref: "#/components/parameters/ContactType" },
{ $ref: "#/components/parameters/ContactSourceType" },
{ $ref: "#/components/parameters/PermissionBasis" },
{ $ref: "#/components/parameters/ContactStatus" },
{ $ref: "#/components/parameters/MarketId" },
{ $ref: "#/components/parameters/PropertyInterestId" },
{ $ref: "#/components/parameters/TagId" },
{ $ref: "#/components/parameters/Suppressed" },
{ $ref: "#/components/parameters/ContactSort" },
{ $ref: "#/components/parameters/SortOrder" },
],
responses: { "200": { $ref: "#/components/responses/Paginated" } },
}
post:
{
tags: [Contacts],
operationId: createContact,
summary: Create a contact with relationship metadata,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"201": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/contacts/export:
get:
{
tags: [Contacts],
operationId: exportContacts,
summary: Export formula-safe CSV,
parameters:
[
{ $ref: "#/components/parameters/Search" },
{ $ref: "#/components/parameters/ContactType" },
{ $ref: "#/components/parameters/ContactSourceType" },
{ $ref: "#/components/parameters/PermissionBasis" },
{ $ref: "#/components/parameters/ContactStatus" },
{ $ref: "#/components/parameters/MarketId" },
{ $ref: "#/components/parameters/PropertyInterestId" },
{ $ref: "#/components/parameters/TagId" },
{ $ref: "#/components/parameters/Suppressed" },
{ $ref: "#/components/parameters/ContactSort" },
{ $ref: "#/components/parameters/SortOrder" },
],
responses: { "200": { $ref: "#/components/responses/Csv" } },
}
/contacts/bulk-update:
post:
{
tags: [Contacts],
operationId: bulkUpdateContacts,
summary: Update up to one thousand contacts,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/contacts/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Contacts],
operationId: getContact,
summary: Get contact history and suppression state,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"404": { $ref: "#/components/responses/Error" },
},
}
patch:
{
tags: [Contacts],
operationId: updateContact,
summary: Update contact profile and relationships,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
delete:
{
tags: [Contacts],
operationId: archiveContact,
summary: Archive a contact without deleting audit history,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/contacts/{id}/restore:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Contacts],
operationId: restoreContact,
summary: Restore an archived contact without clearing suppression,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/contact-imports/upload:
post:
tags: [Imports]
operationId: uploadContactImport
summary: Upload a private CSV and source metadata
requestBody:
required: true
content:
multipart/form-data:
schema:
{
type: object,
required: [file, sourceType, permissionBasis],
properties:
{
file: { type: string, format: binary },
sourceType: { type: string },
permissionBasis: { type: string },
sourceDetail: { type: string },
},
}
responses:
{
"201": { $ref: "#/components/responses/Success" },
"413": { $ref: "#/components/responses/Error" },
"415": { $ref: "#/components/responses/Error" },
}
/contact-imports/{id}/validate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Imports],
operationId: validateContactImport,
summary: Validate an explicit CSV column mapping and produce a masked summary,
requestBody:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
properties:
{
mapping:
{
type: object,
additionalProperties: { type: string },
description: Canonical field names mapped to uploaded CSV headers.,
},
},
},
},
},
},
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/contact-imports/{id}/apply:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Imports],
operationId: applyContactImport,
summary: Confirm reference creation and queue an idempotent validated import,
requestBody:
{
required: true,
content:
{
application/json:
{
schema:
{
type: object,
required: [confirmCreateUnknownReferences],
properties:
{ confirmCreateUnknownReferences: { type: boolean, default: false } },
},
},
},
},
responses:
{
"202": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/contact-imports/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Imports],
operationId: getContactImport,
summary: Return import status and aggregate counts,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/contact-imports/{id}/errors.csv:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Imports],
operationId: downloadContactImportErrors,
summary: Download a private formula-safe error report,
responses:
{
"200": { $ref: "#/components/responses/Csv" },
"404": { $ref: "#/components/responses/Error" },
},
}
/tags:
get:
{
tags: [Reference data],
operationId: listTags,
summary: List active tags,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
post:
{
tags: [Reference data],
operationId: createTag,
summary: Create a tag,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/tags/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
patch:
{
tags: [Reference data],
operationId: updateTag,
summary: Update a tag,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
delete:
{
tags: [Reference data],
operationId: deactivateTag,
summary: Deactivate a tag,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/markets:
get:
{
tags: [Reference data],
operationId: listMarkets,
summary: List active market hierarchy,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
post:
{
tags: [Reference data],
operationId: createMarket,
summary: Create a market,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/markets/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
patch:
{
tags: [Reference data],
operationId: updateMarket,
summary: Update a market,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
delete:
{
tags: [Reference data],
operationId: deactivateMarket,
summary: Deactivate a market,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/property-interests:
get:
{
tags: [Reference data],
operationId: listPropertyInterests,
summary: List active property interests,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
post:
{
tags: [Reference data],
operationId: createPropertyInterest,
summary: Create a property interest,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/property-interests/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
patch:
{
tags: [Reference data],
operationId: updatePropertyInterest,
summary: Update a property interest,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
delete:
{
tags: [Reference data],
operationId: deactivatePropertyInterest,
summary: Deactivate a property interest,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/suppressions:
get:
{
tags: [Suppressions],
operationId: listSuppressions,
summary: List global suppressions,
parameters:
[{ $ref: "#/components/parameters/Page" }, { $ref: "#/components/parameters/Limit" }],
responses: { "200": { $ref: "#/components/responses/Paginated" } },
}
/suppressions/manual:
post:
{
tags: [Suppressions],
operationId: createManualSuppression,
summary: Add or strengthen a global suppression,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/suppressions/{id}/release:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Suppressions],
operationId: releaseSuppression,
summary: Admin-only release with a written reason,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
},
}
/listings:
get:
{
tags: [Listings],
operationId: listListings,
summary: Search and page listings,
parameters:
[
{ $ref: "#/components/parameters/Page" },
{ $ref: "#/components/parameters/Limit" },
{ $ref: "#/components/parameters/Cursor" },
{ $ref: "#/components/parameters/Search" },
{ $ref: "#/components/parameters/ListingStatus" },
{ $ref: "#/components/parameters/PropertyType" },
{ $ref: "#/components/parameters/TransactionType" },
{ $ref: "#/components/parameters/AgentId" },
],
responses: { "200": { $ref: "#/components/responses/Paginated" } },
}
post:
{
tags: [Listings],
operationId: createListing,
summary: Create a listing draft,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/listings/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Listings],
operationId: getListing,
summary: Return property facts assets and campaigns,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"404": { $ref: "#/components/responses/Error" },
},
}
patch:
{
tags: [Listings],
operationId: updateListing,
summary: Update a listing with optimistic versioning,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/listings/{id}/duplicate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Listings],
operationId: duplicateListing,
summary: Duplicate listing content into a draft,
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/archive:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Listings],
operationId: archiveListing,
summary: Archive a listing,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/assets:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
tags: [Listings]
operationId: uploadListingAsset
summary: Validate transform and store a listing asset
requestBody:
{
required: true,
content:
{
multipart/form-data:
{
schema:
{
type: object,
required: [file, kind],
properties:
{
file: { type: string, format: binary },
kind: { type: string },
altText: { type: string },
},
},
},
},
}
responses:
{
"201": { $ref: "#/components/responses/Success" },
"413": { $ref: "#/components/responses/Error" },
"415": { $ref: "#/components/responses/Error" },
}
/listings/{id}/assets/reorder:
parameters: [{ $ref: "#/components/parameters/Id" }]
patch:
{
tags: [Listings],
operationId: reorderListingAssets,
summary: Reorder or reclassify assets,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/assets/{assetId}:
parameters:
[{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/AssetId" }]
delete:
{
tags: [Listings],
operationId: deleteListingAsset,
summary: Soft-delete an asset reference,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/audiences/estimate:
post:
{
tags: [Audiences],
operationId: estimateAudience,
summary: Estimate matched eligible suppressed and unknown contacts,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/audiences:
get:
{
tags: [Audiences],
operationId: listAudiences,
summary: List saved audience definitions,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
post:
{
tags: [Audiences],
operationId: createAudience,
summary: Save a validated audience DSL document,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/audiences/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Audiences],
operationId: getAudience,
summary: Get a saved audience,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
patch:
{
tags: [Audiences],
operationId: updateAudience,
summary: Update a saved audience,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
delete:
{
tags: [Audiences],
operationId: deleteAudience,
summary: Delete an unused saved audience,
responses: { "204": { description: Deleted } },
}
/audiences/{id}/duplicate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Audiences],
operationId: duplicateAudience,
summary: Duplicate a saved audience,
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/sender-profiles:
get:
{
tags: [Senders],
operationId: listSenderProfiles,
summary: List sender policies and verification state,
description: "Tracking is managed on the provider sending domain and shared by every sender on that domain. Returned openTrackingEnabled/clickTrackingEnabled are effective observed values, including null when configuration is stale, unverified or unavailable; they are not editable sender preferences. Tracking observation reads use a bounded cache.",
responses:
{
"200":
{
description: Sender policies with observed tracking capability,
content:
{
application/json:
{
schema:
{
type: object,
properties:
{
items:
{
type: array,
items:
{
type: object,
additionalProperties: true,
properties:
{
openTrackingEnabled: { type: [boolean, "null"] },
clickTrackingEnabled: { type: [boolean, "null"] },
tracking:
{
$ref: "#/components/schemas/DomainTrackingObservation",
},
},
},
},
},
},
},
},
},
},
}
post:
{
tags: [Senders],
operationId: createSenderProfile,
summary: Create an unverified sender policy,
description: "ADMIN only. openTrackingEnabled and clickTrackingEnabled are not sender-level controls. Supplying either boolean returns 409 TRACKING_MANAGED_BY_PROVIDER_DOMAIN. Configure the sending domain in the provider first, then refresh the observed configuration.",
requestBody: { $ref: "#/components/requestBodies/SenderPolicyBody" },
responses:
{
"201": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/sender-profiles/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
patch:
{
tags: [Senders],
operationId: updateSenderProfile,
summary: Update sender windows quota or warm-up,
description: "ADMIN only. Supplying openTrackingEnabled or clickTrackingEnabled returns 409 TRACKING_MANAGED_BY_PROVIDER_DOMAIN, even when the value matches current provider state. Tracking applies to the sending domain and cannot be toggled on an individual sender.",
requestBody: { $ref: "#/components/requestBodies/SenderPolicyBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
delete:
{
tags: [Senders],
operationId: deleteUnreferencedSenderProfile,
summary: Delete a sender only when no campaign references it,
parameters:
[
{
name: confirmation,
in: query,
required: true,
schema: { type: string, enum: [DELETE_UNREFERENCED_SENDER] },
},
],
responses:
{ "204": { description: Deleted }, "409": { $ref: "#/components/responses/Error" } },
}
/sender-profiles/{id}/verify:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Senders],
operationId: verifySenderProfile,
summary: Record an administrator domain-verification confirmation,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/sender-profiles/{id}/tracking/refresh:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
tags: [Senders]
operationId: refreshSenderDomainTracking
summary: Refresh observed provider-domain tracking configuration
description: >-
ADMIN only. Reads the provider's settings for the domain of the sender's actual From
address and persists a shared observation. This does not enable tracking, change DNS,
send an email, or revise historical recipient tracking snapshots. Verified fresh
capability is true, explicit disabled configuration is false, and unavailable/stale/
unverified state is null. A bounded refresh lease may return another in-progress read.
responses:
"200":
description: Effective tracking observation for the sending domain
content:
application/json:
schema:
type: object
required: [tracking]
properties:
tracking: { $ref: "#/components/schemas/DomainTrackingObservation" }
"403": { $ref: "#/components/responses/Error" }
/sender-profiles/{id}/suspend:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Senders],
operationId: suspendSenderProfile,
summary: Suspend a sender from future campaigns,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/sender-profiles/{id}/quota:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Senders],
operationId: getSenderQuota,
summary: Return recent reserved accepted and released quota,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/sender-profiles/{id}/reactivate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Senders],
operationId: reactivateSenderProfile,
summary: Reactivate a sender after explicit DNS review confirmation,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/sender-profiles/{id}/default:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Senders],
operationId: setDefaultSenderProfile,
summary: Atomically make a sender the default,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns:
get:
{
tags: [Campaigns],
operationId: listCampaigns,
summary: Search and page campaigns,
parameters:
[
{ $ref: "#/components/parameters/Page" },
{ $ref: "#/components/parameters/Limit" },
{ $ref: "#/components/parameters/Cursor" },
{ $ref: "#/components/parameters/Search" },
],
responses: { "200": { $ref: "#/components/responses/Paginated" } },
}
post:
{
tags: [Campaigns],
operationId: createCampaign,
summary: Create a mutable campaign draft,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/campaigns/quick-start:
post:
tags: [Campaigns]
operationId: quickStartCampaign
summary: Create or reuse a recent listing-email draft with safe defaults
parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }]
requestBody: { $ref: "#/components/requestBodies/JsonBody" }
responses:
{
"200": { $ref: "#/components/responses/Success" },
"201": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
}
/campaigns/{id}:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Campaigns],
operationId: getCampaign,
summary: Get campaign configuration,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
patch:
{
tags: [Campaigns],
operationId: updateCampaign,
summary: Update an editable campaign using If-Match version,
parameters:
[{ name: If-Match, in: header, required: true, schema: { type: integer, minimum: 1 } }],
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/campaigns/{id}/duplicate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: duplicateCampaign,
summary: Duplicate a campaign into a new draft,
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/preview:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: previewCampaign,
summary: Render HTML and text without sending,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/test-send:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: testSendCampaign,
summary: Send the current version only to the authenticated user's allowlisted address,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
"409": { $ref: "#/components/responses/Error" },
},
}
/campaigns/{id}/recipients/onekey-nearby:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
tags: [Campaigns, OneKey]
operationId: configureCampaignOneKeyRecipients
summary: Import compliant nearby OneKey agents and attach a reusable audience to the draft
requestBody: { $ref: "#/components/requestBodies/JsonBody" }
responses:
{
"200": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
}
/campaigns/{id}/publish:
parameters:
[
{ $ref: "#/components/parameters/Id" },
{ $ref: "#/components/parameters/RequiredIdempotencyKey" },
]
post:
tags: [Campaigns]
operationId: publishCampaign
summary: Atomically validate the current tested version and queue its immutable snapshot
requestBody: { $ref: "#/components/requestBodies/JsonBody" }
responses:
{
"202": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
}
/campaigns/{id}/mark-ready:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: markCampaignReady,
summary: Mark a draft ready for final review,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/schedule:
parameters:
[{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
post:
{
tags: [Campaigns],
operationId: scheduleCampaign,
summary: Queue a durable recipient/content snapshot for a future delivery time,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"202": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/campaigns/{id}/send-now:
parameters:
[{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
post:
{
tags: [Campaigns],
operationId: queueCampaignNow,
summary: Queue a durable recipient/content snapshot for immediate delivery,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"202": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/campaigns/{id}/pause:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: pauseCampaign,
summary: Pause future delivery claims,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/resume:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: resumeCampaign,
summary: Resume a paused campaign,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/cancel:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Campaigns],
operationId: cancelCampaign,
summary: Cancel all recipients not submitted to the provider,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/stats:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Analytics],
operationId: getCampaignStats,
summary: Recompute and return campaign aggregates,
responses:
{
"200":
{
description: Compatibility aggregates and coverage-aware report,
content:
{
application/json:
{
schema:
{
allOf:
[
{ $ref: "#/components/schemas/CampaignLegacyStats" },
{
type: object,
required: [reporting],
properties:
{
reporting: { $ref: "#/components/schemas/CampaignReporting" },
},
},
],
},
},
},
},
},
}
/product-events:
post:
tags: [Analytics]
operationId: recordProductEvent
summary: Record an allowlisted low-cardinality composer event in the audit trail
requestBody: { $ref: "#/components/requestBodies/JsonBody" }
responses: { "204": { description: Event recorded } }
/campaigns/{id}/recipients:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Campaigns],
operationId: listCampaignRecipients,
summary: Page frozen recipient outcomes,
parameters:
[
{ $ref: "#/components/parameters/Page" },
{ $ref: "#/components/parameters/Limit" },
{ $ref: "#/components/parameters/RecipientSendState" },
{ $ref: "#/components/parameters/RecipientDeliveryState" },
],
responses: { "200": { $ref: "#/components/responses/Paginated" } },
}
/campaigns/{id}/recipients/{recipientId}:
parameters:
[{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/RecipientId" }]
get:
{
tags: [Campaigns],
operationId: getCampaignRecipient,
summary: Get one recipient attempts and provider events,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/events:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Campaigns],
operationId: listCampaignEvents,
summary: List retained provider webhook events,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
/campaigns/{id}/export.csv:
parameters: [{ $ref: "#/components/parameters/Id" }]
get:
{
tags: [Campaigns],
operationId: exportCampaignRecipients,
summary: Export formula-safe recipient outcomes,
parameters:
[
{ $ref: "#/components/parameters/RecipientSendState" },
{ $ref: "#/components/parameters/RecipientDeliveryState" },
],
responses: { "200": { $ref: "#/components/responses/Csv" } },
}
/dashboard/summary:
get:
{
tags: [Analytics],
operationId: getDashboardSummary,
summary: Return marketing delivery and action-required metrics,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/dashboard/recent-campaigns:
get:
{
tags: [Analytics],
operationId: getRecentCampaigns,
summary: Return the ten most recently changed campaigns,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
/audit-logs:
get:
{
tags: [Operations],
operationId: listAuditLogs,
summary: Page administrative audit events,
parameters:
[{ $ref: "#/components/parameters/Page" }, { $ref: "#/components/parameters/Limit" }],
responses:
{
"200": { $ref: "#/components/responses/Collection" },
"403": { $ref: "#/components/responses/Error" },
},
}
/system/readiness:
get:
{
tags: [Operations],
operationId: getSystemReadiness,
summary: Return non-secret database storage sender and worker readiness,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
},
}
/system/sending/pause:
post:
{
tags: [Operations],
operationId: pauseGlobalSending,
summary: Activate the global send kill switch,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
},
}
/system/sending/resume:
post:
{
tags: [Operations],
operationId: resumeGlobalSending,
summary: Release the global send kill switch after operator review,
responses:
{
"200": { $ref: "#/components/responses/Success" },
"403": { $ref: "#/components/responses/Error" },
},
}
/public/webhooks/resend:
post:
{
tags: [Public],
operationId: ingestResendWebhook,
summary: Verify and retain the untouched signed Resend webhook,
security: [],
servers: [{ url: /api }],
requestBody:
{
required: true,
content: { application/json: { schema: { type: object, additionalProperties: true } } },
},
responses:
{
"200": { $ref: "#/components/responses/Success" },
"401": { $ref: "#/components/responses/Error" },
},
}
/public/unsubscribe/confirm:
post:
{
tags: [Public],
operationId: confirmVisibleUnsubscribe,
summary: Confirm a visible signed unsubscribe link,
security: [],
servers: [{ url: /api }],
requestBody: { $ref: "#/components/requestBodies/TokenBody" },
responses:
{
"200":
{
description: Human-readable confirmation,
content: { text/html: { schema: { type: string } } },
},
"400": { $ref: "#/components/responses/Error" },
},
}
/public/unsubscribe/one-click:
post:
{
tags: [Public],
operationId: oneClickUnsubscribe,
summary: RFC 8058 one-click unsubscribe,
security: [],
servers: [{ url: /api }],
parameters: [{ name: token, in: query, required: false, schema: { type: string } }],
responses:
{ "204": { description: Suppressed }, "400": { $ref: "#/components/responses/Error" } },
}
/ai/status:
get:
{
tags: [AI],
operationId: getAiStatus,
summary: Return server-side AI feature state without secrets,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/ai/generate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [AI],
operationId: generateListingAiCopy,
summary: Generate a non-destructive structured listing-copy proposal,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses:
{
"200": { $ref: "#/components/responses/Success" },
"409": { $ref: "#/components/responses/Error" },
},
}
/listings/{id}/ai/apply:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [AI],
operationId: applyListingAiCopy,
summary: Explicitly apply selected fields from a stored proposal,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/ai/generate:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [AI],
operationId: generateCampaignAiCopy,
summary: Generate three non-destructive campaign variants,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/campaigns/{id}/ai/apply:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [AI],
operationId: applyCampaignAiCopy,
summary: Explicitly apply selected fields from one campaign variant,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/onekey/status:
get:
{
tags: [OneKey],
operationId: getOneKeyStatus,
summary: Return provider index and cursor health,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/onekey/test:
post:
{
tags: [OneKey],
operationId: testOneKeyProvider,
summary: Test the configured BBO listing-data connection,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/onekey/sync/{kind}:
parameters:
[
{
name: kind,
in: path,
required: true,
schema: { type: string, enum: [initial, delta, rebuild] },
},
]
post:
{
tags: [OneKey],
operationId: queueOneKeySync,
summary: Queue an initial delta or rebuild index job,
responses: { "202": { $ref: "#/components/responses/Success" } },
}
/onekey/listings/search:
get:
{
tags: [OneKey],
operationId: searchOneKeyListings,
summary: Search the local OneKey index first and BBO provider second; bare numeric MLS values automatically match BBO KEY-prefixed IDs,
parameters:
[
{
name: q,
in: query,
required: true,
schema: { type: string, minLength: 2, maxLength: 200 },
},
{ $ref: "#/components/parameters/Limit" },
],
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
/onekey/listings/{sourceKey}:
parameters: [{ $ref: "#/components/parameters/SourceKey" }]
get:
{
tags: [OneKey],
operationId: reviewOneKeyListing,
summary: Return source facts beside any imported marketing record,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/onekey/listings/{sourceKey}/import:
parameters: [{ $ref: "#/components/parameters/SourceKey" }]
post:
{
tags: [OneKey],
operationId: importOneKeyListing,
summary: Import a viewable listing and automatically synchronize its listing Agent from BBO,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/onekey/listings/{sourceKey}/recipients:
parameters: [{ $ref: "#/components/parameters/SourceKey" }]
get:
{
tags: [OneKey],
operationId: previewOneKeyRecipientCandidates,
summary: Preview active external agents matched through recent Closed transactions,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/onekey/refresh:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [OneKey],
operationId: refreshImportedOneKeyListing,
summary: Refresh source facts while preserving all marketing overrides,
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/onekey/media/retry:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [OneKey],
operationId: retryOneKeyMediaImport,
summary: Retry bounded copying into Homix-owned storage,
responses: { "202": { $ref: "#/components/responses/Success" } },
}
/listings/{id}/onekey/recipients/import:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [OneKey],
operationId: importOneKeyRecipientCandidates,
summary: Explicitly import candidates and create a saved audience without sending,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "201": { $ref: "#/components/responses/Success" } },
}
/operations/manual-review:
get:
{
tags: [Operations],
operationId: listManualReviewBatches,
summary: List uncertain provider submissions awaiting an operator,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
/operations/manual-review/{id}/resolve:
parameters: [{ $ref: "#/components/parameters/Id" }]
post:
{
tags: [Operations],
operationId: resolveManualReviewBatch,
summary: Apply a confirmed audited quota-safe resolution,
requestBody: { $ref: "#/components/requestBodies/JsonBody" },
responses: { "200": { $ref: "#/components/responses/Success" } },
}
/operations/webhooks:
get:
{
tags: [Operations],
operationId: listWebhookReconciliation,
summary: List unresolved retry-pending and dead-letter webhooks,
responses: { "200": { $ref: "#/components/responses/Collection" } },
}
components:
securitySchemes:
portalBearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Short-lived signed Portal identity bound to HTTP method, path and request body.
cookieAuth:
{
type: apiKey,
in: cookie,
name: homix_session,
description: Local development cookie; Azure production uses Easy Auth before this API.,
}
parameters:
Id: { name: id, in: path, required: true, schema: { type: string, format: uuid } }
AssetId: { name: assetId, in: path, required: true, schema: { type: string, format: uuid } }
RecipientId:
{ name: recipientId, in: path, required: true, schema: { type: string, format: uuid } }
SourceKey:
{ name: sourceKey, in: path, required: true, schema: { type: string, maxLength: 255 } }
Page:
{ name: page, in: query, required: false, schema: { type: integer, minimum: 1, default: 1 } }
Limit:
{
name: limit,
in: query,
required: false,
schema: { type: integer, minimum: 1, maximum: 100, default: 25 },
}
Cursor:
{
name: cursor,
in: query,
required: false,
schema: { type: string, format: uuid },
description: Opaque ID returned as nextCursor by the preceding page.,
}
Search: { name: search, in: query, required: false, schema: { type: string, maxLength: 200 } }
ContactType:
{
name: contactType,
in: query,
schema:
{
type: string,
enum:
[
BUYER,
SELLER,
INVESTOR,
BROKER,
TENANT,
LANDLORD,
DEVELOPER,
LENDER,
ATTORNEY,
VENDOR,
PAST_CLIENT,
OTHER,
],
},
}
ContactSourceType:
{
name: sourceType,
in: query,
schema:
{
type: string,
enum:
[
PAST_CLIENT,
OPEN_HOUSE,
WEBSITE,
BROKER_RELATIONSHIP,
EVENT,
CRM_IMPORT,
MANUAL,
REFERRAL,
LEGACY_EMAIL_SERVICE,
OTHER,
],
},
}
PermissionBasis:
{
name: permissionBasis,
in: query,
schema: { type: string, enum: [OPT_IN, EXISTING_RELATIONSHIP, BUSINESS_CONTACT, UNKNOWN] },
}
ContactStatus:
{ name: status, in: query, schema: { type: string, enum: [ACTIVE, INACTIVE, ARCHIVED] } }
MarketId: { name: marketId, in: query, schema: { type: string, format: uuid } }
PropertyInterestId:
{ name: propertyInterestId, in: query, schema: { type: string, format: uuid } }
TagId: { name: tagId, in: query, schema: { type: string, format: uuid } }
Suppressed: { name: suppressed, in: query, schema: { type: string, enum: ["true", "false"] } }
ContactSort:
{
name: sort,
in: query,
schema: { type: string, enum: [createdAt, lastEngagedAt, lastSentAt], default: createdAt },
}
SortOrder:
{ name: order, in: query, schema: { type: string, enum: [asc, desc], default: desc } }
ListingStatus:
{
name: status,
in: query,
schema:
{
type: string,
enum: [DRAFT, ACTIVE, UNDER_CONTRACT, SOLD, LEASED, WITHDRAWN, ARCHIVED],
},
}
PropertyType:
{
name: propertyType,
in: query,
schema:
{
type: string,
enum:
[
OFFICE,
RETAIL,
INDUSTRIAL,
MULTIFAMILY,
LAND,
MIXED_USE,
HOSPITALITY,
SPECIAL_PURPOSE,
BUSINESS,
RESIDENTIAL,
OTHER,
],
},
}
TransactionType:
{
name: transactionType,
in: query,
schema: { type: string, enum: [FOR_SALE, FOR_LEASE, SALE_OR_LEASE] },
}
AgentId: { name: agentId, in: query, schema: { type: string, format: uuid } }
RecipientSendState:
{
name: sendState,
in: query,
schema:
{
type: string,
enum:
[
PENDING,
RESERVED,
SENDING,
ACCEPTED,
TEMPORARY_FAILED,
PERMANENT_FAILED,
SUPPRESSED,
CANCELLED,
MANUAL_REVIEW,
],
},
}
RecipientDeliveryState:
{
name: deliveryState,
in: query,
schema:
{ type: string, enum: [UNKNOWN, DELIVERED, BOUNCED, COMPLAINED, PROVIDER_SUPPRESSED] },
}
IdempotencyKey:
{
name: Idempotency-Key,
in: header,
required: false,
schema: { type: string, maxLength: 80 },
description: Stable client action key used to deduplicate schedule or send-now.,
}
RequiredIdempotencyKey:
{
name: Idempotency-Key,
in: header,
required: true,
schema: { type: string, minLength: 8, maxLength: 200 },
description: Stable client action key required to safely retry a composer action.,
}
requestBodies:
SenderPolicyBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/SenderPolicyInput" }
JsonBody:
{
required: true,
content: { application/json: { schema: { type: object, additionalProperties: true } } },
}
TokenBody:
{
required: true,
content:
{
application/x-www-form-urlencoded:
{
schema:
{
type: object,
required: [token],
properties: { token: { type: string, minLength: 20, maxLength: 1000 } },
},
},
application/json:
{
schema:
{
type: object,
required: [token],
properties: { token: { type: string, minLength: 20, maxLength: 1000 } },
},
},
},
}
schemas:
DomainTrackingObservation:
type: object
required:
[
version,
provider,
domain,
revision,
checkedAt,
openTrackingEnabled,
clickTrackingEnabled,
trackingDomain,
managedBy,
]
properties:
version: { type: integer, const: 1 }
provider: { type: string }
domain: { type: string }
revision: { type: [string, "null"] }
checkedAt: { type: [string, "null"], format: date-time }
openTrackingEnabled: { type: [boolean, "null"] }
clickTrackingEnabled: { type: [boolean, "null"] }
trackingDomain: { type: [string, "null"] }
managedBy: { type: string, const: provider-domain }
SenderPolicyInput:
type: object
additionalProperties: true
description: Sender policy fields; provider-domain tracking capabilities are read-only.
not:
anyOf:
- properties: { openTrackingEnabled: {} }
required: [openTrackingEnabled]
- properties: { clickTrackingEnabled: {} }
required: [clickTrackingEnabled]
CampaignLegacyStats:
type: object
description: >-
Compatibility counts retain event-history semantics and may overlap. acceptedCount uses
immutable acceptedAt (or legacy ACCEPTED state). Use reporting for disjoint outcomes and
tracking availability; legacy openedCount/clickedCount equal to zero do not mean no reader.
properties:
targetCount: { type: integer, minimum: 0 }
eligibleCount: { type: integer, minimum: 0 }
suppressedCount: { type: integer, minimum: 0 }
acceptedCount: { type: integer, minimum: 0 }
deliveredCount: { type: integer, minimum: 0 }
openedCount: { type: integer, minimum: 0 }
clickedCount: { type: integer, minimum: 0 }
bouncedCount: { type: integer, minimum: 0 }
complainedCount: { type: integer, minimum: 0 }
failedCount: { type: integer, minimum: 0 }
EngagementReporting:
type: object
required:
[
availability,
count,
rate,
coverageSentCount,
coverageDeliveredCount,
untrackedSentCount,
unknownSentCount,
startedAt,
asOf,
]
properties:
availability:
type: string
enum: [disabled, unknown, active, partial]
description: "disabled means all accepted messages explicitly had tracking off; unknown includes missing historical snapshots or no accepted messages; active covers every accepted message; partial covers a proper subset. This describes historical message coverage, not the provider's current configuration or current webhook health."
count:
type: [integer, "null"]
minimum: 0
description: "Unique tracked accepted recipient messages with an engagement event. Clicks count classified listing CTA events only. Null when there is no known tracked cohort. Zero is reserved for an observed tracked cohort without recorded engagement."
rate:
type: [number, "null"]
minimum: 0
maximum: 1
description: "Engaged messages with historical delivery receipts divided by delivered messages in the same tracked cohort. Clicks without delivery receipts remain in count but are excluded from the rate until delivery is confirmed. Null for a zero denominator."
coverageSentCount: { type: integer, minimum: 0 }
coverageDeliveredCount: { type: integer, minimum: 0 }
untrackedSentCount: { type: integer, minimum: 0 }
unknownSentCount: { type: integer, minimum: 0 }
startedAt:
type: [string, "null"]
format: date-time
description: First accepted tracked message in this campaign; coverage need not be contiguous.
asOf: { type: string, format: date-time }
CampaignReporting:
type: object
required: [version, asOf, delivery, clicks, opens]
properties:
version: { type: integer, const: 1 }
asOf: { type: string, format: date-time }
delivery:
type: object
description: >-
acceptedCount = deliveredCount + pendingCount + undeliveredCount.
targetCount = acceptedCount + pendingSendCount + failedBeforeSendCount + excludedCount
+ cancelledCount + manualReviewCount. Historical bounce/provider suppression or
permanent send failure takes precedence over a prior delivery receipt. Complaint
and unsubscribe are separate facts and do not themselves subtract a delivery.
required:
[
targetCount,
acceptedCount,
deliveredCount,
pendingCount,
undeliveredCount,
pendingSendCount,
failedBeforeSendCount,
excludedCount,
cancelledCount,
manualReviewCount,
bounce,
suppressionReasons,
]
properties:
targetCount: { type: integer, minimum: 0 }
acceptedCount: { type: integer, minimum: 0 }
deliveredCount: { type: integer, minimum: 0 }
pendingCount: { type: integer, minimum: 0 }
undeliveredCount: { type: integer, minimum: 0 }
pendingSendCount: { type: integer, minimum: 0 }
failedBeforeSendCount: { type: integer, minimum: 0 }
excludedCount: { type: integer, minimum: 0 }
cancelledCount: { type: integer, minimum: 0 }
manualReviewCount: { type: integer, minimum: 0 }
bounce:
type: object
required: [permanentCount, transientCount, undeterminedCount]
properties:
permanentCount: { type: integer, minimum: 0 }
transientCount: { type: integer, minimum: 0 }
undeterminedCount: { type: integer, minimum: 0 }
suppressionReasons:
type: array
items:
type: object
required: [reason, count]
properties:
reason: { type: string }
count: { type: integer, minimum: 0 }
clicks: { $ref: "#/components/schemas/EngagementReporting" }
opens:
allOf: [{ $ref: "#/components/schemas/EngagementReporting" }]
description: Approximate opening signals for diagnostics; never a true reader count.
sending:
type: object
description: Dynamic Portal read-time state. Global pause is returned separately and overrides this state.
required: [state, nextSendAt, estimateKind]
properties:
state:
type: string
enum:
[
draft,
ready,
preparing,
scheduled,
sending,
waiting_window,
waiting_cadence,
paused,
blocked,
completed,
cancelled,
failed,
archived,
]
nextSendAt: { type: [string, "null"], format: date-time }
estimateKind:
type: [string, "null"]
enum: [earliest_possible, null]
description: Next eligible time only; shared queue load, daily quota and retries may delay delivery.
AnyObject: { type: object, additionalProperties: true }
Error:
type: object
required: [error]
properties:
error:
type: object
required: [code, message, requestId]
properties:
{
code: { type: string },
message: { type: string },
details: { type: object, additionalProperties: true },
requestId: { type: string },
}
responses:
Success:
{
description: Successful JSON response,
content: { application/json: { schema: { $ref: "#/components/schemas/AnyObject" } } },
}
Collection:
{
description: JSON collection,
content:
{
application/json:
{
schema:
{
type: object,
required: [items],
properties:
{ items: { type: array, items: { $ref: "#/components/schemas/AnyObject" } } },
},
},
},
}
Paginated:
{
description: Paginated JSON collection,
content:
{
application/json:
{
schema:
{
type: object,
required: [items, total, page, limit],
properties:
{
items: { type: array, items: { $ref: "#/components/schemas/AnyObject" } },
total: { type: integer },
page: { type: integer },
limit: { type: integer },
nextCursor: { type: [string, "null"], format: uuid },
},
},
},
},
}
Csv:
{
description: Formula-safe CSV attachment,
content: { text/csv: { schema: { type: string } } },
}
Error:
{
description: Structured API error,
content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } },
}