Docs / API reference

REST API reference

Create and reconcile subscriptions, test delivery, query tenant health, and integrate webhook senders. The machine-readable contract is available as OpenAPI 3.1 JSON.

Managed service or self-hosted?

The hosted service uses Microsoft Entra bearer tokens for management calls. Function keys plus x-org-id are for self-hosted deployments where Entra is not configured. These modes are mutually exclusive.

Base URL and formats

https://tdckbprod-flex-prod.azurewebsites.net/api
  • Request and response bodies are JSON unless noted.
  • Send Content-Type: application/json for request bodies.
  • Timestamps are UTC ISO 8601 strings.
  • Webhook and API-key fields are write-only and never echoed back.

Authentication

CredentialSend asUsed for
Entra access tokenAuthorization: Bearer …Hosted management API. Tenant must be installed and caller must hold Connector.Admin.
Function host keyx-functions-key: …Self-hosted management API when Entra is disabled.
Organization IDx-org-id: acmeRequired with a function key; ignored as identity in Entra mode.
Subscription key?key=… or x-api-key: …Direct ingest and broadcast webhook authentication.

A malformed bearer token never falls back to a function key. Never put management or ingest credentials in logs, browser analytics, Prometheus target URLs, or support screenshots.

Subscriptions

A subscription binds one platform to exactly one default destination: Teams Workflows webhook, bot channel, or bot DM. It may also include filters, routing, branding, and noise controls.

POST/subscriptions · create

Creates a subscription. The response returns the plaintext apiKey and complete ingestUrl once. Store the ingest URL in the sending platform immediately.

curl -X POST https://tdckbprod-flex-prod.azurewebsites.net/api/subscriptions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production builds",
    "platform": "github",
    "botChannel": {
      "conversationId": "19:…@thread.tacv2",
      "serviceUrl": "https://smba.trafficmanager.net/amer/"
    },
    "statusFilter": ["failure", "deployment_failure"],
    "enabled": true
  }'

Responses: 201 created, 400 invalid input, 402 plan limit.

GET/subscriptions · list

Lists the caller tenant's subscriptions. Each item can include lastDelivery. Ingest keys and destination URLs are redacted.

curl https://tdckbprod-flex-prod.azurewebsites.net/api/subscriptions \
  -H "Authorization: Bearer $ACCESS_TOKEN"
GET/subscriptions/{id} · read

Returns one redacted subscription and an ETag response header. Save that ETag before a declarative PUT.

PUT/subscriptions/{id} · full replacement

Replaces every client-configurable field. It requires the exact ETag returned by the latest GET. Omitted optional fields are intentionally cleared.

# 1. Read the current ETag
curl -sS -D headers.txt -o subscription.json \
  https://tdckbprod-flex-prod.azurewebsites.net/api/subscriptions/$SUB_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# 2. Send the complete desired state
curl -X PUT https://tdckbprod-flex-prod.azurewebsites.net/api/subscriptions/$SUB_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'If-Match: "current-etag"' \
  -H "Content-Type: application/json" \
  -d @desired-subscription.json

428 means the exact If-Match header is missing. 412 means another writer changed the subscription: GET, reconcile, and retry. IDs, organization, API-key hash, and creation time remain server-owned. Platform cannot change because it is part of the ingest URL.

PATCH/subscriptions/{id} · partial update

Updates only fields present in the body. Use PATCH for ordinary UI-style edits. Setting one destination clears the previous destination atomically.

curl -X PATCH https://tdckbprod-flex-prod.azurewebsites.net/api/subscriptions/$SUB_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
DELETE/subscriptions/{id} · delete

Deletes a subscription and returns 204. The audit trail records the deletion.

POST/subscriptions/{id}/test · test delivery

Sends a synthetic success card through the subscription's actual destination. It bypasses filters so it tests connectivity. The result updates lastDelivery.

POST/subscriptions/{id}/rotate-key · rotate ingest key

Invalidates the old key and returns a new plaintext key and ingest URL once. Update the sender immediately.

Health and monitoring

GET/health · public liveness

Returns 200 when the process is alive. It does not check dependencies.

{"ok":true,"service":"permylastwebhook","version":"…","time":"…"}
GET/ready · public readiness

Returns 200 only when required configuration and Cosmos are ready; otherwise 503 with sanitized named checks. No internal endpoint or SDK error is exposed.

GET/monitoring/status · tenant status

Authenticated tenant-scoped summary for scripts, Datadog, or Grafana. It contains healthy/failing/unknown/disabled counts and last-delivery outcomes, but no tenant IDs, payloads, keys, ingest URLs, webhook URLs, bot conversation details, or DM identifiers.

curl https://tdckbprod-flex-prod.azurewebsites.net/api/monitoring/status \
  -H "Authorization: Bearer $ACCESS_TOKEN"

See Monitoring & health for Prometheus and OTLP patterns.

Webhook ingestion

Direct ingest (recommended)

POST /api/ingest/{orgId}/{subscriptionId}/{platform}?key={subscriptionKey}

Use the exact ingestUrl returned on create or key rotation. A direct URL selects one subscription, validates the platform and key, then applies its filters and routing.

Broadcast ingest

POST /api/webhooks/{platform}
x-api-key: {subscriptionKey}
x-org-id: {organization}

For header-capable senders. It fans out to matching enabled subscriptions. Direct ingest is simpler and avoids ambiguous fan-out.

Errors and retries

Errors use {"error":"message","details":{…}}. Validation details contain field errors, never credentials.

StatusMeaningClient action
400Invalid input or platform mismatchFix the request; do not retry unchanged.
401Missing/invalid identity or API keyRefresh token or replace the key.
402Plan limitReduce gated configuration or upgrade.
404Resource not foundRefresh local state.
409State conflictRead current state before retrying.
412 / 428Stale or missing PUT preconditionGET the ETag, reconcile, retry.
429Rate limitWait for Retry-After.
502Teams delivery exhausted retriesInspect Last delivery; retry later.
Need a complete schema?

Download the live OpenAPI document, or email hello@permylastwebhook.com with an integration question.

▣ StartPER MY LAST WEBHOOKC:\DOCS\APIREST: 200 OK