{
  "openapi": "3.1.1",
  "info": {
    "title": "Messaging API",
    "description": "Multi-tenant SMS and email with the compliance layer built in: consent is checked on every send (an opt-out always wins), STOP/HELP/START are handled on inbound, tenant quiet hours hold non-exempt SMS, and every action lands in an exportable audit log.\n\n**Auth**: send the `X-Api-Key` header on every `/v1` request (the Event Grid webhooks and `/v1/health` are the only exceptions). **Key rotation** is create-then-revoke: mint a replacement with `POST /v1/keys`, move your caller over, then `DELETE /v1/keys/{id}` for the old key.\n\n**Idempotency**: `POST /v1/messages` and `POST /v1/emails` accept an `Idempotency-Key` header (up to 64 characters). The first request runs normally and its response is stored for 24 hours; a repeat with the same key and the same request body returns that original response verbatim and sends nothing. The same key with a different body is refused with `idempotency_conflict`.\n\n**Errors**: every error body carries a machine-readable `code` beside the human-readable `error` text. The catalog: `account_suspended`, `admin_only`, `body_or_template`, `content_refused`, `destination_limit_reached`, `destination_not_verified`, `field_too_long`, `idempotency_conflict`, `idempotency_in_flight`, `invalid_api_key`, `invalid_domain_name`, `invalid_inbound_policy`, `invalid_mode`, `invalid_phone_number`, `invalid_quiet_hours`, `invalid_request`, `invalid_verification_code`, `invalid_webhook_secret`, `keyword_conflict`, `missing_field`, `missing_recipients`, `no_sending_number`, `not_found`, `provider_error`, `quota_exceeded`, `sandbox_key_required`, `sandbox_unavailable`, `subject_required`, `tenant_mismatch`, `tenant_not_on_number`, `unknown_category`, `unknown_source`, `unknown_status`, `unknown_template`, `unknown_tenant`, `unknown_time_zone`, `unresolved_merge_fields`, `unsupported_region`, `velocity_limit_exceeded`, `verification_code_expired`, `verification_limit_reached`.\n\n**Quotas**: tenants may carry per-channel monthly caps (live SMS, email, and a separate sandbox-SMS cap). A send that would cross its cap is refused whole with `quota_exceeded` plus the cap, current usage, the reset moment (the UTC calendar month), and an upgrade pointer when one is configured; read standing any time at `GET /v1/usage`. Sandbox sends never consume a live cap.\n\n**Abuse controls** (P6-05): tenant-scoped keys carry a short-window velocity limit over attempted recipients — crossing it answers HTTP 429 `velocity_limit_exceeded` with a `Retry-After` header. A suspended account refuses every send with `account_suspended`. Tenants with the content screen enabled have message bodies (and email subjects) checked against a SHAFT/prohibited-content deny-list and a public-URL-shortener flag — a match refuses the whole request with `content_refused`, audited; send full links, not shortened ones.\n\n**Sandbox**: a key minted with mode `sandbox` (`ak_test_` prefix) sends through an in-process simulator — nothing ever reaches a carrier or mail provider, rows are flagged `sandbox: true`, and a realistic Delivered receipt appears on the message record. Consent, STOP, and quiet hours run identically to live: the rules are the product, learn them here. `POST /v1/sandbox/inbound` (sandbox keys only) simulates a resident's text — a topic keyword, YES, STOP, HELP, or a plain sentence — so the whole opt-in flow works end to end without a phone.\n\n**Verified destinations**: free live SMS goes only to numbers you have proven you control. `POST /v1/verified-destinations` texts a 6-digit code (US and Canada numbers only, daily caps apply); `POST /v1/verified-destinations/confirm` completes it, records consent in the ledger (campaign `self-verified`), and — a standing property — a send to your own verified number is never held by quiet hours. Live sends to anything else refuse with `destination_not_verified` while the tenant is verified-only.",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.pombity.com/"
    }
  ],
  "paths": {
    "/v1/health": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Health check",
        "description": "Anonymous liveness probe.",
        "operationId": "get-health",
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok",
                  "service": "messaging"
                }
              }
            }
          }
        },
        "security": [ ]
      }
    },
    "/v1/messages": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Send an SMS to one or more recipients",
        "description": "Creates one message per recipient sharing a correlationId. Consent is enforced (no opt-in on record, or an opt-out, yields a Blocked row and no carrier traffic); tenant quiet hours hold non-exempt sends; Emergency overrides quiet hours only when the tenant has enabled it. Provide exactly one of body or templateId. Supports the Idempotency-Key header (see the API description). A sandbox key sends through the in-process simulator (rows flagged, Delivered receipt, zero carrier traffic); a verified-only tenant's live sends refuse unverified recipients with destination_not_verified.",
        "operationId": "post-messages",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMessageDto"
              },
              "example": {
                "tenantId": "fleet",
                "to": [
                  "+14785551234"
                ],
                "body": "Pothole crew dispatched to Main St.",
                "category": "Alert"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "One entry per recipient; check each status (Sent | Failed | Blocked | NotConfigured).",
            "content": {
              "application/json": {
                "example": {
                  "correlationId": "7f3b2c90a1e14bd3",
                  "messages": [
                    {
                      "id": "0ac87ba2f29b4d1d9d6f0d6c0b7f3a21",
                      "to": "+14785551234",
                      "channel": "Sms",
                      "subject": null,
                      "status": "Sent",
                      "statusDetail": null,
                      "segments": 1,
                      "providerMessageId": "acs-msg-1",
                      "deliveryStatus": null,
                      "correlationId": "7f3b2c90a1e14bd3",
                      "createdAt": "2026-11-02T15:04:05+00:00",
                      "sandbox": false
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Refused whole; nothing sent. The code names the reason (quota_exceeded refusals also carry a quota object; account_suspended and content_refused are whole-request too).",
            "content": {
              "application/json": {
                "example": {
                  "error": "Monthly SMS cap would be exceeded: 25 of 25 sent this period and this request adds 1. The cap resets 2026-12-01.",
                  "code": "quota_exceeded",
                  "quota": {
                    "cap": 25,
                    "used": 25,
                    "resetsAt": "2026-12-01T00:00:00+00:00",
                    "upgradeUrl": null
                  }
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key conflict or still in flight.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "429": {
            "description": "This key's velocity limit is reached; retry after the stated delay.",
            "content": {
              "application/json": {
                "example": {
                  "error": "This key's send velocity limit is reached; slow down and retry. Retry after 15 second(s).",
                  "code": "velocity_limit_exceeded",
                  "retryAfterSeconds": 15
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List messages",
        "description": "The tenant's message log, filterable by correlationId and time range; format=csv returns a CSV export.",
        "operationId": "get-messages",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "correlationId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest first.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "0ac87ba2f29b4d1d9d6f0d6c0b7f3a21",
                    "to": "+14785551234",
                    "channel": "Sms",
                    "subject": null,
                    "status": "Sent",
                    "statusDetail": null,
                    "segments": 1,
                    "providerMessageId": "acs-msg-1",
                    "deliveryStatus": null,
                    "correlationId": "7f3b2c90a1e14bd3",
                    "createdAt": "2026-11-02T15:04:05+00:00",
                    "sandbox": false
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/messages/preview": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Preview a send without sending",
        "description": "What the send WOULD do: segment count for the resolved body and, per recipient, whether consent would block it. Read-only — no provider call, no rows, no audit.",
        "operationId": "post-messages-preview",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PreviewMessageDto"
              },
              "example": {
                "tenantId": "fleet",
                "to": [
                  "+14785551234"
                ],
                "body": "Pothole crew dispatched to Main St."
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The counts.",
            "content": {
              "application/json": {
                "example": {
                  "segments": 1,
                  "recipients": 1,
                  "consentBlocked": 0,
                  "invalidRecipients": 0
                }
              }
            }
          },
          "400": {
            "description": "Invalid preview request.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/messages/{id}": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Read one message and its delivery report",
        "description": "The full message record including every delivery receipt received so far — poll this after a send to see carrier delivery.",
        "operationId": "get-messages-id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The message with its deliveries.",
            "content": {
              "application/json": {
                "example": {
                  "id": "0ac87ba2f29b4d1d9d6f0d6c0b7f3a21",
                  "tenantId": "fleet",
                  "to": "+14785551234",
                  "channel": "Sms",
                  "subject": null,
                  "body": "Pothole crew dispatched to Main St.",
                  "category": "Alert",
                  "status": "Sent",
                  "statusDetail": null,
                  "segments": 1,
                  "templateId": null,
                  "correlationId": "7f3b2c90a1e14bd3",
                  "providerMessageId": "acs-msg-1",
                  "deliveryStatus": "Delivered",
                  "deliveryStatusAt": "2026-11-02T15:04:11+00:00",
                  "createdAt": "2026-11-02T15:04:05+00:00",
                  "deliveries": [
                    {
                      "status": "Delivered",
                      "receivedAt": "2026-11-02T15:04:11+00:00",
                      "detail": null
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such message for this tenant.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/emails": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Send an email to one or more recipients",
        "description": "Sender identity comes from the tenant (localPart@sendingDomain, platform default domain when the tenant has none). An opt-out is honored; email is never held by quiet hours. Supports the Idempotency-Key header.",
        "operationId": "post-emails",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailDto"
              },
              "example": {
                "tenantId": "fleet",
                "to": [
                  "ops@example.com"
                ],
                "subject": "Nightly fleet report",
                "body": "All vehicles accounted for.",
                "category": "Informational"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "One entry per recipient.",
            "content": {
              "application/json": {
                "example": {
                  "correlationId": "7f3b2c90a1e14bd3",
                  "messages": [
                    {
                      "id": "1bd87ba2f29b4d1d9d6f0d6c0b7f3a22",
                      "to": "ops@example.com",
                      "channel": "Email",
                      "subject": "Nightly fleet report",
                      "status": "Sent",
                      "statusDetail": null,
                      "segments": 0,
                      "providerMessageId": "acs-email-1",
                      "deliveryStatus": null,
                      "correlationId": "7f3b2c90a1e14bd3",
                      "createdAt": "2026-11-02T15:04:05+00:00",
                      "sandbox": false
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Refused whole; the code names the reason.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key conflict or still in flight.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "429": {
            "description": "This key's velocity limit is reached; retry after the stated delay.",
            "content": {
              "application/json": {
                "example": {
                  "error": "This key's send velocity limit is reached; slow down and retry. Retry after 15 second(s).",
                  "code": "velocity_limit_exceeded",
                  "retryAfterSeconds": 15
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Mint an API key (shown once)",
        "description": "Creates a tenant-scoped key. The full key appears only in this response — store it now; every later listing shows only the display prefix. mode is live (default) or sandbox (ak_test_ prefix). A tenant-scoped caller mints only for its own tenant; an unscoped (admin) key mints for any tenant. Rotation = mint here, then DELETE the old key.",
        "operationId": "post-keys",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyCreateDto"
              },
              "example": {
                "tenantId": "fleet",
                "name": "app key",
                "mode": "live"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The new key — the only time it is shown.",
            "content": {
              "application/json": {
                "example": {
                  "id": "9c1f0d6c0b7f3a210ac87ba2f29b4d1d",
                  "tenantId": "fleet",
                  "name": "app key",
                  "mode": "Live",
                  "key": "ak_live_3f9c…(shown once)",
                  "displayPrefix": "ak_live_3f9c",
                  "createdAt": "2026-11-02T15:04:05+00:00"
                }
              }
            }
          },
          "400": {
            "description": "Missing tenantId or name, unknown tenant, or an unknown mode.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List a tenant's keys",
        "description": "Every key minted for the tenant, revoked included — never the key itself or its hash.",
        "operationId": "get-keys",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The tenant's keys, by id.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "9c1f0d6c0b7f3a210ac87ba2f29b4d1d",
                    "tenantId": "fleet",
                    "name": "app key",
                    "mode": "Live",
                    "displayPrefix": "ak_live_3f9c",
                    "revoked": false,
                    "createdAt": "2026-11-02T15:04:05+00:00",
                    "lastUsedAt": "2026-11-02T16:11:42+00:00"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/keys/{id}": {
      "delete": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Revoke a key",
        "description": "Marks the key revoked (the record is kept for the audit trail); it stops authenticating immediately. A key outside the stated tenant answers 404.",
        "operationId": "delete-keys-id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Revoked."
          },
          "404": {
            "description": "No such key for this tenant.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Read current-period usage, caps, and standing",
        "description": "Counts of sent messages per channel over the UTC calendar month, measured from this service's own log, with the tenant's caps (null = uncapped) and standing (ok | quota_exceeded | account_suspended). The sandbox-SMS allowance has its own cap, separate from the live caps.",
        "operationId": "get-usage",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current period's numbers (live counts feed the live caps; the sandbox split carries its own cap).",
            "content": {
              "application/json": {
                "example": {
                  "tenantId": "fleet",
                  "period": {
                    "start": "2026-11-01T00:00:00+00:00",
                    "end": "2026-12-01T00:00:00+00:00",
                    "resetsAt": "2026-12-01T00:00:00+00:00"
                  },
                  "sms": {
                    "sent": 12,
                    "segments": 14,
                    "cap": 25,
                    "remaining": 13,
                    "sandbox": {
                      "sent": 40,
                      "segments": 41,
                      "cap": 5000,
                      "remaining": 4960
                    }
                  },
                  "email": {
                    "sent": 3,
                    "cap": null,
                    "remaining": null,
                    "sandbox": {
                      "sent": 7
                    }
                  },
                  "standing": "ok"
                }
              }
            }
          },
          "404": {
            "description": "Unknown tenant.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/tenants": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List tenants (admin)",
        "description": "Every tenant. Requires an unscoped key.",
        "operationId": "get-tenants",
        "responses": {
          "200": {
            "description": "All tenants.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "fleet",
                    "name": "Aric Fleet",
                    "sendingNumber": "+18442198901",
                    "timeZone": "America/New_York",
                    "quietHoursStartHour": 20,
                    "quietHoursEndHour": 8,
                    "emergencyOverrideEnabled": true,
                    "sendingDomain": null,
                    "emailLocalPart": "alerts",
                    "emailReplyTo": null,
                    "inboundPolicy": "none",
                    "contactPhone": null,
                    "requireVerifiedDestinations": false,
                    "smsMonthlyCap": null,
                    "emailMonthlyCap": null,
                    "sandboxSmsMonthlyCap": null,
                    "suspended": false,
                    "contentScreenEnabled": false
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/tenants/{id}": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Read one tenant",
        "description": "A tenant-scoped key may read its own record; unscoped keys read any.",
        "operationId": "get-tenants-id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The tenant.",
            "content": {
              "application/json": {
                "example": {
                  "id": "fleet",
                  "name": "Aric Fleet",
                  "sendingNumber": "+18442198901",
                  "timeZone": "America/New_York",
                  "quietHoursStartHour": 20,
                  "quietHoursEndHour": 8,
                  "emergencyOverrideEnabled": true,
                  "sendingDomain": null,
                  "emailLocalPart": "alerts",
                  "emailReplyTo": null,
                  "inboundPolicy": "none",
                  "contactPhone": null,
                  "requireVerifiedDestinations": false,
                  "smsMonthlyCap": null,
                  "emailMonthlyCap": null,
                  "sandboxSmsMonthlyCap": null,
                  "suspended": false,
                  "contentScreenEnabled": false
                }
              }
            }
          },
          "404": {
            "description": "Unknown tenant.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Create or update a tenant (admin)",
        "description": "Merge semantics: unspecified fields keep their current values, so a rename never resets quiet hours. sendingNumber \"\" explicitly takes the tenant off its number. P6-05 (the platform's entitlement push): smsMonthlyCap, emailMonthlyCap, and sandboxSmsMonthlyCap set the per-channel/per-mode caps (-1 explicitly clears a cap to uncapped); suspended disables every send (account_suspended); contentScreenEnabled turns on the SHAFT/shortener content screen.",
        "operationId": "put-tenants-id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantUpsertDto"
              },
              "example": {
                "name": "Aric Fleet",
                "sendingNumber": "+18442198901",
                "timeZone": "America/New_York",
                "quietHoursStartHour": 20,
                "quietHoursEndHour": 8,
                "smsMonthlyCap": 25,
                "emailMonthlyCap": 1000,
                "sandboxSmsMonthlyCap": 5000,
                "suspended": false,
                "contentScreenEnabled": true
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The stored tenant.",
            "content": {
              "application/json": {
                "example": {
                  "id": "fleet",
                  "name": "Aric Fleet",
                  "sendingNumber": "+18442198901",
                  "timeZone": "America/New_York",
                  "quietHoursStartHour": 20,
                  "quietHoursEndHour": 8,
                  "emergencyOverrideEnabled": true,
                  "sendingDomain": null,
                  "emailLocalPart": "alerts",
                  "emailReplyTo": null,
                  "inboundPolicy": "none",
                  "contactPhone": null,
                  "requireVerifiedDestinations": false,
                  "smsMonthlyCap": null,
                  "emailMonthlyCap": null,
                  "sandboxSmsMonthlyCap": null,
                  "suspended": false,
                  "contentScreenEnabled": false
                }
              }
            }
          },
          "400": {
            "description": "Invalid field; the code names it.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/numbers/{number}": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Read a sending number's default tenant (admin)",
        "description": "The explicit default tenant of a shared sending number; unset numbers answer 404 (the default is then the first tenant by id).",
        "operationId": "get-numbers-number",
        "parameters": [
          {
            "name": "number",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The setting.",
            "content": {
              "application/json": {
                "example": {
                  "number": "+18442198901",
                  "defaultTenantId": "pilot-town",
                  "updatedAt": "2026-11-02T15:04:05+00:00"
                }
              }
            }
          },
          "404": {
            "description": "No explicit default set.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Set a sending number's default tenant (admin)",
        "description": "Reserved keywords and unattributable inbound texts on a shared number route to this tenant, which must be on the number.",
        "operationId": "put-numbers-number",
        "parameters": [
          {
            "name": "number",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NumberDefaultUpsertDto"
              },
              "example": {
                "defaultTenantId": "pilot-town"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The stored setting.",
            "content": {
              "application/json": {
                "example": {
                  "number": "+18442198901",
                  "defaultTenantId": "pilot-town",
                  "updatedAt": "2026-11-02T15:04:05+00:00"
                }
              }
            }
          },
          "400": {
            "description": "Unusable number, missing tenant, or a tenant not on the number.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List sending domains (admin)",
        "description": "The email sending-domain registry.",
        "operationId": "get-domains",
        "responses": {
          "200": {
            "description": "All domains.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "name": "aricalerts.com",
                    "ownerTenantId": null,
                    "status": "Verified",
                    "dnsRecords": [
                      {
                        "type": "TXT",
                        "name": "@",
                        "value": "ms-domain-verification=…",
                        "ttl": 3600
                      }
                    ],
                    "createdAt": "2026-10-21T00:00:00+00:00",
                    "verifiedAt": "2026-10-21T12:00:00+00:00"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/domains/{name}": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Read one sending domain (admin)",
        "description": "Includes the DNS records to publish and the verification status.",
        "operationId": "get-domains-name",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The domain.",
            "content": {
              "application/json": {
                "example": {
                  "name": "aricalerts.com",
                  "ownerTenantId": null,
                  "status": "Verified",
                  "dnsRecords": [
                    {
                      "type": "TXT",
                      "name": "@",
                      "value": "ms-domain-verification=…",
                      "ttl": 3600
                    }
                  ],
                  "createdAt": "2026-10-21T00:00:00+00:00",
                  "verifiedAt": "2026-10-21T12:00:00+00:00"
                }
              }
            }
          },
          "404": {
            "description": "Not registered.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Register a sending domain (admin)",
        "description": "Registers the domain with the mail provider and returns the DNS records to publish (verification TXT, SPF, DKIM CNAMEs, DMARC).",
        "operationId": "put-domains-name",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DomainUpsertDto"
              },
              "example": {
                "ownerTenantId": "fleet"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The domain with its DNS records.",
            "content": {
              "application/json": {
                "example": {
                  "name": "aricalerts.com",
                  "ownerTenantId": null,
                  "status": "Verified",
                  "dnsRecords": [
                    {
                      "type": "TXT",
                      "name": "@",
                      "value": "ms-domain-verification=…",
                      "ttl": 3600
                    }
                  ],
                  "createdAt": "2026-10-21T00:00:00+00:00",
                  "verifiedAt": "2026-10-21T12:00:00+00:00"
                }
              }
            }
          },
          "400": {
            "description": "Unusable domain name.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "502": {
            "description": "The mail provider refused the request.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{name}/verify": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Check a sending domain's verification (admin)",
        "description": "Re-checks DNS verification at the provider; on the flip to Verified the default sender identity is ensured.",
        "operationId": "post-domains-name-verify",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The refreshed domain.",
            "content": {
              "application/json": {
                "example": {
                  "name": "aricalerts.com",
                  "ownerTenantId": null,
                  "status": "Verified",
                  "dnsRecords": [
                    {
                      "type": "TXT",
                      "name": "@",
                      "value": "ms-domain-verification=…",
                      "ttl": 3600
                    }
                  ],
                  "createdAt": "2026-10-21T00:00:00+00:00",
                  "verifiedAt": "2026-10-21T12:00:00+00:00"
                }
              }
            }
          },
          "404": {
            "description": "Not registered.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          },
          "502": {
            "description": "The mail provider refused the request.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/acs/delivery": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Event Grid: SMS delivery reports (internal)",
        "description": "Azure Communication Services delivery receipts arrive here via Event Grid; not for API callers. Guarded by the webhook secret, not X-Api-Key.",
        "operationId": "post-webhooks-acs-delivery",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonElement"
              },
              "example": [
                {
                  "eventType": "Microsoft.Communication.SMSDeliveryReportReceived",
                  "data": {
                    "messageId": "acs-msg-1",
                    "deliveryStatus": "Delivered",
                    "receivedTimestamp": "2026-11-02T15:04:11Z",
                    "tag": "0ac87ba2f29b4d1d9d6f0d6c0b7f3a21"
                  }
                }
              ]
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Acknowledged; a SubscriptionValidationEvent echoes its code.",
            "content": {
              "application/json": {
                "example": {
                  "validationResponse": "<validation-code>"
                }
              }
            }
          }
        },
        "security": [ ]
      }
    },
    "/v1/webhooks/acs/email-delivery": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Event Grid: email delivery reports (internal)",
        "description": "Azure Communication Services email delivery reports, correlated by the send operation id; not for API callers.",
        "operationId": "post-webhooks-acs-email-delivery",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonElement"
              },
              "example": [
                {
                  "eventType": "Microsoft.Communication.EmailDeliveryReportReceived",
                  "data": {
                    "messageId": "acs-email-1",
                    "status": "Delivered",
                    "deliveryAttemptTimestamp": "2026-11-02T15:04:11Z"
                  }
                }
              ]
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Acknowledged; a SubscriptionValidationEvent echoes its code.",
            "content": {
              "application/json": {
                "example": {
                  "validationResponse": "<validation-code>"
                }
              }
            }
          }
        },
        "security": [ ]
      }
    },
    "/v1/webhooks/acs/inbound": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Event Grid: inbound SMS (internal)",
        "description": "Inbound texts (STOP/HELP/START, registered keywords, everything else) arrive here via Event Grid, deduplicated by provider message id; not for API callers.",
        "operationId": "post-webhooks-acs-inbound",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonElement"
              },
              "example": [
                {
                  "eventType": "Microsoft.Communication.SMSReceived",
                  "data": {
                    "from": "+14785551234",
                    "to": "+18442198901",
                    "message": "STOP",
                    "receivedTimestamp": "2026-11-02T15:04:11Z",
                    "messageId": "acs-in-1"
                  }
                }
              ]
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Acknowledged; a SubscriptionValidationEvent echoes its code.",
            "content": {
              "application/json": {
                "example": {
                  "validationResponse": "<validation-code>"
                }
              }
            }
          }
        },
        "security": [ ]
      }
    },
    "/v1/consents": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Record a consent change",
        "description": "Appends to the consent ledger (never overwrites history) and audits the change. status: OptedIn | OptedOut; source: Keyword | WebForm | Manual | CsvImport. Self-verified consents are recorded automatically by POST /v1/verified-destinations/confirm (campaign \"self-verified\"), not here.",
        "operationId": "post-consents",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConsentDto"
              },
              "example": {
                "tenantId": "fleet",
                "phone": "+14785551234",
                "status": "OptedIn",
                "source": "Manual",
                "campaign": "fleet-ops"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The recorded entry.",
            "content": {
              "application/json": {
                "example": {
                  "phone": "+14785551234",
                  "status": "OptedIn",
                  "source": "Manual",
                  "occurredAt": "2026-11-02T15:04:05+00:00",
                  "campaign": "fleet-ops",
                  "note": null
                }
              }
            }
          },
          "400": {
            "description": "Unknown status/source or unusable phone number.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Read consent state",
        "description": "Without phone: every number's current status for the tenant. With phone: the current record, or the full history with history=true.",
        "operationId": "get-consents",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "history",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current records (or history).",
            "content": {
              "application/json": {
                "example": [
                  {
                    "phone": "+14785551234",
                    "status": "OptedIn",
                    "source": "Manual",
                    "occurredAt": "2026-11-02T15:04:05+00:00",
                    "campaign": "fleet-ops",
                    "note": null
                  }
                ]
              }
            }
          },
          "404": {
            "description": "No consent on record for that phone.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List a tenant's message templates",
        "description": "Templates referenced by templateId on sends; merge fields use {Name} placeholders.",
        "operationId": "get-templates",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The templates.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "fleet-check-engine",
                    "name": "Check engine",
                    "body": "Aric Fleet: {Asset} check-engine ON…"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/templates/{id}": {
      "put": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Create or update a template",
        "description": "Upserts the tenant's template under the given id.",
        "operationId": "put-templates-id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateDto"
              },
              "example": {
                "name": "Road closure",
                "body": "{Tenant}: {Street} is closed until {Time}."
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "example": {
                  "id": "road-closure"
                }
              }
            }
          },
          "400": {
            "description": "Over-long field.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Delete a template",
        "description": "Removes the tenant's template.",
        "operationId": "delete-templates-id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "No such template.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keyword-callbacks": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List keyword callbacks",
        "description": "The tenant's registered inbound keywords and their callback URLs.",
        "operationId": "get-keyword-callbacks",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The registrations.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "keyword": "ACK",
                    "callbackUrl": "https://alerts.example/ack"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/keyword-callbacks/{keyword}": {
      "put": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Register a keyword callback",
        "description": "Inbound texts carrying the keyword dispatch to the callback URL, signed with HMAC-SHA256 (X-Aric-Signature). A keyword is unique per sending number across tenants (P1-04): a conflict is a 409 and nothing is written.",
        "operationId": "put-keyword-callbacks-keyword",
        "parameters": [
          {
            "name": "keyword",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeywordCallbackDto"
              },
              "example": {
                "callbackUrl": "https://alerts.example/ack",
                "secret": "<shared-hmac-secret>"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Registered.",
            "content": {
              "application/json": {
                "example": {
                  "keyword": "ACK"
                }
              }
            }
          },
          "400": {
            "description": "Over-long field.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "409": {
            "description": "Another tenant on the same number holds this keyword.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Remove a keyword callback",
        "description": "Unregisters the tenant's keyword.",
        "operationId": "delete-keyword-callbacks-keyword",
        "parameters": [
          {
            "name": "keyword",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Removed."
          },
          "404": {
            "description": "No such registration.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/resident-actions": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Report a resident's own action",
        "description": "Records an action the service cannot see (e.g. a web sign-up) so the next single-recipient send to that phone within 15 minutes rides the quiet-hours response exemption. Inbound texts are recorded automatically.",
        "operationId": "post-resident-actions",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResidentActionDto"
              },
              "example": {
                "tenantId": "pilot-town",
                "phone": "+14785551234",
                "source": "web-signup"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "example": {
                  "tenantId": "pilot-town",
                  "phone": "+14785551234",
                  "occurredAt": "2026-11-02T15:04:05+00:00"
                }
              }
            }
          },
          "400": {
            "description": "Unusable phone number or over-long field.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/verified-destinations": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Request a destination verification code",
        "description": "Texts a 6-digit code to the number (expires in 10 minutes). US and Canada numbers only; verification sends are capped per tenant per UTC day and per destination number per UTC day, and a free tenant holds at most 3 verified destinations — each refusal names its code. An already-verified number answers already-verified and sends nothing.",
        "operationId": "post-verified-destinations",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifiedDestinationRequestDto"
              },
              "example": {
                "tenantId": "my-tenant",
                "phone": "+14785551234"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Code sent (or the number was already verified).",
            "content": {
              "application/json": {
                "example": {
                  "tenantId": "my-tenant",
                  "status": "code-sent",
                  "expiresAt": "2026-11-02T15:14:05+00:00"
                }
              }
            }
          },
          "400": {
            "description": "Unusable number, unsupported region, or a cap refusal — the code names it.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "502": {
            "description": "The verification text could not be sent.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "List the tenant's verified destinations",
        "description": "Every number the tenant's key holder has verified.",
        "operationId": "get-verified-destinations",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The verified numbers.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "phone": "+14785551234",
                    "verifiedAt": "2026-11-02T15:05:11+00:00"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/v1/verified-destinations/confirm": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Confirm a verification code",
        "description": "Proves you control the number. On success the destination is verified, consent is recorded in the ledger as self-verified, and a send to this number is never held by quiet hours. Wrong codes are limited to 5 attempts; codes expire after 10 minutes.",
        "operationId": "post-verified-destinations-confirm",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifiedDestinationConfirmDto"
              },
              "example": {
                "tenantId": "my-tenant",
                "phone": "+14785551234",
                "code": "123456"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Verified.",
            "content": {
              "application/json": {
                "example": {
                  "tenantId": "my-tenant",
                  "phone": "+14785551234",
                  "verifiedAt": "2026-11-02T15:05:11+00:00"
                }
              }
            }
          },
          "400": {
            "description": "Wrong, expired, or exhausted code — the code field names it.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/verified-destinations/{phone}": {
      "delete": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Remove a verified destination",
        "description": "Frees one of the tenant's verified-destination slots; live sends to the number then refuse again while the tenant is verified-only.",
        "operationId": "delete-verified-destinations-phone",
        "parameters": [
          {
            "name": "phone",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Removed."
          },
          "404": {
            "description": "Not a verified destination of this tenant.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Not found.",
                  "code": "not_found"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/inbound": {
      "post": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Simulate an inbound text (sandbox keys only)",
        "description": "Drives the real inbound pipeline for your tenant with a simulated text from a number — a topic keyword, YES, STOP, HELP, or a plain sentence — so the whole opt-in flow is exercisable without a carrier. Consent writes are real (one ledger, one set of rules) but never touch co-tenants on a shared number; replies go through the sandbox provider. Live keys are refused with sandbox_key_required.",
        "operationId": "post-sandbox-inbound",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SandboxInboundDto"
              },
              "example": {
                "tenantId": "my-tenant",
                "from": "+14785551234",
                "body": "STOP"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The pipeline outcome (OptedOut | OptedIn | HelpSent | CallbackDispatched | AutoReplied | Unhandled | ...).",
            "content": {
              "application/json": {
                "example": {
                  "outcome": "OptedOut",
                  "from": "+14785551234"
                }
              }
            }
          },
          "400": {
            "description": "Unknown tenant, unusable number, or missing body.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          },
          "403": {
            "description": "The caller is a live key.",
            "content": {
              "application/json": {
                "example": {
                  "error": "Human-readable explanation.",
                  "code": "invalid_request"
                }
              }
            }
          }
        }
      }
    },
    "/v1/audit": {
      "get": {
        "tags": [
          "Messaging.Api"
        ],
        "summary": "Query the audit log",
        "description": "The tenant's append-only audit trail, filterable by time range and action; format=csv returns a CSV export.",
        "operationId": "get-audit",
        "parameters": [
          {
            "name": "tenantId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "action",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching events, oldest first.",
            "content": {
              "application/json": {
                "example": [
                  {
                    "id": "5f2a…",
                    "occurredAt": "2026-11-02T15:04:05+00:00",
                    "actor": "app key",
                    "action": "message.send",
                    "details": "{…}"
                  }
                ]
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ConsentDto": {
        "required": [
          "tenantId",
          "phone",
          "status"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "source": {
            "type": [
              "null",
              "string"
            ]
          },
          "campaign": {
            "type": [
              "null",
              "string"
            ]
          },
          "note": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "DomainUpsertDto": {
        "type": "object",
        "properties": {
          "ownerTenantId": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "JsonElement": { },
      "KeyCreateDto": {
        "type": "object",
        "properties": {
          "tenantId": {
            "type": [
              "null",
              "string"
            ]
          },
          "name": {
            "type": [
              "null",
              "string"
            ]
          },
          "mode": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "KeywordCallbackDto": {
        "required": [
          "callbackUrl",
          "secret"
        ],
        "type": "object",
        "properties": {
          "callbackUrl": {
            "type": "string"
          },
          "secret": {
            "type": "string"
          }
        }
      },
      "NumberDefaultUpsertDto": {
        "type": "object",
        "properties": {
          "defaultTenantId": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "PreviewMessageDto": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "to": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            }
          },
          "templateId": {
            "type": [
              "null",
              "string"
            ]
          },
          "body": {
            "type": [
              "null",
              "string"
            ]
          },
          "mergeFields": {
            "type": [
              "null",
              "object"
            ],
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "ResidentActionDto": {
        "required": [
          "tenantId",
          "phone"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "source": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "SandboxInboundDto": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "from": {
            "type": [
              "null",
              "string"
            ]
          },
          "body": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "SendEmailDto": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "to": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            }
          },
          "subject": {
            "type": [
              "null",
              "string"
            ]
          },
          "templateId": {
            "type": [
              "null",
              "string"
            ]
          },
          "body": {
            "type": [
              "null",
              "string"
            ]
          },
          "mergeFields": {
            "type": [
              "null",
              "object"
            ],
            "additionalProperties": {
              "type": "string"
            }
          },
          "category": {
            "type": [
              "null",
              "string"
            ]
          },
          "isHtml": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "correlationId": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "SendMessageDto": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "to": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            }
          },
          "templateId": {
            "type": [
              "null",
              "string"
            ]
          },
          "body": {
            "type": [
              "null",
              "string"
            ]
          },
          "mergeFields": {
            "type": [
              "null",
              "object"
            ],
            "additionalProperties": {
              "type": "string"
            }
          },
          "category": {
            "type": [
              "null",
              "string"
            ]
          },
          "correlationId": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "TemplateDto": {
        "required": [
          "name",
          "body"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "body": {
            "type": "string"
          }
        }
      },
      "TenantUpsertDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "null",
              "string"
            ]
          },
          "sendingNumber": {
            "type": [
              "null",
              "string"
            ]
          },
          "timeZone": {
            "type": [
              "null",
              "string"
            ]
          },
          "quietHoursStartHour": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "quietHoursEndHour": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "emergencyOverrideEnabled": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "sendingDomain": {
            "type": [
              "null",
              "string"
            ]
          },
          "emailLocalPart": {
            "type": [
              "null",
              "string"
            ]
          },
          "emailReplyTo": {
            "type": [
              "null",
              "string"
            ]
          },
          "inboundPolicy": {
            "type": [
              "null",
              "string"
            ]
          },
          "contactPhone": {
            "type": [
              "null",
              "string"
            ]
          },
          "requireVerifiedDestinations": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "smsMonthlyCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "emailMonthlyCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "sandboxSmsMonthlyCap": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "suspended": {
            "type": [
              "null",
              "boolean"
            ]
          },
          "contentScreenEnabled": {
            "type": [
              "null",
              "boolean"
            ]
          }
        }
      },
      "VerifiedDestinationConfirmDto": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "phone": {
            "type": [
              "null",
              "string"
            ]
          },
          "code": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "VerifiedDestinationRequestDto": {
        "required": [
          "tenantId"
        ],
        "type": "object",
        "properties": {
          "tenantId": {
            "type": "string"
          },
          "phone": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "Your API key, presented on every request. Keys are minted at POST /v1/keys (shown once; only a hash is stored). An unscoped (admin) key may act for any tenant; a tenant-scoped key only for its own.",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "ApiKey": [ ]
    }
  ],
  "tags": [
    {
      "name": "Messaging.Api"
    }
  ]
}