Pombity

by Aric Software & Design

API reference

Messaging API 1.0.0, rendered from a snapshot of the service's own OpenAPI document. The authoritative, always-current copy is https://api.pombity.com/openapi/v1.json; it carries the full error-code catalog in its description and examples on every operation.

Base URL: https://api.pombity.com. Auth: the X-Api-Key header on every request. MCP mount: https://api.pombity.com/mcp (same key as a bearer token). Sends accept Idempotency-Key. The rules behind every refusal: the compliance layer.

Operations

GET /v1/health

Health check

Anonymous liveness probe.

Response 200 (example)

{
  "status": "ok",
  "service": "messaging"
}

POST /v1/messages

Send an SMS to one or more recipients

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.

Request body (example)

{
  "tenantId": "fleet",
  "to": [
    "+14785551234"
  ],
  "body": "Pothole crew dispatched to Main St.",
  "category": "Alert"
}

Response 200 (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
    }
  ]
}

Error responses: 400, 409, 429; every error body carries a catalog code.

GET /v1/messages

List messages

The tenant's message log, filterable by correlationId and time range; format=csv returns a CSV export.

Parameters
NameInRequiredDescription
tenantIdqueryyes
correlationIdqueryno
fromqueryno
toqueryno
formatqueryno
modequeryno

Response 200 (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
  }
]

POST /v1/messages/preview

Preview a send without sending

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.

Request body (example)

{
  "tenantId": "fleet",
  "to": [
    "+14785551234"
  ],
  "body": "Pothole crew dispatched to Main St."
}

Response 200 (example)

{
  "segments": 1,
  "recipients": 1,
  "consentBlocked": 0,
  "invalidRecipients": 0
}

Error responses: 400; every error body carries a catalog code.

GET /v1/messages/{id}

Read one message and its delivery report

The full message record including every delivery receipt received so far — poll this after a send to see carrier delivery.

Parameters
NameInRequiredDescription
idpathyes
tenantIdqueryyes

Response 200 (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
    }
  ]
}

Error responses: 404; every error body carries a catalog code.

POST /v1/emails

Send an email to one or more recipients

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.

Request body (example)

{
  "tenantId": "fleet",
  "to": [
    "ops@example.com"
  ],
  "subject": "Nightly fleet report",
  "body": "All vehicles accounted for.",
  "category": "Informational"
}

Response 200 (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
    }
  ]
}

Error responses: 400, 409, 429; every error body carries a catalog code.

POST /v1/keys

Mint an API key (shown once)

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.

Request body (example)

{
  "tenantId": "fleet",
  "name": "app key",
  "mode": "live"
}

Response 200 (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"
}

Error responses: 400; every error body carries a catalog code.

GET /v1/keys

List a tenant's keys

Every key minted for the tenant, revoked included — never the key itself or its hash.

Parameters
NameInRequiredDescription
tenantIdqueryyes

Response 200 (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"
  }
]

DELETE /v1/keys/{id}

Revoke a key

Marks the key revoked (the record is kept for the audit trail); it stops authenticating immediately. A key outside the stated tenant answers 404.

Parameters
NameInRequiredDescription
idpathyes
tenantIdqueryyes

Error responses: 404; every error body carries a catalog code.

GET /v1/usage

Read current-period usage, caps, and standing

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.

Parameters
NameInRequiredDescription
tenantIdqueryyes

Response 200 (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"
}

Error responses: 404; every error body carries a catalog code.

GET /v1/tenants

List tenants (admin)

Every tenant. Requires an unscoped key.

Response 200 (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
  }
]

GET /v1/tenants/{id}

Read one tenant

A tenant-scoped key may read its own record; unscoped keys read any.

Parameters
NameInRequiredDescription
idpathyes

Response 200 (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
}

Error responses: 404; every error body carries a catalog code.

PUT /v1/tenants/{id}

Create or update a tenant (admin)

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.

Parameters
NameInRequiredDescription
idpathyes

Request body (example)

{
  "name": "Aric Fleet",
  "sendingNumber": "+18442198901",
  "timeZone": "America/New_York",
  "quietHoursStartHour": 20,
  "quietHoursEndHour": 8,
  "smsMonthlyCap": 25,
  "emailMonthlyCap": 1000,
  "sandboxSmsMonthlyCap": 5000,
  "suspended": false,
  "contentScreenEnabled": true
}

Response 200 (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
}

Error responses: 400; every error body carries a catalog code.

GET /v1/numbers/{number}

Read a sending number's default tenant (admin)

The explicit default tenant of a shared sending number; unset numbers answer 404 (the default is then the first tenant by id).

Parameters
NameInRequiredDescription
numberpathyes

Response 200 (example)

