{
  "openapi": "3.0.3",
  "info": {
    "title": "NotifyHub API",
    "description": "Multi-channel notification API by InOnda Network. Send messages via WhatsApp, Telegram, Email, Push, Discord, and Slack through a single unified endpoint.",
    "version": "1.0.0",
    "contact": {
      "name": "NotifyHub Support",
      "email": "info@trovido.com",
      "url": "https://notify.trovido.com/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://notify.trovido.com/api/notifyhub/v1",
      "description": "Production"
    }
  ],
  "security": [
    { "BearerAuth": [] }
  ],
  "paths": {
    "/send": {
      "post": {
        "operationId": "sendNotification",
        "summary": "Send a notification",
        "description": "Send a single notification to a recipient via the specified channel. Supports idempotency, scheduled delivery, action buttons with callbacks, media attachments, and automatic channel inference.",
        "tags": ["Send"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendRequest" },
              "examples": {
                "whatsapp_template": {
                  "summary": "WhatsApp template message",
                  "value": {
                    "recipient": { "id": 1 },
                    "template": "nh_booking_created",
                    "vars": { "1": "Diego", "2": "15/05/2026", "3": "18/05/2026", "4": "3" },
                    "channel": "whatsapp",
                    "priority": "normal"
                  }
                },
                "telegram_with_actions": {
                  "summary": "Telegram with action buttons",
                  "value": {
                    "recipient": { "telegram_chat_id": "624419126", "name": "Diego" },
                    "template": "nh_review_request",
                    "vars": { "1": "Diego", "2": "https://example.com/review" },
                    "channel": "telegram",
                    "actions": [
                      { "slug": "approve", "label": "Pubblica", "callback_url": "https://example.com/api/moderate", "payload": { "review_id": 42 } },
                      { "slug": "reject", "label": "Rifiuta", "callback_url": "https://example.com/api/moderate", "payload": { "review_id": 42 } }
                    ]
                  }
                },
                "email_scheduled": {
                  "summary": "Scheduled email",
                  "value": {
                    "recipient": { "email": "user@example.com", "name": "Mario Rossi" },
                    "template": "welcome_notification",
                    "channel": "email",
                    "scheduled_for": "2026-05-01T09:00:00Z",
                    "idempotency_key": "welcome-mario-20260501"
                  }
                },
                "discord_webhook": {
                  "summary": "Discord notification",
                  "value": {
                    "recipient": { "id": 5 },
                    "template": "service_update",
                    "vars": { "1": "Server maintenance", "2": "Completed successfully" },
                    "channel": "discord"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Notification queued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SendResponse" }
              }
            }
          },
          "200": {
            "description": "Idempotent replay (notification already sent)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReplayResponse" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/RecipientNotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/send/batch": {
      "post": {
        "operationId": "sendBatch",
        "summary": "Send batch notifications",
        "description": "Send up to 100 notifications in a single request. Each message is processed independently.",
        "tags": ["Send"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BatchRequest" },
              "example": {
                "messages": [
                  { "recipient": { "id": 1 }, "template": "service_update", "channel": "telegram" },
                  { "recipient": { "id": 2 }, "template": "service_update", "channel": "email" }
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Batch accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchResponse" }
              }
            }
          },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get usage statistics",
        "description": "Returns current month usage statistics for the authenticated source's workspace, including per-channel breakdowns and quota limits.",
        "tags": ["Usage"],
        "responses": {
          "200": {
            "description": "Usage statistics",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UsageResponse" }
              }
            }
          }
        }
      }
    },
    "/wh/{webhook_key}": {
      "post": {
        "operationId": "webhookTrigger",
        "summary": "Webhook trigger (no auth header)",
        "description": "Send a notification via webhook URL. No Authorization header needed — the URL itself authenticates. Same payload format as POST /send. Ideal for Zapier, Make, n8n, GitHub Actions, Sentry, etc.",
        "tags": ["Send"],
        "security": [],
        "parameters": [
          {
            "name": "webhook_key",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "pattern": "^nhwh_[A-Za-z0-9]+$" },
            "description": "Webhook key from the portal (Sources > Generate Webhook URL)"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendRequest" },
              "example": {
                "recipient": { "id": 1 },
                "template": "service_update",
                "vars": { "1": "Deploy completed", "2": "v2.3.1 deployed successfully" },
                "channel": "telegram"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Notification queued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SendResponse" }
              }
            }
          },
          "401": { "description": "Invalid or revoked webhook key" },
          "429": { "$ref": "#/components/responses/QuotaExceeded" }
        }
      }
    },
    "/nh/action/{token}": {
      "get": {
        "operationId": "actionCallback",
        "summary": "Action callback (end-user facing)",
        "description": "When a recipient taps an action button in a notification (Telegram inline button, WhatsApp quick-reply, or email link), they are redirected here. The token is single-use, HMAC-verified, and expires after 72 hours.\n\n**Flow:**\n1. Your API sends a notification with `actions[]` containing `callback_url` and `payload`\n2. NotifyHub generates HMAC-signed tokens and embeds them as buttons/links\n3. Recipient clicks → this endpoint validates the token\n4. NotifyHub POSTs to your `callback_url` with the action decision\n5. Recipient sees a branded confirmation page\n\n**Callback POST to your server:**\n```json\nPOST {callback_url}\nX-NotifyHub-Signature: sha256=abc123...\nContent-Type: application/json\n\n{\n  \"delivery_id\": 42,\n  \"action\": \"approve\",\n  \"payload\": { \"review_id\": 123 },\n  \"recipient\": { \"id\": 1, \"name\": \"Diego\" },\n  \"timestamp\": \"2026-05-07T10:30:00Z\"\n}\n```\n\nVerify the `X-NotifyHub-Signature` header using your source API key as the HMAC secret.",
        "tags": ["Actions"],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "pattern": "^[A-Za-z0-9]{64}$" },
            "description": "64-character action token (auto-generated, embedded in notification buttons)"
          }
        ],
        "responses": {
          "200": {
            "description": "Action processed — renders branded confirmation page to the user",
            "content": {
              "text/html": {
                "schema": { "type": "string" }
              }
            }
          },
          "404": {
            "description": "Token not found or invalid"
          },
          "410": {
            "description": "Token already used or expired"
          },
          "403": {
            "description": "HMAC signature verification failed"
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "healthCheck",
        "summary": "System health check",
        "description": "Returns system health status including worker activity, queue depth, and delivery stats for the last hour.",
        "tags": ["System"],
        "security": [],
        "responses": {
          "200": {
            "description": "System healthy",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthResponse" }
              }
            }
          },
          "503": {
            "description": "System degraded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from the NotifyHub portal (Sources > API Keys). Format: `nh_live_xxxx` or `nh_test_xxxx`."
      }
    },
    "schemas": {
      "SendRequest": {
        "type": "object",
        "required": ["recipient", "template"],
        "properties": {
          "recipient": { "$ref": "#/components/schemas/Recipient" },
          "template": { "type": "string", "maxLength": 100, "description": "Template slug", "example": "nh_booking_created" },
          "vars": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Template variables (positional for WhatsApp: '1', '2', ...)" },
          "channel": { "type": "string", "enum": ["telegram", "whatsapp", "email", "push", "discord", "slack", "messenger"], "description": "Target channel. If omitted, inferred from recipient fields." },
          "priority": { "type": "string", "enum": ["low", "normal", "high"], "default": "normal" },
          "idempotency_key": { "type": "string", "maxLength": 128, "description": "Unique key for idempotent delivery (dedup window: 1 hour)" },
          "scheduled_for": { "type": "string", "format": "date-time", "description": "ISO 8601 datetime for scheduled delivery" },
          "actions": { "type": "array", "maxItems": 5, "items": { "$ref": "#/components/schemas/Action" } },
          "media": { "$ref": "#/components/schemas/Media" },
          "attachments": { "type": "array", "maxItems": 5, "items": { "$ref": "#/components/schemas/Attachment" } }
        }
      },
      "Recipient": {
        "type": "object",
        "description": "Identify recipient by ID or contact fields. At least one is required.",
        "properties": {
          "id": { "type": "integer", "description": "Existing recipient ID in your workspace" },
          "name": { "type": "string", "maxLength": 255 },
          "email": { "type": "string", "format": "email" },
          "whatsapp_phone": { "type": "string", "maxLength": 32, "example": "+393332535589" },
          "telegram_chat_id": { "type": "string", "maxLength": 255 },
          "locale": { "type": "string", "maxLength": 10, "default": "it", "example": "it" }
        }
      },
      "Action": {
        "type": "object",
        "required": ["slug", "callback_url"],
        "properties": {
          "slug": { "type": "string", "maxLength": 50, "example": "approve" },
          "label": { "type": "string", "maxLength": 30, "example": "Pubblica" },
          "callback_url": { "type": "string", "format": "uri", "maxLength": 1000, "description": "URL that receives HMAC-signed POST when user clicks" },
          "payload": { "type": "object", "description": "Custom data forwarded to callback" }
        }
      },
      "Media": {
        "type": "object",
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "type": { "type": "string", "enum": ["image", "document", "video"], "default": "image" },
          "filename": { "type": "string" },
          "caption": { "type": "string", "maxLength": 1000 }
        }
      },
      "Attachment": {
        "type": "object",
        "properties": {
          "filename": { "type": "string", "maxLength": 255 },
          "content": { "type": "string", "description": "Base64-encoded file content" },
          "url": { "type": "string", "format": "uri", "description": "Public URL (alternative to base64 content)" },
          "mime_type": { "type": "string", "maxLength": 100 }
        }
      },
      "SendResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["queued", "sending", "sent", "delivered"], "example": "queued" },
          "delivery_id": { "type": "integer", "example": 42 },
          "channel": { "type": "string", "example": "whatsapp" },
          "recipient": {
            "type": "object",
            "properties": {
              "id": { "type": "integer" },
              "name": { "type": "string" },
              "address": { "type": "string" }
            }
          },
          "template": { "type": "string" },
          "actions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": { "type": "string" },
                "url": { "type": "string", "format": "uri" }
              }
            }
          },
          "idempotency_key": { "type": "string", "nullable": true },
          "overage": {
            "type": "object",
            "nullable": true,
            "properties": {
              "credits_deducted": { "type": "integer" },
              "channel_cost": { "type": "integer" }
            }
          }
        }
      },
      "ReplayResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string" },
          "delivery_id": { "type": "integer" },
          "idempotency_key": { "type": "string" },
          "replayed": { "type": "boolean", "example": true }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": ["messages"],
        "properties": {
          "messages": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "$ref": "#/components/schemas/SendRequest" } }
        }
      },
      "BatchResponse": {
        "type": "object",
        "properties": {
          "total": { "type": "integer" },
          "queued": { "type": "integer" },
          "failed": { "type": "integer" },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/SendResponse" } }
        }
      },
      "UsageResponse": {
        "type": "object",
        "properties": {
          "workspace_id": { "type": "integer" },
          "period": { "type": "string", "example": "2026-04" },
          "total_sent": { "type": "integer" },
          "total_failed": { "type": "integer" },
          "by_channel": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "sent": { "type": "integer" },
                "failed": { "type": "integer" }
              }
            }
          },
          "quota": {
            "type": "object",
            "properties": {
              "limit": { "type": "integer", "nullable": true },
              "used": { "type": "integer" },
              "remaining": { "type": "integer", "nullable": true }
            }
          }
        }
      },
      "ActionCallbackPayload": {
        "type": "object",
        "description": "Payload POSTed to your callback_url when a recipient clicks an action button. Verify the X-NotifyHub-Signature header (HMAC-SHA256 of the body using your API key as secret).",
        "properties": {
          "delivery_id": { "type": "integer", "description": "The delivery that contained the action", "example": 42 },
          "action": { "type": "string", "description": "The action slug that was clicked", "example": "approve" },
          "payload": { "type": "object", "description": "Custom payload you provided in the original send request", "example": { "review_id": 123 } },
          "recipient": {
            "type": "object",
            "properties": {
              "id": { "type": "integer" },
              "name": { "type": "string" }
            }
          },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["healthy", "degraded"] },
          "worker": { "type": "string", "enum": ["active", "stale"] },
          "worker_last_activity": { "type": "string", "format": "date-time", "nullable": true },
          "queue_depth": { "type": "integer" },
          "last_hour": {
            "type": "object",
            "properties": {
              "sent": { "type": "integer" },
              "delivered": { "type": "integer" },
              "failed": { "type": "integer" }
            }
          },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      }
    },
    "responses": {
      "RecipientNotFound": {
        "description": "Recipient not found",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string", "example": "recipient_not_found" },
                "message": { "type": "string" }
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Validation failed",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string", "example": "validation_failed" },
                "details": { "type": "object" }
              }
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "Monthly quota exceeded",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": { "type": "string", "example": "quota_exceeded" },
                "message": { "type": "string" },
                "used": { "type": "integer" },
                "limit": { "type": "integer" },
                "hint": { "type": "string", "nullable": true }
              }
            }
          }
        }
      }
    }
  }
}
