openapi: 3.2.0 info: title: CommsHarbor Organizations API version: 2d690e87 description: CommsHarbor API. Organization identity is explicit and tenant-scoped. servers: - url: https://commsharbor.com tags: - name: Organizations paths: /api/organizations: get: operationId: commsharbor_organizations summary: List the organizations the current person belongs to description: 'Returns: { organizations[{id,name,sending_domain,created_at}] }' security: - bearerAuth: [] responses: '200': description: '{ organizations[{id,name,sending_domain,created_at}] }' content: application/json: schema: type: object properties: organizations: type: array items: $ref: '#/components/schemas/Organization' description: Every organization with a membership for this person. required: - organizations '401': description: No session, or the session expired. tags: - Organizations post: operationId: commsharbor_organization_create summary: Create one organization trial for a verified owner and a sending domain description: 'One trial per owner. The domain is registered here but not verified — DNS state is only ever observed, in the domains resource. Returns: { id, name, sending_domain, created_at }' security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Display name of the organization. sending_domain: type: string description: Domain the organization will send from. required: - name - sending_domain example: name: Acme sending_domain: example.com responses: '200': description: '{ id, name, sending_domain, created_at }' content: application/json: schema: $ref: '#/components/schemas/Organization' '400': description: Missing name or domain. '401': description: No session, or the session expired. '409': description: This owner already has a trial organization. tags: - Organizations /api/organizations/{organization_id}: get: operationId: commsharbor_organization_get summary: Read one organization description: 'Returns: { id, name, sending_domain, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ id, name, sending_domain, created_at }' content: application/json: schema: $ref: '#/components/schemas/Organization' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_organization_update summary: Update an organization's name description: 'Returns: { id, name, sending_domain, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: New display name. required: - name example: name: Acme Global responses: '200': description: '{ id, name, sending_domain, created_at }' content: application/json: schema: $ref: '#/components/schemas/Organization' '400': description: Empty name. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/members: get: operationId: commsharbor_members summary: List the members of an organization and their roles description: 'Returns: { members[{organization_id,user_id,role,created_at}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ members[{organization_id,user_id,role,created_at}] }' content: application/json: schema: type: object properties: members: type: array items: $ref: '#/components/schemas/Membership' description: Every member and the role they hold. required: - members '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/members/{user_id}: patch: operationId: commsharbor_member_role summary: Change the role of a member who is not the owner description: 'The owner''s role cannot be changed through this route — an organization that can lose its last owner is an organization nobody can administer. Returns: { organization_id, user_id, role, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: user_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: role: type: string description: Role to give the member from now on. required: - role example: role: marketer responses: '200': description: '{ organization_id, user_id, role, created_at }' content: application/json: schema: $ref: '#/components/schemas/Membership' '400': description: Unknown role. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/invitations: get: operationId: commsharbor_invitations summary: List invitations without their tokens description: 'The token is shown once, when the invitation is created. Listing never shows it again. Returns: { invitations[{id,email,role,expires_at,invitation_token?,token_notice?}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ invitations[{id,email,role,expires_at,invitation_token?,token_notice?}] }' content: application/json: schema: type: object properties: invitations: type: array items: $ref: '#/components/schemas/Invitation' description: Invitations for this organization, without tokens. required: - invitations '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_invitation_create summary: Create an invitation and reveal its token exactly once description: 'Store the token now: this is the only response that carries it. Returns: { id, email, role, expires_at, invitation_token?, token_notice? }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: Who to invite. role: type: string description: Role the invitation grants. required: - email - role example: email: member@example.com role: viewer responses: '200': description: '{ id, email, role, expires_at, invitation_token?, token_notice? }' content: application/json: schema: $ref: '#/components/schemas/Invitation' '400': description: Missing email or unknown role. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/api-keys: get: operationId: commsharbor_api_keys summary: List API keys without token hashes or secrets description: 'Returns: { api_keys[{id,name,scopes,prefix,last_used_at,revoked_at,created_at,token?}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ api_keys[{id,name,scopes,prefix,last_used_at,revoked_at,created_at,token?}] }' content: application/json: schema: type: object properties: api_keys: type: array items: $ref: '#/components/schemas/ApiKey' description: Keys of this organization, without the tokens. required: - api_keys '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_api_key_create summary: Create a scoped API key and reveal it exactly once description: 'The key determines its own organization. It cannot manage credentials or membership — those stay with human admins. Returns: { id, name, scopes, prefix, last_used_at, revoked_at, created_at, token? }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Label so the key can be recognised later. scopes: type: array items: type: string description: What the key may do, e.g. `organization:read`, `crm:write`, `messages:send`. required: - name - scopes example: name: Automation scopes: - organization:read - crm:read - crm:write responses: '200': description: '{ id, name, scopes, prefix, last_used_at, revoked_at, created_at, token? }' content: application/json: schema: $ref: '#/components/schemas/ApiKey' '400': description: Missing name or unknown scope. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/api-keys/{api_key_id}: delete: operationId: commsharbor_api_key_revoke summary: Revoke an API key. It stops working immediately, not at the next cache expiry description: 'Returns: { id, name, scopes, prefix, last_used_at, revoked_at, created_at, token? }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: api_key_id in: path required: true schema: type: string responses: '200': description: '{ id, name, scopes, prefix, last_used_at, revoked_at, created_at, token? }' content: application/json: schema: $ref: '#/components/schemas/ApiKey' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/audit: get: operationId: commsharbor_audit summary: List the append-only audit trail of the organization description: 'Audit records are never rewritten and never carry recipient PII. Returns: { items[{id,action,actor_user_id,target,metadata,created_at}], nextCursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,action,actor_user_id,target,metadata,created_at}], nextCursor }' content: application/json: schema: $ref: '#/components/schemas/PageCamelAuditEvent' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/domains: get: operationId: commsharbor_domains summary: List the sending domains of the organization and their last observed state description: '`status` is what was last OBSERVED at SES and DNS. It does not become `active` because provisioning was requested. Returns: { items[{id,domain,status,dkim_tokens,dmarc,mail_from,last_observed_at,created_at}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ items[{id,domain,status,dkim_tokens,dmarc,mail_from,last_observed_at,created_at}] }' content: application/json: schema: $ref: '#/components/schemas/ListaDomain' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_domain_create summary: Register the organization's sending domain and queue idempotent SES provisioning description: 'Asking twice does not provision twice. Publish the returned DKIM records, then call `verify` to have the state observed. Returns: { domain{id,domain,status,dkim_tokens,dmarc,mail_from,last_observed_at,created_at}, provisioning{queued,job_id} }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: domain: type: string description: The domain to send from, e.g. `example.com`. required: - domain example: domain: example.com responses: '200': description: '{ domain{id,domain,status,dkim_tokens,dmarc,mail_from,last_observed_at,created_at}, provisioning{queued,job_id} }' content: application/json: schema: type: object properties: domain: allOf: - $ref: '#/components/schemas/Domain' description: The registered domain, with the DKIM records to publish. provisioning: allOf: - $ref: '#/components/schemas/Provisioning' description: Whether this call enqueued provisioning work. required: - domain - provisioning '400': description: Missing or malformed domain. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/domains/{domain_id}: get: operationId: commsharbor_domain_get summary: Read one sending domain, without inferring current DNS state description: 'This returns what was stored at the last observation. To look again, call `verify`. Returns: { id, domain, status, dkim_tokens, dmarc, mail_from, last_observed_at, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: domain_id in: path required: true schema: type: string responses: '200': description: '{ id, domain, status, dkim_tokens, dmarc, mail_from, last_observed_at, created_at }' content: application/json: schema: $ref: '#/components/schemas/Domain' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/domains/{domain_id}/verify: post: operationId: commsharbor_domain_verify summary: Observe SES, DKIM, DMARC and custom MAIL FROM state right now, and store what… description: 'This is the ONLY operation that can move a domain to `active`, and only because it actually looked. Returns: { id, domain, status, dkim_tokens, dmarc, mail_from, last_observed_at, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: domain_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ id, domain, status, dkim_tokens, dmarc, mail_from, last_observed_at, created_at }' content: application/json: schema: $ref: '#/components/schemas/Domain' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/domains/{domain_id}/smoke: post: operationId: commsharbor_domain_smoke summary: Queue one controlled smoke message to the server-side QA recipient description: 'The request never accepts a recipient: the destination is a server-side secret. That is what keeps this from becoming a way to send mail to arbitrary addresses through someone else''s verified domain. Returns: { delivery{id,status,domain_id,template_id,template_version,ses_message_id,created_at}, capacity{remaining,global_remaining,hard_cap}, dispatch{queued,job_id} }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: domain_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ delivery{id,status,domain_id,template_id,template_version,ses_message_id,created_at}, capacity{remaining,global_remaining,hard_cap}, dispatch{queued,job_id} }' content: application/json: schema: type: object properties: delivery: allOf: - $ref: '#/components/schemas/Delivery' description: The delivery record for the smoke. capacity: allOf: - $ref: '#/components/schemas/Capacity' description: What is left to send after it. dispatch: allOf: - $ref: '#/components/schemas/Dispatch' description: The queue work behind it. required: - delivery - capacity - dispatch '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The domain is not active yet — verify it first. '429': description: No capacity left. tags: - Organizations /api/organizations/{organization_id}/domains/{domain_id}/deliveries: get: operationId: commsharbor_domain_deliveries summary: List deliveries sent from one domain, without recipient addresses description: 'Returns: { items[{id,status,domain_id,template_id,template_version,ses_message_id,created_at}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: domain_id in: path required: true schema: type: string responses: '200': description: '{ items[{id,status,domain_id,template_id,template_version,ses_message_id,created_at}] }' content: application/json: schema: $ref: '#/components/schemas/ListaDelivery' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/deliveries/{delivery_id}: get: operationId: commsharbor_delivery_get summary: Read one delivery and its SES MessageId, without recipient data description: '`ses_message_id` is what correlates this record with AWS when you need to chase a message there. Returns: { id, status, domain_id, template_id, template_version, ses_message_id, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: delivery_id in: path required: true schema: type: string responses: '200': description: '{ id, status, domain_id, template_id, template_version, ses_message_id, created_at }' content: application/json: schema: $ref: '#/components/schemas/Delivery' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/deliveries/{delivery_id}/events: get: operationId: commsharbor_delivery_events summary: List the normalized SES feedback events for one delivery description: 'Open and Click are ADDITIVE: they are recorded alongside delivery state and never overwrite it. Returns: { items[{id,event_type,occurred_at,detail}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: delivery_id in: path required: true schema: type: string responses: '200': description: '{ items[{id,event_type,occurred_at,detail}] }' content: application/json: schema: $ref: '#/components/schemas/ListaDeliveryEvent' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/contacts: get: operationId: commsharbor_crm_contacts_list summary: List contacts in the tenant CRM — a person in the tenant CRM description: 'Being in the CRM is not permission to email: consent lives in the `marketing` sub-resource. Returns: { items[{id,email,first_name,last_name,company_id,created_at,updated_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. - name: q in: query required: false schema: type: string description: Free-text search over the record's main fields. - name: company_id in: query required: false schema: type: string description: Restrict to one CRM company. responses: '200': description: '{ items[{id,email,first_name,last_name,company_id,created_at,updated_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageContact' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_contacts_create summary: Create a contact in the tenant CRM description: 'Being in the CRM is not permission to email: consent lives in the `marketing` sub-resource. Returns: { id, email, first_name, last_name, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: Email address. Normalized and never truncated. first_name: type: string description: Given name of the contact. last_name: type: string description: Family name. company_id: type: string description: Company this contact belongs to. required: - email - first_name example: email: buyer@example.com first_name: Buyer company_id: co_… responses: '200': description: '{ id, email, first_name, last_name, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Contact' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/contacts/{contact_id}: get: operationId: commsharbor_crm_contacts_get summary: Read one contact from the tenant CRM description: 'Being in the CRM is not permission to email: consent lives in the `marketing` sub-resource. Returns: { id, email, first_name, last_name, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string responses: '200': description: '{ id, email, first_name, last_name, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Contact' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_contacts_update summary: Update one contact in the tenant CRM. Only the fields you send change description: 'Being in the CRM is not permission to email: consent lives in the `marketing` sub-resource. Returns: { id, email, first_name, last_name, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: Email address. Normalized and never truncated. first_name: type: string description: Given name of the contact. last_name: type: string description: Family name. company_id: type: string description: Company this contact belongs to. required: - email - first_name example: email: buyer@example.com first_name: Buyer company_id: co_… responses: '200': description: '{ id, email, first_name, last_name, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Contact' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_contacts_delete summary: Delete one contact from the tenant CRM. description: 'Being in the CRM is not permission to email: consent lives in the `marketing` sub-resource. Returns: { id, email, first_name, last_name, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string responses: '200': description: '{ id, email, first_name, last_name, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Contact' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/companies: get: operationId: commsharbor_crm_companies_list summary: List companies in the tenant CRM — an organization in the tenant CRM — a… description: 'Returns: { items[{id,name,domain,website,created_at,updated_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. - name: q in: query required: false schema: type: string description: Free-text search over the record's main fields. responses: '200': description: '{ items[{id,name,domain,website,created_at,updated_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageCompany' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_companies_create summary: Create a company in the tenant CRM description: 'Returns: { id, name, domain, website, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Company name. domain: type: string description: Primary domain, used to group contacts. website: type: string description: Website URL. required: - name example: name: Acme domain: example.com website: https://example.com responses: '200': description: '{ id, name, domain, website, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Company' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/companies/{company_id}: get: operationId: commsharbor_crm_companies_get summary: Read one company from the tenant CRM description: 'Returns: { id, name, domain, website, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: company_id in: path required: true schema: type: string responses: '200': description: '{ id, name, domain, website, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Company' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_companies_update summary: Update one company in the tenant CRM. Only the fields you send change description: 'Returns: { id, name, domain, website, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: company_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Company name. domain: type: string description: Primary domain, used to group contacts. website: type: string description: Website URL. required: - name example: name: Acme domain: example.com website: https://example.com responses: '200': description: '{ id, name, domain, website, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Company' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_companies_delete summary: Delete one company from the tenant CRM. description: 'Returns: { id, name, domain, website, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: company_id in: path required: true schema: type: string responses: '200': description: '{ id, name, domain, website, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Company' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/pipelines: get: operationId: commsharbor_crm_pipelines_list summary: List pipelines in the tenant CRM — a named sequence of stages that deals move… description: 'Returns: { items[{id,name,created_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. - name: q in: query required: false schema: type: string description: Free-text search over the record's main fields. responses: '200': description: '{ items[{id,name,created_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PagePipeline' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_pipelines_create summary: Create a pipeline in the tenant CRM description: 'Returns: { id, name, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Pipeline name. stages: type: array items: type: string description: Stage names to create with the pipeline, in order. required: - name example: name: Sales stages: - New - Proposal - Won responses: '200': description: '{ id, name, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Pipeline' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/pipelines/{pipeline_id}: get: operationId: commsharbor_crm_pipelines_get summary: Read one pipeline from the tenant CRM description: 'Returns: { id, name, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string responses: '200': description: '{ id, name, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Pipeline' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_pipelines_update summary: Update one pipeline in the tenant CRM. Only the fields you send change description: 'Returns: { id, name, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Pipeline name. stages: type: array items: type: string description: Stage names to create with the pipeline, in order. required: - name example: name: Sales stages: - New - Proposal - Won responses: '200': description: '{ id, name, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Pipeline' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_pipelines_delete summary: Delete one pipeline from the tenant CRM. description: 'Returns: { id, name, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string responses: '200': description: '{ id, name, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Pipeline' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/deals: get: operationId: commsharbor_crm_deals_list summary: List deals in the tenant CRM — an opportunity moving through a pipeline description: 'Money is in minor units: `value_minor: 10000` with `currency: "USD"` is $100.00, not $10,000. Returns: { items[{id,title,pipeline_id,stage_id,status,value_minor,currency,contact_id,company_id,created_at,updated_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. - name: q in: query required: false schema: type: string description: Free-text search over the record's main fields. - name: pipeline_id in: query required: false schema: type: string description: Restrict to one pipeline. - name: stage_id in: query required: false schema: type: string description: Restrict to one pipeline stage. - name: status in: query required: false schema: type: string description: Restrict to one status value. responses: '200': description: '{ items[{id,title,pipeline_id,stage_id,status,value_minor,currency,contact_id,company_id,created_at,updated_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageDeal' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_deals_create summary: Create a deal in the tenant CRM description: 'Money is in minor units: `value_minor: 10000` with `currency: "USD"` is $100.00, not $10,000. Returns: { id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: title: type: string description: What the deal is. pipeline_id: type: string description: Pipeline the deal lives in. stage_id: type: string description: Stage the deal is at. value_minor: type: integer description: Value in the currency's MINOR unit — cents, not dollars. currency: type: string description: ISO 4217 code for `value_minor`. required: - title - pipeline_id - stage_id example: title: Renewal pipeline_id: pl_… stage_id: st_… value_minor: 10000 currency: USD responses: '200': description: '{ id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Deal' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/deals/{deal_id}: get: operationId: commsharbor_crm_deals_get summary: Read one deal from the tenant CRM description: 'Money is in minor units: `value_minor: 10000` with `currency: "USD"` is $100.00, not $10,000. Returns: { id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: deal_id in: path required: true schema: type: string responses: '200': description: '{ id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Deal' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_deals_update summary: Update one deal in the tenant CRM. Only the fields you send change description: 'Money is in minor units: `value_minor: 10000` with `currency: "USD"` is $100.00, not $10,000. Returns: { id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: deal_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: title: type: string description: What the deal is. pipeline_id: type: string description: Pipeline the deal lives in. stage_id: type: string description: Stage the deal is at. value_minor: type: integer description: Value in the currency's MINOR unit — cents, not dollars. currency: type: string description: ISO 4217 code for `value_minor`. required: - title - pipeline_id - stage_id example: title: Renewal pipeline_id: pl_… stage_id: st_… value_minor: 10000 currency: USD responses: '200': description: '{ id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Deal' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_deals_delete summary: Delete one deal from the tenant CRM. The response carries the record as it was description: 'Money is in minor units: `value_minor: 10000` with `currency: "USD"` is $100.00, not $10,000. Returns: { id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: deal_id in: path required: true schema: type: string responses: '200': description: '{ id, title, pipeline_id, stage_id, status, value_minor, currency, contact_id, company_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Deal' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/activities: get: operationId: commsharbor_crm_activities_list summary: List activities in the tenant CRM — something that happened with a contact… description: 'Returns: { items[{id,activity_type,note,deal_id,contact_id,company_id,created_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. - name: q in: query required: false schema: type: string description: Free-text search over the record's main fields. - name: deal_id in: query required: false schema: type: string description: Restrict to one deal. - name: contact_id in: query required: false schema: type: string description: Restrict to one CRM contact. - name: company_id in: query required: false schema: type: string description: Restrict to one CRM company. - name: activity_type in: query required: false schema: type: string description: Restrict to one activity type, e.g. `note` or `call`. responses: '200': description: '{ items[{id,activity_type,note,deal_id,contact_id,company_id,created_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageActivity' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_activities_create summary: Create a activity in the tenant CRM description: 'Returns: { id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: note: type: string description: The text of the activity. activity_type: type: string description: What kind it was, e.g. `note`, `call`, `meeting`. deal_id: type: string description: Deal it refers to. contact_id: type: string description: Contact it refers to. company_id: type: string description: Company it refers to. required: - note example: activity_type: note note: Followed up deal_id: de_… responses: '200': description: '{ id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Activity' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/activities/{activity_id}: get: operationId: commsharbor_crm_activities_get summary: Read one activity from the tenant CRM description: 'Returns: { id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: activity_id in: path required: true schema: type: string responses: '200': description: '{ id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Activity' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_activities_update summary: Update one activity in the tenant CRM. Only the fields you send change description: 'Returns: { id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: activity_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: note: type: string description: The text of the activity. activity_type: type: string description: What kind it was, e.g. `note`, `call`, `meeting`. deal_id: type: string description: Deal it refers to. contact_id: type: string description: Contact it refers to. company_id: type: string description: Company it refers to. required: - note example: activity_type: note note: Followed up deal_id: de_… responses: '200': description: '{ id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Activity' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_activities_delete summary: Delete one activity from the tenant CRM. description: 'Returns: { id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: activity_id in: path required: true schema: type: string responses: '200': description: '{ id, activity_type, note, deal_id, contact_id, company_id, created_at, url }' content: application/json: schema: $ref: '#/components/schemas/Activity' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/tasks: get: operationId: commsharbor_crm_tasks_list summary: List tasks in the tenant CRM — work someone still has to do in the tenant CRM description: 'Returns: { items[{id,title,status,due_at,assignee_user_id,deal_id,contact_id,created_at,updated_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. - name: q in: query required: false schema: type: string description: Free-text search over the record's main fields. - name: status in: query required: false schema: type: string description: Restrict to one status value. - name: deal_id in: query required: false schema: type: string description: Restrict to one deal. - name: contact_id in: query required: false schema: type: string description: Restrict to one CRM contact. - name: assignee_user_id in: query required: false schema: type: string description: Restrict to the member the work is assigned to. responses: '200': description: '{ items[{id,title,status,due_at,assignee_user_id,deal_id,contact_id,created_at,updated_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageTask' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_tasks_create summary: Create a task in the tenant CRM description: 'Returns: { id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: title: type: string description: What has to be done. status: type: string description: Current state. due_at: type: string description: When it is due, ISO-8601. assignee_user_id: type: string description: Member responsible for it. deal_id: type: string description: Deal the task belongs to. contact_id: type: string description: Contact the task belongs to. required: - title example: title: Follow up status: open deal_id: de_… responses: '200': description: '{ id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Task' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/tasks/{task_id}: get: operationId: commsharbor_crm_tasks_get summary: Read one task from the tenant CRM description: 'Returns: { id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: task_id in: path required: true schema: type: string responses: '200': description: '{ id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Task' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_tasks_update summary: Update one task in the tenant CRM. Only the fields you send change description: 'Returns: { id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: task_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: title: type: string description: What has to be done. status: type: string description: Current state. due_at: type: string description: When it is due, ISO-8601. assignee_user_id: type: string description: Member responsible for it. deal_id: type: string description: Deal the task belongs to. contact_id: type: string description: Contact the task belongs to. required: - title example: title: Follow up status: open deal_id: de_… responses: '200': description: '{ id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Task' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_tasks_delete summary: Delete one task from the tenant CRM. The response carries the record as it was description: 'Returns: { id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: task_id in: path required: true schema: type: string responses: '200': description: '{ id, title, status, due_at, assignee_user_id, deal_id, contact_id, created_at, updated_at, url }' content: application/json: schema: $ref: '#/components/schemas/Task' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/pipelines/{pipeline_id}/stages: get: operationId: commsharbor_crm_stages_list summary: List stages in the tenant CRM — one step of a pipeline, addressed under the… description: 'Returns: { items[{id,pipeline_id,name,position,probability,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,pipeline_id,name,position,probability,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageStage' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_crm_stages_create summary: Create a stage in the tenant CRM description: 'Returns: { id, pipeline_id, name, position, probability, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Stage name, e.g. `Proposal`. position: type: integer description: Order within the pipeline, lowest first. probability: type: integer description: Chance of winning at this stage, 0 to 100. required: - name example: name: Proposal position: 2 probability: 60 responses: '200': description: '{ id, pipeline_id, name, position, probability, url }' content: application/json: schema: $ref: '#/components/schemas/Stage' '400': description: A required field is missing, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/pipelines/{pipeline_id}/stages/{stage_id}: get: operationId: commsharbor_crm_stages_get summary: Read one stage from the tenant CRM description: 'Returns: { id, pipeline_id, name, position, probability, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string - name: stage_id in: path required: true schema: type: string responses: '200': description: '{ id, pipeline_id, name, position, probability, url }' content: application/json: schema: $ref: '#/components/schemas/Stage' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_crm_stages_update summary: Update one stage in the tenant CRM. Only the fields you send change description: 'Returns: { id, pipeline_id, name, position, probability, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string - name: stage_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Stage name, e.g. `Proposal`. position: type: integer description: Order within the pipeline, lowest first. probability: type: integer description: Chance of winning at this stage, 0 to 100. required: - name example: name: Proposal position: 2 probability: 60 responses: '200': description: '{ id, pipeline_id, name, position, probability, url }' content: application/json: schema: $ref: '#/components/schemas/Stage' '400': description: A field is invalid, or a referenced record does not exist here. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_crm_stages_delete summary: Delete one stage from the tenant CRM. The response carries the record as it was description: 'Returns: { id, pipeline_id, name, position, probability, url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: pipeline_id in: path required: true schema: type: string - name: stage_id in: path required: true schema: type: string responses: '200': description: '{ id, pipeline_id, name, position, probability, url }' content: application/json: schema: $ref: '#/components/schemas/Stage' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/contacts/{contact_id}/marketing: get: operationId: commsharbor_contact_marketing_get summary: Read consent and marketing preference for one CRM contact description: 'Being in the CRM is not permission to email. This resource is where permission actually lives. Returns: { marketing_enabled, basis, source, captured_at, unsubscribed_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string responses: '200': description: '{ marketing_enabled, basis, source, captured_at, unsubscribed_at }' content: application/json: schema: $ref: '#/components/schemas/Marketing' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations put: operationId: commsharbor_contact_marketing_put summary: Record permission-based marketing consent for one contact description: 'This NEVER restores a previous unsubscribe. If the contact opted out, they stay out and `marketing_enabled` remains false — recording consent after the fact does not undo their decision. Returns: { marketing_enabled, basis, source, captured_at, unsubscribed_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: basis: type: string description: Why sending is allowed. source: type: string description: Where the consent came from, e.g. the signup form that captured it. captured_at: type: string description: When consent was captured, ISO-8601. required: - basis - source - captured_at example: basis: explicit source: website signup form captured_at: '2026-08-29T12:00:00.000Z' responses: '200': description: '{ marketing_enabled, basis, source, captured_at, unsubscribed_at }' content: application/json: schema: $ref: '#/components/schemas/Marketing' '400': description: Missing basis, source or capture time, or an unknown basis. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/crm/contacts/{contact_id}/preference-token: post: operationId: commsharbor_preference_token_create summary: Create a signed preference and one-click unsubscribe capability for a contact description: 'The capability is scoped to one organization and one contact, it expires, and it embeds no email address. Put `marketing_headers` in the message and the unsubscribe works without a login. Returns: { api_url, preferences_url, unsubscribe_url, marketing_headers }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ api_url, preferences_url, unsubscribe_url, marketing_headers }' content: application/json: schema: $ref: '#/components/schemas/PreferenceToken' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/domains/{domain_id}/marketing-smoke: post: operationId: commsharbor_marketing_smoke summary: Queue one controlled permission-based marketing message to the server-side… description: 'Like the transactional smoke, the destination is a server-side secret. `contact_id` must be the CRM contact that matches it — you cannot point this at an arbitrary person. Returns: { delivery{id,status,domain_id,template_id,template_version,ses_message_id,created_at}, capacity{remaining,global_remaining,hard_cap}, dispatch{queued,job_id}, replayed }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: domain_id in: path required: true schema: type: string - name: Idempotency-Key in: header required: true schema: type: string description: Unique key for this controlled send. Replaying the same key returns the same result and produces NO second Queue message; a different payload under the same key conflicts. requestBody: required: true content: application/json: schema: type: object properties: contact_id: type: string description: CRM contact matching the server-side QA recipient. required: - contact_id example: contact_id: ct_controlled responses: '200': description: '{ delivery{id,status,domain_id,template_id,template_version,ses_message_id,created_at}, capacity{remaining,global_remaining,hard_cap}, dispatch{queued,job_id}, replayed }' content: application/json: schema: $ref: '#/components/schemas/Send' '400': description: Missing contact. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The contact does not match the controlled recipient, or the domain is not active. tags: - Organizations /api/organizations/{organization_id}/contact-imports: get: operationId: commsharbor_contact_imports summary: List the contact imports of the organization description: 'Returns: { items[{id,status,rows,accepted,rejected,basis,file_id,created_at}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,status,rows,accepted,rejected,basis,file_id,created_at}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageContactImport' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_contact_import_preview summary: Upload a consent-declared CSV and get back a safe preview before anything is… description: 'Nothing is created by this call. The consent declaration is mandatory: an import that cannot say why these people may be emailed is an import that does not happen. Returns: { import{id,status,rows,accepted,rejected,basis,file_id,created_at}, preview, errors[{row,code,message}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: csv: type: string description: The CSV itself, up to 5 MB and 10,000 rows. mapping: type: object description: 'Which CSV column feeds which contact field, e.g. `{ "email": "Email" }`.' basis: type: string description: Why sending is allowed. source: type: string description: Where the consent came from, e.g. the signup form that captured it. captured_at: type: string description: When consent was captured, ISO-8601. required: - csv - mapping - basis - source - captured_at example: csv: Email,First name\nalice@example.com,Alice mapping: email: Email first_name: First name basis: explicit source: website signup form captured_at: '2026-08-29T12:00:00.000Z' responses: '200': description: '{ import{id,status,rows,accepted,rejected,basis,file_id,created_at}, preview, errors[{row,code,message}] }' content: application/json: schema: type: object properties: import: allOf: - $ref: '#/components/schemas/ContactImport' description: The import in `preview` state. preview: type: array items: type: object description: The first parsed rows, so you can check the mapping before confirming. errors: type: array items: $ref: '#/components/schemas/ImportError' description: Rows that would be rejected, addressed by line number. required: - import - preview - errors '400': description: CSV too large, unparseable, or missing the consent declaration. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '413': description: Above the size limit. tags: - Organizations /api/organizations/{organization_id}/contact-imports/{import_id}: get: operationId: commsharbor_contact_import_get summary: Read one durable contact import and its current counts description: 'Returns: { id, status, rows, accepted, rejected, basis, file_id, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: import_id in: path required: true schema: type: string responses: '200': description: '{ id, status, rows, accepted, rejected, basis, file_id, created_at }' content: application/json: schema: $ref: '#/components/schemas/ContactImport' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/contact-imports/{import_id}/confirm: post: operationId: commsharbor_contact_import_confirm summary: Confirm a previewed import and enqueue it, exactly once description: 'The idempotency key is mandatory here. Reusing it returns the same import and never creates a second Queue message — which is what keeps a retry from importing everyone twice. Returns: { id, status, rows, accepted, rejected, basis, file_id, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: import_id in: path required: true schema: type: string - name: Idempotency-Key in: header required: true schema: type: string description: Unique key for this import confirmation. Replaying the same key returns the same result and produces NO second Queue message; a different payload under the same key conflicts. requestBody: required: true content: application/json: example: {} responses: '200': description: '{ id, status, rows, accepted, rejected, basis, file_id, created_at }' content: application/json: schema: $ref: '#/components/schemas/ContactImport' '400': description: Missing Idempotency-Key. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The same key was used with a different payload, or the import was already confirmed. tags: - Organizations /api/organizations/{organization_id}/contact-imports/{import_id}/errors: get: operationId: commsharbor_contact_import_errors summary: List the row-numbered errors of one import, so the source file can be fixed description: 'Returns: { items[{row,code,message}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: import_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{row,code,message}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageImportError' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/contact-files/{file_id}: get: operationId: commsharbor_contact_file_get summary: Download a tenant-owned CSV before its seven-day expiry description: 'Files expire seven days after creation. Durable audit and row-level results survive the file. Returns: `text/csv` as an attachment, with the tenant''s file name.' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: file_id in: path required: true schema: type: string responses: '200': description: '`text/csv` as an attachment, with the tenant''s file name.' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '410': description: The file expired. Its audit record still exists. tags: - Organizations /api/organizations/{organization_id}/contacts/export: post: operationId: commsharbor_contacts_export summary: Create a CSV export of the organization's contacts, retained for seven days description: 'Returns: { export{id,status,rows,accepted,rejected,basis,file_id,created_at}, download_url }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ export{id,status,rows,accepted,rejected,basis,file_id,created_at}, download_url }' content: application/json: schema: type: object properties: export: allOf: - $ref: '#/components/schemas/ContactImport' description: The export record, with its file and expiry. download_url: type: string description: Where to fetch the CSV while it lasts. required: - export - download_url '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/suppressions: get: operationId: commsharbor_suppressions summary: List the organization's suppressions, without exposing email hashes description: 'Global, organization and SES tenant suppressions are all checked BEFORE quota and before any queue work. Returns: { items[{id,scope,reason,contact_id,created_at}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,scope,reason,contact_id,created_at}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageSuppression' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_suppression_create summary: Suppress one recipient inside the active organization description: 'Returns: { suppressed }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: Address to suppress. contact_id: type: string description: CRM contact it corresponds to, when you know it. required: - email example: email: alice@example.com contact_id: ct_example responses: '200': description: '{ suppressed }' content: application/json: schema: type: object properties: suppressed: type: boolean description: Always true once the address is suppressed. required: - suppressed '400': description: Missing or malformed email. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/audiences: get: operationId: commsharbor_audiences summary: List static audiences and saved segments description: 'Returns: { items[{id,name,kind,filter,member_count,created_at}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,name,kind,filter,member_count,created_at}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageAudience' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_audience_create summary: Create a static audience or a saved segment with an allowlisted filter description: 'Only allowlisted filter fields are accepted — a saved segment cannot be turned into an arbitrary query over the CRM. Returns: { id, name, kind, filter, member_count, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Audience name. kind: type: string description: How membership is decided. filter: type: object description: The saved filter, for `saved_segment`. Allowlisted fields only. required: - name - kind example: name: Newsletter kind: static responses: '200': description: '{ id, name, kind, filter, member_count, created_at }' content: application/json: schema: $ref: '#/components/schemas/Audience' '400': description: Missing name or kind, or a filter field outside the allowlist. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/audiences/{audience_id}: get: operationId: commsharbor_audience_get summary: Read one audience of this organization description: 'Returns: { id, name, kind, filter, member_count, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: audience_id in: path required: true schema: type: string responses: '200': description: '{ id, name, kind, filter, member_count, created_at }' content: application/json: schema: $ref: '#/components/schemas/Audience' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_audience_update summary: Rename an audience or change its saved filter description: 'Returns: { id, name, kind, filter, member_count, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: audience_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: New audience name. filter: type: object description: New saved filter. Allowlisted fields only. example: name: Customers filter: consent_basis: explicit responses: '200': description: '{ id, name, kind, filter, member_count, created_at }' content: application/json: schema: $ref: '#/components/schemas/Audience' '400': description: A filter field outside the allowlist. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_audience_delete summary: Delete an audience and its memberships. Contacts themselves are untouched description: 'Returns: { id, name, kind, filter, member_count, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: audience_id in: path required: true schema: type: string responses: '200': description: '{ id, name, kind, filter, member_count, created_at }' content: application/json: schema: $ref: '#/components/schemas/Audience' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/audiences/{audience_id}/members: get: operationId: commsharbor_audience_members summary: List the contacts currently in one audience description: 'Returns: { items[{id,email,first_name,last_name,company_id,created_at,updated_at,url}], next_cursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: audience_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,email,first_name,last_name,company_id,created_at,updated_at,url}], next_cursor }' content: application/json: schema: $ref: '#/components/schemas/PageContact' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_audience_member_add summary: Add a CRM contact to a static audience description: 'Only for `static` audiences: a saved segment''s membership comes from its filter, not from this route. Returns: { membership }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: audience_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: contact_id: type: string description: CRM contact to add. required: - contact_id example: contact_id: ct_example responses: '200': description: '{ membership }' content: application/json: schema: type: object properties: membership: type: object description: The audience membership that was created. required: - membership '400': description: Missing contact. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The audience is a saved segment and has no manual membership. tags: - Organizations /api/organizations/{organization_id}/audiences/{audience_id}/members/{contact_id}: delete: operationId: commsharbor_audience_member_remove summary: Remove a contact from a static audience description: 'Returns: { membership }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: audience_id in: path required: true schema: type: string - name: contact_id in: path required: true schema: type: string responses: '200': description: '{ membership }' content: application/json: schema: type: object properties: membership: type: object description: The membership as it was before removal. required: - membership '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/templates: get: operationId: commsharbor_templates summary: List the versioned email templates of the organization description: 'Returns: { items[{id,name,message_type,subject,variables,blocks,status,latest_version,updated_at}], nextCursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,name,message_type,subject,variables,blocks,status,latest_version,updated_at}], nextCursor }' content: application/json: schema: $ref: '#/components/schemas/PageCamelTemplate' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_template_create summary: Create a template draft from canonical blocks description: 'Creating never publishes. Nothing can send this template until `publish` freezes a version. Returns: { id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Template name. message_type: type: string description: What the template may be used for. A marketing campaign will not accept a transactional template. subject: type: string description: Subject line, with `{{variable}}` placeholders. variables: type: array items: type: object description: 'Typed variables the template declares: `name`, `type` and whether it is `required`.' blocks: type: array items: type: object description: Canonical content blocks. This is the stored form — HTML is converted into it, never kept raw. required: - name - message_type - subject example: name: Welcome message_type: transactional subject: Welcome, {{name}} variables: - name: name type: string required: true blocks: - type: heading text: Hello {{name}} level: 1 align: left responses: '200': description: '{ id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' content: application/json: schema: $ref: '#/components/schemas/Template' '400': description: Missing name, subject or message type, or an unknown block type. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/templates/{template_id}: get: operationId: commsharbor_template_get summary: Read one template draft owned by this organization description: 'Returns: { id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: template_id in: path required: true schema: type: string responses: '200': description: '{ id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' content: application/json: schema: $ref: '#/components/schemas/Template' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_template_update summary: Update a template as a new draft, leaving published versions untouched description: 'Editing a draft can never change what a past delivery rendered: published versions are immutable. Returns: { id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: template_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Template name. message_type: type: string description: What the template may be used for. A marketing campaign will not accept a transactional template. subject: type: string description: Subject line, with `{{variable}}` placeholders. variables: type: array items: type: object description: 'Typed variables the template declares: `name`, `type` and whether it is `required`.' blocks: type: array items: type: object description: Canonical content blocks. This is the stored form — HTML is converted into it, never kept raw. required: - name - message_type - subject example: name: Welcome message_type: transactional subject: Welcome, {{name}} variables: - name: name type: string required: true blocks: - type: heading text: Hello {{name}} level: 1 align: left responses: '200': description: '{ id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' content: application/json: schema: $ref: '#/components/schemas/Template' '400': description: Invalid field or unknown block type. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_template_archive summary: Archive a template without deleting its published versions description: 'Deliveries that referenced a version must keep resolving to it, so archiving hides the draft and keeps the history. Returns: { id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: template_id in: path required: true schema: type: string responses: '200': description: '{ id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' content: application/json: schema: $ref: '#/components/schemas/Template' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/templates/{template_id}/publish: post: operationId: commsharbor_template_publish summary: Publish an immutable, content-hashed version of the draft description: 'The version''s identity is the hash of its compiled content — publishing identical content does not create a second version. Returns: { version, content_hash, subject, published_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: template_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ version, content_hash, subject, published_at }' content: application/json: schema: $ref: '#/components/schemas/TemplateVersion' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The draft does not compile, or nothing changed since the last version. tags: - Organizations /api/organizations/{organization_id}/templates/{template_id}/versions: get: operationId: commsharbor_template_versions summary: List the immutable published versions of a template description: 'Returns: { items[{version,content_hash,subject,published_at}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: template_id in: path required: true schema: type: string responses: '200': description: '{ items[{version,content_hash,subject,published_at}] }' content: application/json: schema: $ref: '#/components/schemas/ListaTemplateVersion' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/templates/preview: post: operationId: commsharbor_template_preview summary: Compile and safely render a draft, without saving or publishing anything description: '`warnings` tells you what was stripped and what would not render — read it before publishing rather than after sending. Returns: { subject, html, text, warnings }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Template name. message_type: type: string description: What the template may be used for. A marketing campaign will not accept a transactional template. subject: type: string description: Subject line, with `{{variable}}` placeholders. variables: type: array items: type: object description: 'Typed variables the template declares: `name`, `type` and whether it is `required`.' blocks: type: array items: type: object description: Canonical content blocks. This is the stored form — HTML is converted into it, never kept raw. values: type: object description: Values to substitute into the declared variables for this preview. required: - name - message_type - subject example: name: Welcome message_type: transactional subject: Welcome, {{name}} variables: - name: name type: string required: true blocks: - type: heading text: Hello {{name}} level: 1 align: left values: name: Ada responses: '200': description: '{ subject, html, text, warnings }' content: application/json: schema: $ref: '#/components/schemas/TemplatePreview' '400': description: The draft does not compile. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/templates/{template_id}/export: get: operationId: commsharbor_template_export summary: Export the latest published version as HTML, plain text and MJML description: 'Returns: { template_id, version, subject, html, text, mjml, content_hash }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: template_id in: path required: true schema: type: string responses: '200': description: '{ template_id, version, subject, html, text, mjml, content_hash }' content: application/json: schema: $ref: '#/components/schemas/TemplateExport' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The template has never been published. tags: - Organizations /api/organizations/{organization_id}/templates/import-html: post: operationId: commsharbor_template_import_html summary: Import a conservative HTML subset and convert it into canonical blocks description: 'Active content, forms and unsafe URLs are rejected, not sanitised-and-kept. What survives is stored as blocks, so an imported template behaves exactly like an authored one. Returns: { id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Template name. message_type: type: string description: What it may be used for. subject: type: string description: Subject line. html: type: string description: The HTML to import. Scripts, forms, event handlers and unsafe URLs are rejected. required: - name - message_type - subject - html example: name: Imported message_type: transactional subject: Imported html:
Content
responses: '200': description: '{ id, name, message_type, subject, variables, blocks, status, latest_version, updated_at }' content: application/json: schema: $ref: '#/components/schemas/Template' '400': description: The HTML carries active content, a form, or a URL scheme that is not allowed. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/messages: post: operationId: commsharbor_message_send summary: Send one transactional message with the organization in the path instead of the… description: 'Same behaviour and same idempotency as `POST /api/messages`; it exists for clients that cannot set `X-Organization-Id`. Prefer the canonical route. Returns: { delivery{id,status,domain_id,template_id,template_version,ses_message_id,created_at}, capacity{remaining,global_remaining,hard_cap}, dispatch{queued,job_id}, replayed }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: Idempotency-Key in: header required: true schema: type: string description: Unique key for this logical message. An equivalent replay returns the SAME delivery and produces no second send; the same key with a different payload conflicts before quota or Queue work. requestBody: required: true content: application/json: schema: type: object properties: domain_id: type: string description: Active sending domain to send from. to: type: string description: Recipient address. It is not echoed back in the response. template_id: type: string description: Template to render. template_version: type: integer description: Published version to use. Without it, the latest published version is used — pin it when the content must not drift. variables: type: object description: Values for the template's declared variables. reply_to: type: string description: Reply-To address, when it differs from the sending domain. required: - domain_id - to - template_id example: domain_id: dom_… to: recipient@example.com template_id: tpl_… variables: name: Ada responses: '200': description: '{ delivery{id,status,domain_id,template_id,template_version,ses_message_id,created_at}, capacity{remaining,global_remaining,hard_cap}, dispatch{queued,job_id}, replayed }' content: application/json: schema: $ref: '#/components/schemas/Send' '400': description: Missing Idempotency-Key, unknown template, or a required variable with no value. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: Key reused with a different payload, or the recipient is suppressed. '429': description: No capacity left. tags: - Organizations /api/organizations/{organization_id}/campaigns: get: operationId: commsharbor_campaigns summary: List the campaigns of the organization description: 'Returns: { items[{id,name,status,domain_id,audience_id,template_id,template_version,scheduled_for,frozen_recipients,created_at}], nextCursor }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page. Do not build or parse it. - name: limit in: query required: false schema: type: integer default: 50 description: Page size, from 1 to 100. responses: '200': description: '{ items[{id,name,status,domain_id,audience_id,template_id,template_version,scheduled_for,frozen_recipients,created_at}], nextCursor }' content: application/json: schema: $ref: '#/components/schemas/PageCamelCampaign' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_campaign_create summary: Create a campaign draft from an active domain, an audience and a published… description: 'All three must already exist and be usable: a draft cannot be created against an unverified domain or an unpublished template. Returns: { id, name, status, domain_id, audience_id, template_id, template_version, scheduled_for, frozen_recipients, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: Campaign name. domain_id: type: string description: Active sending domain to send from. audience_id: type: string description: Audience the campaign goes to. template_id: type: string description: Published MARKETING template. A transactional template is refused here. template_version: type: integer description: Published version to freeze into the campaign. required: - name - domain_id - audience_id - template_id example: name: August update domain_id: dom_… audience_id: aud_… template_id: tpl_… template_version: 1 responses: '200': description: '{ id, name, status, domain_id, audience_id, template_id, template_version, scheduled_for, frozen_recipients, created_at }' content: application/json: schema: $ref: '#/components/schemas/Campaign' '400': description: Missing field. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The domain is not active, the template is not published, or it is transactional. tags: - Organizations /api/organizations/{organization_id}/campaigns/{campaign_id}: get: operationId: commsharbor_campaign_get summary: Read one campaign, without any recipient data description: 'Returns: { id, name, status, domain_id, audience_id, template_id, template_version, scheduled_for, frozen_recipients, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: campaign_id in: path required: true schema: type: string responses: '200': description: '{ id, name, status, domain_id, audience_id, template_id, template_version, scheduled_for, frozen_recipients, created_at }' content: application/json: schema: $ref: '#/components/schemas/Campaign' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_campaign_update summary: Edit a draft, or pause, resume or cancel a campaign that already launched description: 'Content can only change while the campaign is a draft. After launch this route moves state — the frozen recipient set and the frozen template version do not change. Returns: { id, name, status, domain_id, audience_id, template_id, template_version, scheduled_for, frozen_recipients, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: campaign_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: New campaign name. domain_id: type: string description: New sending domain, while still a draft. audience_id: type: string description: New audience, while still a draft. template_id: type: string description: New published marketing template, while still a draft. template_version: type: integer description: Published version to freeze into the campaign. status: type: string description: Move a launched campaign. example: name: August update status: paused responses: '200': description: '{ id, name, status, domain_id, audience_id, template_id, template_version, scheduled_for, frozen_recipients, created_at }' content: application/json: schema: $ref: '#/components/schemas/Campaign' '400': description: Unknown status. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: Editing content after launch, or resuming while marketing is paused for the organization. tags: - Organizations /api/organizations/{organization_id}/campaigns/{campaign_id}/launch: post: operationId: commsharbor_campaign_launch summary: Freeze the eligible recipients and launch — or schedule — the campaign, exactly… description: 'The freeze happens once and never again: consent, suppressions and audience membership are evaluated at this moment, and the resulting set is what gets sent. Replaying the same Idempotency-Key returns the same campaign and produces no second dispatch. Returns: { campaign{id,name,status,domain_id,audience_id,template_id,template_version,scheduled_for,frozen_recipients,created_at}, dispatch_job_id, replayed }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: campaign_id in: path required: true schema: type: string - name: Idempotency-Key in: header required: true schema: type: string description: Stable key for this launch. A replay returns the same campaign and never freezes a second recipient set. requestBody: required: true content: application/json: schema: type: object properties: scheduled_for: type: string description: When to send, ISO-8601 with offset, interpreted in the organization's timezone. Omit to send now. example: scheduled_for: '2026-08-30T14:00:00-03:00' responses: '200': description: '{ campaign{id,name,status,domain_id,audience_id,template_id,template_version,scheduled_for,frozen_recipients,created_at}, dispatch_job_id, replayed }' content: application/json: schema: type: object properties: campaign: allOf: - $ref: '#/components/schemas/Campaign' description: The campaign, now scheduled or sending, with the frozen recipient count. dispatch_job_id: type: string description: The dispatch job that will walk the frozen set. nullable: true replayed: type: boolean description: True when this was a replay and nothing new was frozen or queued. required: - campaign - dispatch_job_id - replayed '400': description: Missing Idempotency-Key or an unparseable schedule. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: Already launched, marketing paused for the organization, or no eligible recipients. '429': description: Not enough capacity for the frozen recipient set. tags: - Organizations /api/organizations/{organization_id}/campaigns/{campaign_id}/report: get: operationId: commsharbor_campaign_report summary: 'Reconcile a campaign: delivery counts and normalized feedback counts' description: 'Counts only. Which specific person opened what is not something this API answers. Returns: { campaign{id,name,status,domain_id,audience_id,template_id,template_version,scheduled_for,frozen_recipients,created_at}, deliveries, events }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: campaign_id in: path required: true schema: type: string responses: '200': description: '{ campaign{id,name,status,domain_id,audience_id,template_id,template_version,scheduled_for,frozen_recipients,created_at}, deliveries, events }' content: application/json: schema: $ref: '#/components/schemas/CampaignReport' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/messaging-settings: get: operationId: commsharbor_messaging_settings summary: Read the organization's timezone and whether marketing is currently paused description: 'Returns: { timezone, marketing_state, paused_reason }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ timezone, marketing_state, paused_reason }' content: application/json: schema: $ref: '#/components/schemas/MessagingSettings' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations patch: operationId: commsharbor_messaging_settings_update summary: Change the scheduling timezone, or pause and safely resume marketing description: 'Pausing is always allowed. Resuming is not: if the pause came from reputation or SES tenant risk, a healthy observation has to exist first. Returns: { timezone, marketing_state, paused_reason }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: timezone: type: string description: IANA timezone used to interpret campaign schedules, e.g. `America/Sao_Paulo`. marketing_state: type: string description: Whether marketing may go out. example: timezone: America/Sao_Paulo marketing_state: active responses: '200': description: '{ timezone, marketing_state, paused_reason }' content: application/json: schema: $ref: '#/components/schemas/MessagingSettings' '400': description: Unknown timezone or state. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: Resuming while the risk that caused the pause is still observed. tags: - Organizations /api/organizations/{organization_id}/deliverability: get: operationId: commsharbor_deliverability summary: Read delivery, backlog, suppression, reputation and dead-letter aggregates in… description: 'This is the operator''s single view of whether sending is healthy. It carries no recipient PII. Returns: { settings{timezone,marketing_state,paused_reason}, deliveries, backlog, dead_letters, suppressions, domains }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ settings{timezone,marketing_state,paused_reason}, deliveries, backlog, dead_letters, suppressions, domains }' content: application/json: schema: type: object properties: settings: allOf: - $ref: '#/components/schemas/MessagingSettings' description: Timezone and marketing state. deliveries: type: object description: Delivery counts by state. backlog: type: object description: What is still queued and how old the oldest item is. dead_letters: type: object description: Dead-letter counts by origin. suppressions: type: object description: Suppression counts by scope. domains: type: array items: type: object description: Per-domain reputation and state. required: - settings - deliveries - backlog - dead_letters - suppressions - domains '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/domains/{domain_id}/report: get: operationId: commsharbor_domain_report summary: Read delivery and feedback aggregates for one sending domain description: 'Returns: { domain{id,domain,status,dkim_tokens,dmarc,mail_from,last_observed_at,created_at}, deliveries, events }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: domain_id in: path required: true schema: type: string responses: '200': description: '{ domain{id,domain,status,dkim_tokens,dmarc,mail_from,last_observed_at,created_at}, deliveries, events }' content: application/json: schema: type: object properties: domain: allOf: - $ref: '#/components/schemas/Domain' description: The domain being reported on. deliveries: type: object description: Delivery counts by state for this domain. events: type: object description: Normalized feedback counts by event type. required: - domain - deliveries - events '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/webhooks: get: operationId: commsharbor_webhooks summary: List the webhook endpoints of the organization, without their signing secrets description: 'Returns: { items[{id,url,event_types,status,created_at,secret?}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ items[{id,url,event_types,status,created_at,secret?}] }' content: application/json: schema: $ref: '#/components/schemas/ListaWebhook' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_webhook_create summary: Create an HTTPS webhook and reveal its signing secret exactly once description: 'Deliveries are signed with timestamped HMAC-SHA256 and retried with exponential backoff; what still fails lands in the tenant-scoped dead-letter queue. Store the secret now — it is never shown again. Returns: { id, url, event_types, status, created_at, secret? }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: url: type: string description: Public HTTPS destination. Plain HTTP is refused. event_types: type: array items: type: string description: Which events to deliver, e.g. `Delivery`, `Bounce`, `Complaint`. required: - url - event_types example: url: https://receiver.example.com/events event_types: - Delivery - Bounce responses: '200': description: '{ id, url, event_types, status, created_at, secret? }' content: application/json: schema: $ref: '#/components/schemas/Webhook' '400': description: Non-HTTPS URL or an unknown event type. '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/webhooks/{webhook_id}: get: operationId: commsharbor_webhook_get summary: Read one webhook, without its signing secret description: 'Returns: { id, url, event_types, status, created_at, secret? }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: webhook_id in: path required: true schema: type: string responses: '200': description: '{ id, url, event_types, status, created_at, secret? }' content: application/json: schema: $ref: '#/components/schemas/Webhook' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations delete: operationId: commsharbor_webhook_disable summary: Disable a webhook without deleting the evidence of what it already delivered description: 'Returns: { id, url, event_types, status, created_at, secret? }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: webhook_id in: path required: true schema: type: string responses: '200': description: '{ id, url, event_types, status, created_at, secret? }' content: application/json: schema: $ref: '#/components/schemas/Webhook' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/webhooks/{webhook_id}/deliveries: get: operationId: commsharbor_webhook_deliveries summary: List the signed attempts made to one webhook and their retry state description: 'Returns: { items }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: webhook_id in: path required: true schema: type: string responses: '200': description: '{ items }' content: application/json: schema: type: object properties: items: type: array items: type: object description: 'One record per attempt: status, response code, when it ran and when it will retry.' required: - items '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/dead-letters: get: operationId: commsharbor_dead_letters summary: Inspect the tenant's dead letters by opaque record ID description: 'What ended up here after every retry. Records are addressed by opaque ID and carry no recipient PII. Returns: { items }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ items }' content: application/json: schema: type: object properties: items: type: array items: type: object description: 'One record per dead letter: origin, reason, when it failed.' required: - items '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/dead-letters/{record_id}/replay: post: operationId: commsharbor_dead_letter_replay summary: Replay one campaign or webhook dead letter, by name description: 'One record at a time, addressed explicitly. There is no "replay everything": a bulk replay of an unknown set is how a bad hour becomes a bad day. Returns: { dead_letter }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: record_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ dead_letter }' content: application/json: schema: type: object properties: dead_letter: type: object description: The record with its new replay state. required: - dead_letter '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The record was already replayed or is not replayable. tags: - Organizations /api/organizations/{organization_id}/billing: get: operationId: commsharbor_billing summary: Read this organization's entitlement, quota allocation and expiration warning description: 'Read this before a bulk send: `state` says how many sends remain and `warning` says when the entitlement runs out. Returns: { catalog{version,trial,pass,topup,checkout_live,global_recipient_hard_cap,auto_renew}, state, warning, payment{provider,mode,network,chain_id,pay_to,homolog,dev,dev_gate,facilitator,asset,asset_address,faucet,wallets} }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ catalog{version,trial,pass,topup,checkout_live,global_recipient_hard_cap,auto_renew}, state, warning, payment{provider,mode,network,chain_id,pay_to,homolog,dev,dev_gate,facilitator,asset,asset_address,faucet,wallets} }' content: application/json: schema: $ref: '#/components/schemas/BillingTenant' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/billing/purchases/{action}: post: operationId: commsharbor_billing_purchase summary: Request a pass or a top-up. description: 'The response is a standard **402 with `accepts[]`** unless `catalog.checkout_live` is true — nobody is charged by accident. The Idempotency-Key makes a retry safe: the same logical purchase never grants entitlement twice. Returns: { replayed, state }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: action in: path required: true schema: type: string - name: Idempotency-Key in: header required: true schema: type: string description: Stable key for this logical purchase. A replay returns the same state and grants nothing extra. - name: X-PAYMENT in: header required: false schema: type: string description: Standard x402 payment payload, when live checkout is explicitly enabled. requestBody: required: true content: application/json: example: {} responses: '200': description: '{ replayed, state }' content: application/json: schema: type: object properties: replayed: type: boolean description: True when this key had already been used and nothing new was granted. state: type: object description: 'The entitlement after the request: what is active, what remains, when it expires.' required: - replayed - state '400': description: Missing Idempotency-Key, or an action other than `pass`/`topup`. '401': description: No session, no API key, or the credential does not resolve to this organization. '402': description: Payment required — the body carries `accepts[]`. This is the normal answer while live checkout is disabled. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The same key was used for a different purchase. tags: - Organizations /api/organizations/{organization_id}/operations: get: operationId: commsharbor_operations summary: List the organization's current operational alerts description: 'Covers dead letters, backlog, worker and SNS failures, reputation, paused sending, quota and conservative SES/SNS cost capacity — never with recipient PII. Returns: { items }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ items }' content: application/json: schema: type: object properties: items: type: array items: type: object description: One entry per active alert, with its kind, severity and what to do about it. required: - items '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_operations_refresh summary: Re-observe alerts, quota and global capacity right now description: 'The GET reads what was last stored; this goes and looks again. Same distinction as domain verification. Returns: { alerts, billing, capacity }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ alerts, billing, capacity }' content: application/json: schema: $ref: '#/components/schemas/Operations' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/data-exports: get: operationId: commsharbor_data_exports summary: List the tenant data exports, which are retained for seven days description: 'Returns: { items[{id,status,expires_at,created_at}] }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ items[{id,status,expires_at,created_at}] }' content: application/json: schema: $ref: '#/components/schemas/ListaDataExport' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_data_export_create summary: Create a tenant-scoped JSON export of the organization's data description: 'Scoped to one organization by construction: an export can never contain another tenant''s data. Returns: { id, status, expires_at, created_at }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: example: {} responses: '200': description: '{ id, status, expires_at, created_at }' content: application/json: schema: $ref: '#/components/schemas/DataExport' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations /api/organizations/{organization_id}/data-exports/{export_id}: get: operationId: commsharbor_data_export_download summary: Download one tenant-scoped JSON export before it expires description: 'Returns: `application/json` as an attachment, with the organization''s data.' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string - name: export_id in: path required: true schema: type: string responses: '200': description: '`application/json` as an attachment, with the organization''s data.' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. '409': description: The export is not ready yet. '410': description: It expired after seven days. tags: - Organizations /api/organizations/{organization_id}/deletion-requests: get: operationId: commsharbor_deletion_request summary: Read the latest erasure request and where it is in the grace period description: '`status: "none"` means no erasure was ever requested — the resource always answers, so a client never has to interpret a 404 as "nothing scheduled". Returns: { id, status, scheduled_for, requested_by }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string responses: '200': description: '{ id, status, scheduled_for, requested_by }' content: application/json: schema: $ref: '#/components/schemas/Deletion' '401': description: No session, no API key, or the credential does not resolve to this organization. '403': description: The identity is valid but lacks the required role or scope for this operation. '404': description: The resource does not exist in this organization. Another tenant's ID answers the same 404 — the API never confirms that it exists elsewhere. tags: - Organizations post: operationId: commsharbor_deletion_schedule summary: Schedule the erasure of the organization, after a seven-day grace period description: 'The confirmation phrase must be exactly `delete ` — typing the id is the point, so nobody erases the wrong tenant by clicking. After erasure only pseudonymous financial and audit evidence remains. Returns: { id, status, scheduled_for, requested_by }' security: - bearerAuth: [] parameters: - name: organization_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object properties: confirmation: type: string description: The exact phrase `delete