{
  "number": "+18442198901",
  "defaultTenantId": "pilot-town",
  "updatedAt": "2026-11-02T15:04:05+00:00"
}

Error responses: 404; every error body carries a catalog code.

PUT /v1/numbers/{number}

Set a sending number's default tenant (admin)

Reserved keywords and unattributable inbound texts on a shared number route to this tenant, which must be on the number.

Parameters
NameInRequiredDescription
numberpathyes

Request body (example)

{
  "defaultTenantId": "pilot-town"
}

Response 200 (example)

{
  "number": "+18442198901",
  "defaultTenantId": "pilot-town",
  "updatedAt": "2026-11-02T15:04:05+00:00"
}

Error responses: 400; every error body carries a catalog code.

GET /v1/domains

List sending domains (admin)

The email sending-domain registry.

Response 200 (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"
  }
]

GET /v1/domains/{name}

Read one sending domain (admin)

Includes the DNS records to publish and the verification status.

Parameters
NameInRequiredDescription
namepathyes

Response 200 (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"
}

Error responses: 404; every error body carries a catalog code.

PUT /v1/domains/{name}

Register a sending domain (admin)

Registers the domain with the mail provider and returns the DNS records to publish (verification TXT, SPF, DKIM CNAMEs, DMARC).

Parameters
NameInRequiredDescription
namepathyes

Request body (example)

{
  "ownerTenantId": "fleet"
}

Response 200 (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"
}

Error responses: 400, 502; every error body carries a catalog code.

POST /v1/domains/{name}/verify

Check a sending domain's verification (admin)

Re-checks DNS verification at the provider; on the flip to Verified the default sender identity is ensured.

Parameters
NameInRequiredDescription
namepathyes

Response 200 (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"
}

Error responses: 404, 502; every error body carries a catalog code.

POST /v1/webhooks/acs/delivery

Event Grid: SMS delivery reports (internal)

Azure Communication Services delivery receipts arrive here via Event Grid; not for API callers. Guarded by the webhook secret, not X-Api-Key.

Request body (example)

[
  {
    "eventType": "Microsoft.Communication.SMSDeliveryReportReceived",
    "data": {
      "messageId": "acs-msg-1",
      "deliveryStatus": "Delivered",
      "receivedTimestamp": "2026-11-02T15:04:11Z",
      "tag": "0ac87ba2f29b4d1d9d6f0d6c0b7f3a21"
    }
  }
]

Response 200 (example)

{
  "validationResponse": "<validation-code>"
}

POST /v1/webhooks/acs/email-delivery

Event Grid: email delivery reports (internal)

Azure Communication Services email delivery reports, correlated by the send operation id; not for API callers.

Request body (example)

[
  {
    "eventType": "Microsoft.Communication.EmailDeliveryReportReceived",
    "data": {
      "messageId": "acs-email-1",
      "status": "Delivered",
      "deliveryAttemptTimestamp": "2026-11-02T15:04:11Z"
    }
  }
]

Response 200 (example)

{
  "validationResponse": "<validation-code>"
}

POST /v1/webhooks/acs/inbound

Event Grid: inbound SMS (internal)

Inbound texts (STOP/HELP/START, registered keywords, everything else) arrive here via Event Grid, deduplicated by provider message id; not for API callers.

Request body (example)

[
  {
    "eventType": "Microsoft.Communication.SMSReceived",
    "data": {
      "from": "+14785551234",
      "to": "+18442198901",
      "message": "STOP",
      "receivedTimestamp": "2026-11-02T15:04:11Z",
      "messageId": "acs-in-1"
    }
  }
]

Response 200 (example)

{
  "validationResponse": "<validation-code>"
}

POST /v1/consents

Record a consent change

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.

Request body (example)

{
  "tenantId": "fleet",
  "phone": "+14785551234",
  "status": "OptedIn",
  "source": "Manual",
  "campaign": "fleet-ops"
}

Response 200 (example)

{
  "phone": "+14785551234",
  "status": "OptedIn",
  "source": "Manual",
  "occurredAt": "2026-11-02T15:04:05+00:00",
  "campaign": "fleet-ops",
  "note": null
}

Error responses: 400; every error body carries a catalog code.

GET /v1/consents

Read consent state

Without phone: every number's current status for the tenant. With phone: the current record, or the full history with history=true.

Parameters
NameInRequiredDescription
tenantIdqueryyes
phonequeryno
historyqueryno

Response 200 (example)

[
  {
    "phone": "+14785551234",
    "status": "OptedIn",
    "source": "Manual",
    "occurredAt": "2026-11-02T15:04:05+00:00",
    "campaign": "fleet-ops",
    "note": null
  }
]

Error responses: 404; every error body carries a catalog code.

GET /v1/templates

List a tenant's message templates

Templates referenced by templateId on sends; merge fields use {Name} placeholders.

Parameters
NameInRequiredDescription
tenantIdqueryyes

Response 200 (example)

[
  {
    "id": "fleet-check-engine",
    "name": "Check engine",
    "body": "Aric Fleet: {Asset} check-engine ON…"
  }
]

PUT /v1/templates/{id}

Create or update a template

Upserts the tenant's template under the given id.

Parameters
NameInRequiredDescription
idpathyes
tenantIdqueryyes

Request body (example)

{
  "name": "Road closure",
  "body": "{Tenant}: {Street} is closed until {Time}."
}

Response 200 (example)

{
  "id": "road-closure"
}

Error responses: 400; every error body carries a catalog code.

DELETE /v1/templates/{id}

Delete a template

Removes the tenant's template.

Parameters
NameInRequiredDescription
idpathyes
tenantIdqueryyes

Error responses: 404; every error body carries a catalog code.

GET /v1/keyword-callbacks

List keyword callbacks

The tenant's registered inbound keywords and their callback URLs.

Parameters
NameInRequiredDescription
tenantIdqueryyes

Response 200 (example)

[
  {
    "keyword": "ACK",
    "callbackUrl": "https://alerts.example/ack"
  }
]

PUT /v1/keyword-callbacks/{keyword}

Register a keyword callback

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.

Parameters
NameInRequiredDescription
keywordpathyes
tenantIdqueryyes

Request body (example)

{
  "callbackUrl": "https://alerts.example/ack",
  "secret": "<shared-hmac-secret>"
}

Response 200 (example)

{
  "keyword": "ACK"
}

Error responses: 400, 409; every error body carries a catalog code.

DELETE /v1/keyword-callbacks/{keyword}

Remove a keyword callback

Unregisters the tenant's keyword.

Parameters
NameInRequiredDescription
keywordpathyes
tenantIdqueryyes

Error responses: 404; every error body carries a catalog code.

POST /v1/resident-actions

Report a resident's own action

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.

Request body (example)

{
  "tenantId": "pilot-town",
  "phone": "+14785551234",
  "source": "web-signup"
}

Response 200 (example)

{
  "tenantId": "pilot-town",
  "phone": "+14785551234",
  "occurredAt": "2026-11-02T15:04:05+00:00"
}

Error responses: 400; every error body carries a catalog code.

POST /v1/verified-destinations

Request a destination verification code

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.

Request body (example)

{
  "tenantId": "my-tenant",
  "phone": "+14785551234"
}

Response 200 (example)

{
  "tenantId": "my-tenant",
  "status": "code-sent",
  "expiresAt": "2026-11-02T15:14:05+00:00"
}

Error responses: 400, 502; every error body carries a catalog code.

GET /v1/verified-destinations

List the tenant's verified destinations

Every number the tenant's key holder has verified.

Parameters
NameInRequiredDescription
tenantIdqueryyes

Response 200 (example)

[
  {
    "phone": "+14785551234",
    "verifiedAt": "2026-11-02T15:05:11+00:00"
  }
]

POST /v1/verified-destinations/confirm

Confirm a verification code

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.

Request body (example)

{
  "tenantId": "my-tenant",
  "phone": "+14785551234",
  "code": "123456"
}

Response 200 (example)

{
  "tenantId": "my-tenant",
  "phone": "+14785551234",
  "verifiedAt": "2026-11-02T15:05:11+00:00"
}

Error responses: 400; every error body carries a catalog code.

DELETE /v1/verified-destinations/{phone}

Remove a verified destination

Frees one of the tenant's verified-destination slots; live sends to the number then refuse again while the tenant is verified-only.

Parameters
NameInRequiredDescription
phonepathyes
tenantIdqueryyes

Error responses: 404; every error body carries a catalog code.

POST /v1/sandbox/inbound

Simulate an inbound text (sandbox keys only)

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.

Request body (example)

{
  "tenantId": "my-tenant",
  "from": "+14785551234",
  "body": "STOP"
}

Response 200 (example)

{
  "outcome": "OptedOut",
  "from": "+14785551234"
}

Error responses: 400, 403; every error body carries a catalog code.

GET /v1/audit

Query the audit log

The tenant's append-only audit trail, filterable by time range and action; format=csv returns a CSV export.

Parameters
NameInRequiredDescription
tenantIdqueryyes
fromqueryno
toqueryno
actionqueryno
formatqueryno

Response 200 (example)

[
  {
    "id": "5f2a…",
    "occurredAt": "2026-11-02T15:04:05+00:00",
    "actor": "app key",
    "action": "message.send",
    "details": "{…}"
  }
]