{
  "openapi": "3.1.0",
  "info": {
    "title": "Przypominamy.com SMS API",
    "version": "1.2.0",
    "summary": "Polskie REST API do wysyłki SMS, MMS i wiadomości głosowych IVR/TTS",
    "description": "REST API do programowej wysyłki SMS, MMS i głosowych wiadomości IVR/TTS w Polsce i na świecie.\n\n**Dla kogo**: małe i średnie firmy potrzebujące prostego, polskiego API do komunikacji z klientami — przypomnienia o wizytach, potwierdzenia zamówień, 2FA/OTP, powiadomienia transakcyjne, kampanie marketingowe.\n\n## Autoryzacja\n\nBearer token wygenerujesz samodzielnie w panelu klienta po założeniu konta na https://app.przypominamy.com/register. Rejestracja przez magic-link (bez hasła), aktywacja konta natychmiastowa. Tokeny rotujesz w sekcji **Klucze API** w panelu — możesz mieć wiele tokenów z etykietami (np. `production`, `n8n`, `zapier`) i odwoływać je w razie kompromitacji.\n\n```\nAuthorization: Bearer pmy_<40-znaków-hex>\n```\n\n## Rate limiting\n\nDomyślnie 100 żądań na minutę per klucz. W przypadku przekroczenia API zwraca HTTP 429 z nagłówkiem `Retry-After`. Limit można zwiększyć po kontakcie z supportem.\n\n## Saldo i płatności\n\n- **Prepaid** (domyślnie): saldo PLN dekrementowane przy każdej wysyłce. Doładowanie przez Stripe (karty, BLIK, Przelewy24) w https://app.przypominamy.com/billing. Niewystarczające środki = HTTP 402.\n- **Postpaid** (Enterprise, po umowie): saldo może iść do `-credit_limit_grosze`, fakturujemy miesięcznie przez Fakturownia (faktura VAT 23%, JPK).\n\nSaldo sprawdzisz przez `GET /v1/balance`. Pole `balance_grosze` zwraca dokładną kwotę w groszach (preferowane dla obliczeń), `balance` zwraca PLN jako liczbę zmiennoprzecinkową (kompat).\n\n## Historia wysyłek\n\nKażda wysyłka jest persystowana. Pobierz paginowany log przez `GET /v1/messages?limit=50&cursor=<sent_at_unix>`. Wszystkie wysyłki dostępne też w panelu w sekcji **Wysyłki**.\n\n## Webhooki (DLR, incoming SMS)\n\nRaporty doręczenia (DLR) i SMS-y przychodzące dostarczamy na Twój `webhook_url` (skonfigurowany w panelu klienta → **Webhook**). Każde wywołanie podpisujemy HMAC-SHA256:\n\n```\nX-Przypominamy-Signature: v1,t=<unix_timestamp>,sig=<hex_signature>\n```\n\n**Weryfikacja**: `sig = HMAC-SHA256(webhook_secret, \"v1.${timestamp}.${rawBody}\")`. Tolerancja czasowa 5 minut (anti-replay). `webhook_secret` znajdziesz w panelu (sekcja **Webhook → Sekret HMAC**), tam też zregenerujesz w razie kompromitacji.\n\nPrzykład w Node.js:\n\n```js\nconst crypto = require('crypto');\nconst header = req.headers['x-przypominamy-signature']; // \"v1,t=1748341234,sig=abc...\"\nconst [version, tPart, sigPart] = header.split(',');\nconst t = tPart.slice(2);\nconst sig = sigPart.slice(4);\nconst expected = crypto.createHmac('sha256', WEBHOOK_SECRET)\n                       .update(`${version}.${t}.${rawBody}`)\n                       .digest('hex');\nif (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {\n  return res.status(403).end();\n}\nif (Math.abs(Date.now()/1000 - Number(t)) > 300) {\n  return res.status(403).end(); // anti-replay\n}\n```\n\n## Cennik\n\nPay-as-you-go od 0,10 zł/SMS, bez abonamentu. Pełny cennik: https://przypominamy.com/#cennik\n\n## Linki\n\n- **Dokumentacja czytelna**: https://przypominamy.com/api/docs\n- **Strona API**: https://przypominamy.com/api\n- **Panel klienta**: https://app.przypominamy.com\n- **Status**: https://przypominamy.com/api → sekcja Status",
    "termsOfService": "https://przypominamy.com/regulamin",
    "contact": {
      "name": "Wsparcie Przypominamy.com",
      "email": "support@przypominamy.com",
      "url": "https://przypominamy.com/api"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://przypominamy.com/regulamin"
    },
    "x-logo": {
      "url": "https://przypominamy.com/przypominamy-logo.png",
      "altText": "Przypominamy.com"
    },
    "x-audience": "Polskie małe i średnie firmy integrujące SMS w CRM, e-commerce, aplikacjach 2FA, systemach umawiania wizyt i windykacji.",
    "x-llm-summary": "Przypominamy.com to polskie REST API do wysyłki SMS, MMS i wiadomości głosowych IVR/TTS dla małych i średnich firm. Osiem endpointów (SMS, bulk SMS, MMS, VMS, email, HLR, balance, messages), autoryzacja Bearer token z self-service rotacją w panelu klienta (app.przypominamy.com). Webhooki DLR i incoming SMS podpisane HMAC-SHA256 (header X-Przypominamy-Signature, format: v1,t=<unix>,sig=<hex>, podpisywany 'v1.${ts}.${body}', tolerancja 5min). Tryb prepaid (Stripe topup) lub postpaid (faktura VAT przez Fakturownia). Rejestracja self-service przez magic-link, KYC NIP via Biała Lista MF. Pay-as-you-go od 0,10 zł/SMS bez abonamentu. Polskie wsparcie."
  },
  "servers": [
    {
      "url": "https://api.przypominamy.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Messaging",
      "description": "Wysyłka SMS, MMS i wiadomości głosowych (VMS)"
    },
    {
      "name": "Lookup",
      "description": "Weryfikacja numeru (HLR) i saldo konta"
    },
    {
      "name": "Webhooks",
      "description": "Raporty doręczenia (DLR) i SMS-y przychodzące — wysyłane z platformy do Twojego URL"
    }
  ],
  "paths": {
    "/v1/sms": {
      "post": {
        "operationId": "sendSms",
        "summary": "Wyślij pojedynczy SMS",
        "description": "Wysyła jeden SMS do jednego odbiorcy. Obsługuje personalizację nadawcy, planowanie wysyłki, flash SMS, normalizację znaków (usuwanie polskich znaków diakrytycznych) oraz idempotency przez pole `idx`.",
        "tags": [
          "Messaging"
        ],
        "x-tags": [
          "bramka SMS API",
          "REST SMS",
          "wysyłka SMS programowo",
          "API SMS Polska"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SmsRequest"
              },
              "example": {
                "to": "+48600123456",
                "message": "Cześć Anna, przypominamy o wizycie jutro o 14:00. Pozdrawiamy!",
                "from": "FIRMA",
                "idx": "appointment-12345"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SMS przyjęty do wysyłki",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageListResponse"
                },
                "example": {
                  "success": true,
                  "data": {
                    "count": 1,
                    "messages": [
                      {
                        "id": "msg_abc123",
                        "to": "+48600123456",
                        "status": "queued",
                        "parts": 1,
                        "cost": 0.1,
                        "sent_at": "2026-05-17T10:30:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -X POST https://api.przypominamy.com/v1/sms \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"to\": \"+48600123456\",\n    \"message\": \"Cześć! Przypominamy o wizycie jutro o 14:00.\",\n    \"from\": \"FIRMA\"\n  }'"
          },
          {
            "lang": "Python",
            "source": "import os, requests\n\nresp = requests.post(\n    \"https://api.przypominamy.com/v1/sms\",\n    headers={\"Authorization\": f\"Bearer {os.environ['PRZYPOMINAMY_API_KEY']}\"},\n    json={\n        \"to\": \"+48600123456\",\n        \"message\": \"Cześć! Przypominamy o wizycie jutro o 14:00.\",\n        \"from\": \"FIRMA\",\n        \"idx\": \"appointment-12345\",\n    },\n    timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json())"
          },
          {
            "lang": "Node.js",
            "source": "const res = await fetch('https://api.przypominamy.com/v1/sms', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${process.env.PRZYPOMINAMY_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    to: '+48600123456',\n    message: 'Cześć! Przypominamy o wizycie jutro o 14:00.',\n    from: 'FIRMA',\n  }),\n});\nconst data = await res.json();\nconsole.log(data);"
          }
        ]
      }
    },
    "/v1/sms/bulk": {
      "post": {
        "operationId": "sendSmsBulk",
        "summary": "Wyślij SMS-y do wielu odbiorców",
        "description": "Dwa tryby:\n\n1. **Ta sama treść do wielu odbiorców** — przekaż `recipients: [...]` (max 10 000) i `message`. Najwydajniejszy tryb dla kampanii.\n2. **Indywidualne treści** — przekaż `messages: [{to, message}, ...]` (max 100). Idealny dla spersonalizowanych przypomnień.\n\nObsługa nadawcy, planowania i webhooków DLR identyczna jak w `/v1/sms`.",
        "tags": [
          "Messaging"
        ],
        "x-tags": [
          "bulk SMS API",
          "masowa wysyłka SMS",
          "kampania SMS API"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/BulkSameMessageRequest"
                  },
                  {
                    "$ref": "#/components/schemas/BulkIndividualRequest"
                  }
                ]
              },
              "examples": {
                "sameMessage": {
                  "summary": "Ta sama treść do wielu odbiorców",
                  "value": {
                    "recipients": [
                      "+48600123456",
                      "+48600234567",
                      "+48600345678"
                    ],
                    "message": "Promocja -20% tylko do niedzieli! Kod: SMS20",
                    "from": "FIRMA"
                  }
                },
                "individual": {
                  "summary": "Indywidualne treści per odbiorca",
                  "value": {
                    "messages": [
                      {
                        "to": "+48600123456",
                        "message": "Cześć Anna, wizyta 17.05 o 14:00"
                      },
                      {
                        "to": "+48600234567",
                        "message": "Cześć Piotr, wizyta 17.05 o 15:30"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lista wysłanych SMS-ów",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -X POST https://api.przypominamy.com/v1/sms/bulk \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"recipients\": [\"+48600123456\", \"+48600234567\"],\n    \"message\": \"Promocja -20% do niedzieli!\"\n  }'"
          },
          {
            "lang": "Python",
            "source": "import os, requests\n\nresp = requests.post(\n    \"https://api.przypominamy.com/v1/sms/bulk\",\n    headers={\"Authorization\": f\"Bearer {os.environ['PRZYPOMINAMY_API_KEY']}\"},\n    json={\n        \"recipients\": [\"+48600123456\", \"+48600234567\"],\n        \"message\": \"Promocja -20% do niedzieli!\",\n    },\n)\nprint(resp.json())"
          },
          {
            "lang": "Node.js",
            "source": "const res = await fetch('https://api.przypominamy.com/v1/sms/bulk', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${process.env.PRZYPOMINAMY_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    recipients: ['+48600123456', '+48600234567'],\n    message: 'Promocja -20% do niedzieli!',\n  }),\n});\nconsole.log(await res.json());"
          }
        ]
      }
    },
    "/v1/mms": {
      "post": {
        "operationId": "sendMms",
        "summary": "Wyślij MMS (wiadomość multimedialną)",
        "description": "Wysyła MMS z markupem SMIL. SMIL pozwala załączyć obraz, audio lub video z timing'iem. Wymagane: `to`, `subject`, `smil`.",
        "tags": [
          "Messaging"
        ],
        "x-tags": [
          "MMS API",
          "wysyłka MMS programowo"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MmsRequest"
              },
              "example": {
                "to": "+48600123456",
                "subject": "Nowa kolekcja wiosna 2026",
                "smil": "<smil><body><par><img src=\"https://example.com/promo.jpg\"/><text>Sprawdź nową kolekcję!</text></par></body></smil>",
                "from": "FIRMA"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MMS przyjęty do wysyłki",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -X POST https://api.przypominamy.com/v1/mms \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"to\": \"+48600123456\",\n    \"subject\": \"Nowa kolekcja\",\n    \"smil\": \"<smil>...</smil>\"\n  }'"
          }
        ]
      }
    },
    "/v1/vms": {
      "post": {
        "operationId": "sendVms",
        "summary": "Wyślij wiadomość głosową (VMS / IVR / TTS)",
        "description": "Wykonuje połączenie głosowe i odczytuje syntetyzowany głosem (TTS) tekst. Cztery polskojęzyczne lektorzy: `ewa`, `jacek`, `jan`, `maja`. Domyślnie `ewa`. Liczba prób połączenia 1–6 (parametr `tries`).",
        "tags": [
          "Messaging"
        ],
        "x-tags": [
          "VMS API",
          "TTS API",
          "IVR Polska",
          "wiadomość głosowa programowo"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VmsRequest"
              },
              "example": {
                "to": "+48600123456",
                "tts": "Dzień dobry, przypominamy o wizycie jutro o czternastej. Pozdrawiamy.",
                "tts_lector": "ewa",
                "tries": 3
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Połączenie zaplanowane",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -X POST https://api.przypominamy.com/v1/vms \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"to\": \"+48600123456\",\n    \"tts\": \"Dzień dobry, przypominamy o wizycie jutro o czternastej.\",\n    \"tts_lector\": \"ewa\",\n    \"tries\": 3\n  }'"
          }
        ]
      }
    },
    "/v1/hlr": {
      "get": {
        "operationId": "lookupHlr",
        "summary": "Weryfikacja numeru (HLR lookup)",
        "description": "Sprawdza status, sieć i kraj numeru telefonu w bazie HLR (Home Location Register). Przydatne do walidacji baz numerów przed kampanią — eliminuje nieaktywne numery i obniża koszty.",
        "tags": [
          "Lookup"
        ],
        "x-tags": [
          "HLR lookup",
          "weryfikacja numeru API"
        ],
        "parameters": [
          {
            "name": "number",
            "in": "query",
            "required": true,
            "description": "Numer telefonu w formacie międzynarodowym (z prefiksem kraju, np. `+48600123456`).",
            "schema": {
              "type": "string",
              "example": "+48600123456"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Wynik HLR",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HlrResponse"
                },
                "example": {
                  "success": true,
                  "data": {
                    "number": "+48600123456",
                    "status": "ok",
                    "ported": false,
                    "network": "Orange",
                    "country": "PL",
                    "cost": 0.05
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl \"https://api.przypominamy.com/v1/hlr?number=%2B48600123456\" \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\""
          }
        ]
      }
    },
    "/v1/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Sprawdź saldo konta",
        "description": "Zwraca aktualne saldo konta w PLN. Dla klientów na planie prepaid/postpaid zwraca pełne pola D1 (`balance_grosze`, `credit_limit_grosze`, `billing_mode`). Dla legacy klientów (bez migracji do panelu) zwraca proxy na saldo upstream (pole `balance` z `username`).",
        "tags": [
          "Lookup"
        ],
        "x-tags": [
          "saldo API",
          "balance check"
        ],
        "responses": {
          "200": {
            "description": "Saldo konta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BalanceResponse"
                },
                "examples": {
                  "prepaid": {
                    "summary": "Klient prepaid (D1 managed)",
                    "value": {
                      "success": true,
                      "data": {
                        "balance": 450.75,
                        "balance_grosze": 45075,
                        "credit_limit_grosze": 0,
                        "currency": "PLN",
                        "billing_mode": "prepaid"
                      }
                    }
                  },
                  "postpaid": {
                    "summary": "Klient postpaid (Enterprise)",
                    "value": {
                      "success": true,
                      "data": {
                        "balance": -1234.5,
                        "balance_grosze": -123450,
                        "credit_limit_grosze": 500000,
                        "currency": "PLN",
                        "billing_mode": "postpaid"
                      }
                    }
                  },
                  "legacy": {
                    "summary": "Klient legacy (proxy na upstream)",
                    "value": {
                      "success": true,
                      "data": {
                        "balance": 450.75,
                        "currency": "PLN",
                        "username": "twojafirma"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl https://api.przypominamy.com/v1/balance \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\""
          }
        ]
      }
    },
    "/v1/messages": {
      "get": {
        "operationId": "listMessages",
        "summary": "Historia wysyłek (paginowany log)",
        "description": "Zwraca paginowany log wysłanych wiadomości w odwrotnej kolejności chronologicznej (najnowsze najpierw). Każda wysyłka (przez `/v1/sms`, `/v1/sms/bulk`, `/v1/mms`, `/v1/vms`) jest persystowana automatycznie w naszej bazie.\n\nPaginacja **cursor-based**: pierwsze żądanie bez `cursor` zwraca najnowsze. W odpowiedzi pole `next_cursor` (lub `null` jeśli to ostatnia strona). Następne żądanie: `?cursor=<next_cursor>`.\n\nFiltry: `status`, zakres dat `from`/`to` (ISO 8601). Maksymalny `limit` to 200 (domyślnie 50).\n\n**Wymóg**: konto migrowane do panelu klienta (app.przypominamy.com). Konta legacy bez migracji dostają HTTP 403 z linkiem do aktywacji.",
        "tags": [
          "Lookup"
        ],
        "x-tags": [
          "historia SMS",
          "log wysyłek API",
          "DLR history"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Liczba rekordów na stronę. Domyślnie 50, max 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Wartość `next_cursor` z poprzedniej strony (unix epoch w sekundach). Zwraca rekordy ze `sent_at < cursor`.",
            "schema": {
              "type": "integer",
              "example": 1748341234
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Filtruj po statusie (np. `delivered`, `failed`, `queued`).",
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "sent",
                "delivered",
                "not_found",
                "expired",
                "failed",
                "accepted",
                "unknown",
                "rejected",
                "undelivered"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Początek zakresu dat (ISO 8601). Zwraca wysyłki `sent_at >= from`.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-05-01T00:00:00Z"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Koniec zakresu dat (ISO 8601). Zwraca wysyłki `sent_at <= to`.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-05-31T23:59:59Z"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Strona logu wysyłek",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagesLogResponse"
                },
                "example": {
                  "success": true,
                  "data": {
                    "count": 2,
                    "next_cursor": 1748340000,
                    "messages": [
                      {
                        "id": "msg_a1b2c3d4e5f6789012345678",
                        "smsapi_id": "sms_xyz123",
                        "type": "sms",
                        "sender": "FIRMA",
                        "recipient": "+48600123456",
                        "message_preview": "Cześć Anna, przypominamy o wizycie jutro o 14:00...",
                        "parts": 1,
                        "cost_grosze": 5,
                        "cost_pln": 0.05,
                        "status": "delivered",
                        "dlr_status": "delivered",
                        "dlr_updated_at": "2026-05-17T10:30:04.000Z",
                        "idx": "appointment-12345",
                        "sent_at": "2026-05-17T10:30:00.000Z"
                      },
                      {
                        "id": "msg_b2c3d4e5f6789012345678ab",
                        "smsapi_id": "sms_xyz124",
                        "type": "sms",
                        "sender": "FIRMA",
                        "recipient": "+48600234567",
                        "message_preview": "Promocja -20% do niedzieli!",
                        "parts": 1,
                        "cost_grosze": 5,
                        "cost_pln": 0.05,
                        "status": "sent",
                        "dlr_status": null,
                        "dlr_updated_at": null,
                        "idx": null,
                        "sent_at": "2026-05-17T10:28:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "# Pierwsza strona (najnowsze 50)\ncurl https://api.przypominamy.com/v1/messages \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\"\n\n# Następna strona\ncurl \"https://api.przypominamy.com/v1/messages?cursor=1748340000&limit=100\" \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\"\n\n# Tylko niedoręczone z ostatniego tygodnia\ncurl \"https://api.przypominamy.com/v1/messages?status=failed&from=2026-05-10T00:00:00Z\" \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\""
          },
          {
            "lang": "Python",
            "source": "import os, requests\n\nparams = {\"limit\": 100}\nwhile True:\n    r = requests.get(\n        \"https://api.przypominamy.com/v1/messages\",\n        headers={\"Authorization\": f\"Bearer {os.environ['PRZYPOMINAMY_API_KEY']}\"},\n        params=params,\n    )\n    r.raise_for_status()\n    page = r.json()[\"data\"]\n    for msg in page[\"messages\"]:\n        print(msg[\"sent_at\"], msg[\"recipient\"], msg[\"status\"])\n    if not page[\"next_cursor\"]:\n        break\n    params[\"cursor\"] = page[\"next_cursor\"]"
          },
          {
            "lang": "Node.js",
            "source": "let cursor = null;\nwhile (true) {\n  const url = new URL('https://api.przypominamy.com/v1/messages');\n  url.searchParams.set('limit', '100');\n  if (cursor) url.searchParams.set('cursor', String(cursor));\n  const res = await fetch(url, {\n    headers: { Authorization: `Bearer ${process.env.PRZYPOMINAMY_API_KEY}` },\n  });\n  const { data } = await res.json();\n  for (const msg of data.messages) {\n    console.log(msg.sent_at, msg.recipient, msg.status);\n  }\n  if (!data.next_cursor) break;\n  cursor = data.next_cursor;\n}"
          }
        ]
      }
    },
    "/v1/email": {
      "post": {
        "operationId": "sendEmail",
        "summary": "Wyślij transactional email",
        "description": "Wysyła transactional email przez Resend (upstream provider). Wymaga zweryfikowanej domeny nadawcy (dodaj w panel.przypominamy.com/settings/email — DKIM/SPF/MX records w DNS).\n\n**Cennik**: 0,01 zł / wysłany email (1 grosz). Saldo dekrementowane przy wysyłce. Niewystarczające środki → HTTP 402.\n\n**Events przez webhook** (skonfigurowany webhook_url): `email_event` z polem `kind` in `[sent, delivered, bounced, complained, delayed, opened, clicked]`. Podpisane HMAC `X-Przypominamy-Signature` (analog do DLR SMS).\n\n**Limity**: HTML max 500 KB, do 50 odbiorców na request (multi-recipient), CC/BCC do 20.",
        "tags": [
          "Messaging"
        ],
        "x-tags": [
          "email API",
          "transactional email",
          "wysyłka email programowo",
          "API email Polska"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailRequest"
              },
              "example": {
                "to": "klient@example.com",
                "subject": "Potwierdzenie zamówienia #12345",
                "html": "<h1>Dziękujemy za zamówienie!</h1><p>Numer: <strong>12345</strong></p>",
                "text": "Dziękujemy za zamówienie! Numer: 12345",
                "from": "sklep@twojafirma.pl",
                "from_name": "Sklep Twojafirma",
                "reply_to": "support@twojafirma.pl",
                "idx": "order-12345"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email przyjęty do wysyłki",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailResponse"
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "a7f3e8b2-9c4d-4e5f-8a1b-2c3d4e5f6a7b",
                    "to": "klient@example.com",
                    "status": "queued",
                    "cost": 0.01,
                    "sent_at": "2026-05-27T10:30:00.000Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -X POST https://api.przypominamy.com/v1/email \\\n  -H \"Authorization: Bearer $PRZYPOMINAMY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"to\": \"klient@example.com\",\n    \"subject\": \"Potwierdzenie zamówienia\",\n    \"html\": \"<h1>Dziękujemy!</h1><p>Numer: 12345</p>\",\n    \"from\": \"sklep@twojafirma.pl\",\n    \"from_name\": \"Sklep Twojafirma\"\n  }'"
          },
          {
            "lang": "Python",
            "source": "import os, requests\n\nresp = requests.post(\n    \"https://api.przypominamy.com/v1/email\",\n    headers={\"Authorization\": f\"Bearer {os.environ['PRZYPOMINAMY_API_KEY']}\"},\n    json={\n        \"to\": \"klient@example.com\",\n        \"subject\": \"Potwierdzenie zamówienia\",\n        \"html\": \"<h1>Dziękujemy!</h1>\",\n        \"from\": \"sklep@twojafirma.pl\",\n    },\n)\nprint(resp.json())"
          }
        ]
      }
    }
  },
  "webhooks": {
    "deliveryReport": {
      "post": {
        "operationId": "deliveryReport",
        "summary": "Raport doręczenia (DLR)",
        "description": "Platforma wysyła POST do skonfigurowanego `webhook_url` za każdym razem, gdy status SMS-a się zmienia (doręczony, niedoręczony, błąd). Konfiguracja `webhook_url` w panelu klienta (app.przypominamy.com/settings/webhook).\n\n**Bezpieczeństwo**: każde wywołanie zawiera nagłówek `X-Przypominamy-Signature: v1,t=<unix>,sig=<hex>` z HMAC-SHA256 podpisem treści (sekretu używasz tego samego co w panelu). Weryfikuj podpis przed przetworzeniem — patrz szczegóły w sekcji opisu API.\n\nWebhook jest fire-and-forget — odpowiedz HTTP 2xx jak najszybciej, najlepiej kolejkując zadanie do dalszej obsługi. Brak retry przy błędach 5xx (na MVP).",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-Przypominamy-Signature",
            "in": "header",
            "required": true,
            "description": "Podpis HMAC w formacie `v1,t=<unix_timestamp>,sig=<hex>`. sig = HMAC-SHA256(webhook_secret, `v1.${t}.${rawBody}`). Tolerancja czasowa 5 minut.",
            "schema": {
              "type": "string",
              "example": "v1,t=1748341234,sig=a3f5b8c9d2e7f8a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "example": "Przypominamy-Webhook/1.0"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeliveryReportPayload"
              },
              "example": {
                "event": "delivery_report",
                "message_id": "msg_abc123",
                "to": "+48600123456",
                "status": "delivered",
                "sent_at": "2026-05-17T10:30:00.000Z",
                "done_at": "2026-05-17T10:30:04.000Z",
                "idx": "appointment-12345"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Przyjęto raport. Platforma nie ponawia w razie błędu."
          }
        }
      }
    },
    "incomingSms": {
      "post": {
        "operationId": "incomingSms",
        "summary": "SMS przychodzący (two-way SMS)",
        "description": "Platforma wysyła POST do skonfigurowanego `webhook_url`, gdy odbiorca odpowie na Twojego SMS-a (lub wyśle SMS na Twój numer). Wymaga numeru dwukierunkowego — kontakt z supportem.\n\n**Bezpieczeństwo**: identyczny format `X-Przypominamy-Signature` jak w DLR.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-Przypominamy-Signature",
            "in": "header",
            "required": true,
            "description": "Podpis HMAC w formacie `v1,t=<unix_timestamp>,sig=<hex>`. Weryfikacja identyczna jak w DLR.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IncomingSmsPayload"
              },
              "example": {
                "event": "incoming_sms",
                "message_id": "in_xyz789",
                "from": "+48600123456",
                "to": "+48732555000",
                "message": "TAK",
                "received_at": "2026-05-17T10:32:15.000Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Przyjęto wiadomość. Platforma nie ponawia w razie błędu."
          }
        }
      }
    },
    "emailEvent": {
      "post": {
        "operationId": "emailEvent",
        "summary": "Email event (delivered/bounced/complained/opened/clicked)",
        "description": "Resend wysyła event do api.przypominamy.com/v1/email/callback, my forwardingujemy do klienta z HMAC podpisem (`X-Przypominamy-Signature: v1,t=<unix>,sig=<hex>`).\n\nPole `kind` wskazuje typ eventu: `sent`/`delivered`/`bounced`/`complained`/`delayed`/`opened`/`clicked`. Pełne dane z providera w `provider_payload`.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-Przypominamy-Signature",
            "in": "header",
            "required": true,
            "description": "HMAC podpis (format jak w DLR SMS).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailEventPayload"
              },
              "example": {
                "event": "email_event",
                "kind": "delivered",
                "email_id": "a7f3e8b2-9c4d-4e5f-8a1b-2c3d4e5f6a7b",
                "to": "klient@example.com",
                "from": "sklep@twojafirma.pl",
                "subject": "Potwierdzenie zamówienia",
                "received_at": "2026-05-27T10:30:14.000Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Przyjęto event. Platforma nie ponawia w razie błędu."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "Token wygenerowany w panelu klienta (https://app.przypominamy.com/tokens). Format: `pmy_<40-znaków-hex>`. Przekaż w nagłówku `Authorization: Bearer <token>`. Wiele tokenów per konto z etykietami; rotacja i odwoływanie natychmiastowe (revoked tokens odrzucamy z HTTP 401)."
      }
    },
    "schemas": {
      "SmsRequest": {
        "type": "object",
        "required": [
          "to",
          "message"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "Numer odbiorcy w formacie międzynarodowym (z prefiksem +48 dla Polski).",
            "example": "+48600123456"
          },
          "message": {
            "type": "string",
            "description": "Treść SMS-a. Dla `encoding=utf-8` jedna część = 70 znaków, kolejne łączone w concatenated SMS. Dla `gsm` jedna część = 160 znaków.",
            "example": "Cześć! Przypominamy o wizycie jutro o 14:00."
          },
          "from": {
            "type": "string",
            "description": "Nazwa nadawcy (sender ID), max 11 znaków. Wymaga pre-rejestracji w panelu. Jeśli pominiete, używamy domyślnego nadawcy klienta.",
            "example": "FIRMA"
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "Data zaplanowanej wysyłki (ISO 8601). Jeśli pominięta — wysłany natychmiast. Maksymalnie 60 dni w przyszłość.",
            "example": "2026-05-18T14:00:00Z"
          },
          "encoding": {
            "type": "string",
            "enum": [
              "utf-8",
              "gsm"
            ],
            "default": "utf-8",
            "description": "`utf-8` zachowuje polskie znaki diakrytyczne (70 znaków/część). `gsm` używa tablicy GSM-7 bez polskich znaków (160 znaków/część)."
          },
          "flash": {
            "type": "boolean",
            "description": "Flash SMS — wyświetlany na ekranie odbiorcy bez konieczności otwierania."
          },
          "normalize": {
            "type": "boolean",
            "description": "Usuń polskie znaki diakrytyczne (ą→a, ż→z, itd.) przed wysyłką."
          },
          "idx": {
            "type": "string",
            "description": "Twój identyfikator wiadomości (idempotency key). Zwracany w DLR webhookach — pozwala skojarzyć raport z Twoją encją.",
            "example": "appointment-12345"
          }
        }
      },
      "BulkSameMessageRequest": {
        "type": "object",
        "required": [
          "recipients",
          "message"
        ],
        "properties": {
          "recipients": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 10000,
            "description": "Lista numerów odbiorców (max 10 000).",
            "example": [
              "+48600123456",
              "+48600234567"
            ]
          },
          "message": {
            "type": "string",
            "description": "Treść identyczna dla wszystkich odbiorców."
          },
          "from": {
            "type": "string",
            "description": "Nadawca (jak w `/v1/sms`)."
          },
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "encoding": {
            "type": "string",
            "enum": [
              "utf-8",
              "gsm"
            ],
            "default": "utf-8"
          }
        }
      },
      "BulkIndividualRequest": {
        "type": "object",
        "required": [
          "messages"
        ],
        "properties": {
          "messages": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "object",
              "required": [
                "to",
                "message"
              ],
              "properties": {
                "to": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "from": {
                  "type": "string"
                },
                "date": {
                  "type": "string",
                  "format": "date-time"
                },
                "idx": {
                  "type": "string"
                }
              }
            },
            "description": "Lista wiadomości z indywidualnymi treściami (max 100). Idealne dla spersonalizowanych przypomnień."
          }
        }
      },
      "MmsRequest": {
        "type": "object",
        "required": [
          "to",
          "subject",
          "smil"
        ],
        "properties": {
          "to": {
            "type": "string",
            "example": "+48600123456"
          },
          "subject": {
            "type": "string",
            "description": "Temat MMS-a."
          },
          "smil": {
            "type": "string",
            "description": "Markup SMIL z multimediami. Obsługuje `<img>`, `<audio>`, `<video>`, `<text>` z timingiem `<par>` i `<seq>`."
          },
          "from": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VmsRequest": {
        "type": "object",
        "required": [
          "to",
          "tts"
        ],
        "properties": {
          "to": {
            "type": "string",
            "example": "+48600123456"
          },
          "tts": {
            "type": "string",
            "description": "Tekst do odczytania przez syntezator mowy."
          },
          "tts_lector": {
            "type": "string",
            "enum": [
              "ewa",
              "jacek",
              "jan",
              "maja"
            ],
            "default": "ewa",
            "description": "Polski lektor TTS."
          },
          "tries": {
            "type": "integer",
            "minimum": 1,
            "maximum": 6,
            "default": 1,
            "description": "Liczba prób połączenia, jeśli abonent nie odbiera."
          },
          "from": {
            "type": "string",
            "description": "Numer prezentowany dzwoniącemu."
          },
          "date": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identyfikator wiadomości w platformie.",
            "example": "msg_abc123"
          },
          "to": {
            "type": "string",
            "example": "+48600123456"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "not_found",
              "expired",
              "failed",
              "accepted",
              "unknown",
              "rejected",
              "undelivered"
            ],
            "description": "Status doręczenia. `queued` → `sent` → `delivered` (lub status terminalny: `failed`, `expired`, itd.)."
          },
          "parts": {
            "type": "integer",
            "description": "Liczba części concatenated SMS (1 dla krótkich, 2+ dla długich).",
            "example": 1
          },
          "cost": {
            "type": "number",
            "description": "Koszt wysyłki w punktach (1 punkt = 1 grosz).",
            "example": 0.1
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "MessageListResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "data": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer",
                "example": 1
              },
              "messages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          }
        }
      },
      "HlrResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "nullable": true,
            "properties": {
              "number": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "description": "Status HLR (np. `ok`, `not_active`, `incorrect`)."
              },
              "ported": {
                "type": "boolean",
                "description": "Czy numer był przenoszony między sieciami."
              },
              "network": {
                "type": "string",
                "nullable": true
              },
              "country": {
                "type": "string",
                "nullable": true,
                "description": "Kod kraju ISO-3166 alfa-2."
              },
              "cost": {
                "type": "number"
              }
            }
          }
        }
      },
      "BalanceResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "balance": {
                "type": "number",
                "description": "Saldo w PLN (liczba zmiennoprzecinkowa). Dla precyzji obliczeń używaj `balance_grosze`."
              },
              "balance_grosze": {
                "type": "integer",
                "description": "Saldo w groszach (precyzyjna kwota). Dostępne tylko dla klientów na D1 (prepaid/postpaid).",
                "example": 45075
              },
              "credit_limit_grosze": {
                "type": "integer",
                "description": "Limit kredytowy w groszach (dla postpaid: saldo może spaść do `-credit_limit_grosze`). 0 dla prepaid.",
                "example": 0
              },
              "currency": {
                "type": "string",
                "example": "PLN"
              },
              "billing_mode": {
                "type": "string",
                "enum": [
                  "prepaid",
                  "postpaid",
                  "legacy_proxy"
                ],
                "description": "Tryb rozliczeń. `prepaid` = saldo doładowywane przez Stripe; `postpaid` = faktura miesięczna z limitem kredytowym; `legacy_proxy` = proxy na saldo upstream (klienci sprzed migracji)."
              },
              "username": {
                "type": "string",
                "nullable": true,
                "description": "Tylko dla `legacy_proxy` — username konta upstream."
              }
            }
          }
        }
      },
      "MessageLog": {
        "type": "object",
        "description": "Rekord historii wysyłki z `/v1/messages`.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Nasze ID rekordu logu (prefiks `msg_`).",
            "example": "msg_a1b2c3d4e5f6789012345678"
          },
          "smsapi_id": {
            "type": "string",
            "nullable": true,
            "description": "ID wiadomości u upstream provider'a — przydatne do korelacji z DLR webhook'iem."
          },
          "type": {
            "type": "string",
            "enum": [
              "sms",
              "mms",
              "vms"
            ],
            "description": "Typ wiadomości."
          },
          "sender": {
            "type": "string",
            "nullable": true,
            "description": "Nadawca użyty przy wysyłce (z `from` w request lub default klienta)."
          },
          "recipient": {
            "type": "string",
            "description": "Numer odbiorcy (E.164).",
            "example": "+48600123456"
          },
          "message_preview": {
            "type": "string",
            "nullable": true,
            "description": "Pierwsze 160 znaków treści (RODO — nie zapisujemy pełnej treści w logu)."
          },
          "parts": {
            "type": "integer",
            "description": "Liczba części concatenated SMS."
          },
          "cost_grosze": {
            "type": "integer",
            "description": "Koszt wysyłki w groszach (dekrementowany z saldo dla prepaid).",
            "example": 5
          },
          "cost_pln": {
            "type": "number",
            "description": "Koszt w PLN (kompat: `cost_grosze / 100`).",
            "example": 0.05
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "not_found",
              "expired",
              "failed",
              "accepted",
              "unknown",
              "rejected",
              "undelivered"
            ],
            "description": "Status w momencie wysyłki (przed DLR)."
          },
          "dlr_status": {
            "type": "string",
            "nullable": true,
            "description": "Status z webhooka DLR (jeśli już przyszedł)."
          },
          "dlr_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Kiedy DLR doszedł."
          },
          "idx": {
            "type": "string",
            "nullable": true,
            "description": "Twój identyfikator z request body — pozwala skojarzyć rekord z encją w Twoim systemie."
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "description": "Kiedy SMS poszedł z naszej platformy do upstream."
          }
        }
      },
      "MessagesLogResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer",
                "description": "Liczba rekordów na bieżącej stronie.",
                "example": 50
              },
              "next_cursor": {
                "type": "integer",
                "nullable": true,
                "description": "Wartość do parametru `cursor` w następnym request. `null` = to ostatnia strona."
              },
              "messages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MessageLog"
                }
              }
            }
          }
        }
      },
      "DeliveryReportPayload": {
        "type": "object",
        "required": [
          "event",
          "message_id",
          "to",
          "status"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "delivery_report"
            ]
          },
          "message_id": {
            "type": "string",
            "description": "ID wiadomości z odpowiedzi `/v1/sms`."
          },
          "to": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "not_found",
              "expired",
              "failed",
              "accepted",
              "unknown",
              "rejected",
              "undelivered"
            ]
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "done_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "idx": {
            "type": "string",
            "nullable": true,
            "description": "Twój identyfikator z `/v1/sms` request."
          }
        }
      },
      "IncomingSmsPayload": {
        "type": "object",
        "required": [
          "event",
          "from",
          "to",
          "message"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "incoming_sms"
            ]
          },
          "message_id": {
            "type": "string"
          },
          "from": {
            "type": "string",
            "description": "Numer nadawcy."
          },
          "to": {
            "type": "string",
            "description": "Twój numer dwukierunkowy."
          },
          "message": {
            "type": "string",
            "description": "Treść SMS-a od odbiorcy."
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "description": "HTTP status code."
              },
              "message": {
                "type": "string",
                "description": "Czytelny komunikat błędu po polsku."
              },
              "details": {
                "description": "Opcjonalne dodatkowe informacje.",
                "nullable": true
              }
            }
          }
        }
      },
      "EmailRequest": {
        "type": "object",
        "required": [
          "to",
          "subject"
        ],
        "properties": {
          "to": {
            "oneOf": [
              {
                "type": "string",
                "format": "email"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                },
                "maxItems": 50
              }
            ],
            "description": "Pojedynczy adres email lub lista (max 50)."
          },
          "subject": {
            "type": "string",
            "maxLength": 200,
            "description": "Temat email."
          },
          "html": {
            "type": "string",
            "maxLength": 500000,
            "description": "Treść HTML. Wymagane: html lub text."
          },
          "text": {
            "type": "string",
            "maxLength": 500000,
            "description": "Treść plain text (fallback dla klientów bez HTML). Wymagane: html lub text."
          },
          "from": {
            "type": "string",
            "format": "email",
            "description": "Adres nadawcy. Domena musi być zweryfikowana w panel.przypominamy.com/settings/email. Jeśli pominięto, używamy domyślnej zweryfikowanej domeny klienta."
          },
          "from_name": {
            "type": "string",
            "maxLength": 100,
            "description": "Display name nadawcy (np. \"Sklep Twojafirma\")."
          },
          "reply_to": {
            "type": "string",
            "format": "email",
            "description": "Reply-To header."
          },
          "cc": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            },
            "maxItems": 20
          },
          "bcc": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            },
            "maxItems": 20
          },
          "idx": {
            "type": "string",
            "maxLength": 64,
            "description": "Twój identyfikator wiadomości (idempotency key). Zwracany w webhookach email_event."
          }
        }
      },
      "EmailResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "Resend email_id (UUID)."
              },
              "to": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "sent",
                  "delivered",
                  "bounced",
                  "complained",
                  "delayed",
                  "opened",
                  "clicked"
                ]
              },
              "cost": {
                "type": "number",
                "description": "Koszt w PLN (0,01 zł).",
                "example": 0.01
              },
              "sent_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "EmailEventPayload": {
        "type": "object",
        "required": [
          "event",
          "kind",
          "email_id"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "email_event"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "sent",
              "delivered",
              "bounced",
              "complained",
              "delayed",
              "opened",
              "clicked"
            ]
          },
          "email_id": {
            "type": "string"
          },
          "to": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "from": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "provider_payload": {
            "description": "Pełne raw dane z providera (Resend) — zawiera click URL, bounce reason itp."
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Brak lub nieprawidłowy token autoryzacji",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 401,
                "message": "Nieprawidłowy token autoryzacji"
              }
            }
          }
        }
      },
      "InsufficientFunds": {
        "description": "Niewystarczające środki na koncie",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 402,
                "message": "Niewystarczające środki na koncie"
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Błąd walidacji danych wejściowych",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 422,
                "message": "Pole \"to\" jest wymagane"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Przekroczono limit żądań (domyślnie 100/min). Sprawdź nagłówek `Retry-After`.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Sekundy do następnej dozwolonej próby."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 429,
                "message": "Przekroczono limit żądań. Spróbuj ponownie za chwilę."
              }
            }
          }
        }
      },
      "UpstreamError": {
        "description": "Błąd po stronie dostawcy SMS lub timeout (10s).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 502,
                "message": "Dostawca SMS nie odpowiada"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Konto zawieszone, zamknięte, lub brak dostępu do endpointu/zasobu",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "suspended": {
                "summary": "Konto zawieszone",
                "value": {
                  "error": {
                    "code": 403,
                    "message": "Konto zostało zawieszone. Skontaktuj się z support@przypominamy.com."
                  }
                }
              },
              "noAccess": {
                "summary": "Brak dostępu do endpointu (allowed_endpoints)",
                "value": {
                  "error": {
                    "code": 403,
                    "message": "Brak dostępu do endpointu: /v1/messages"
                  }
                }
              },
              "notMigrated": {
                "summary": "Konto legacy bez migracji do panelu",
                "value": {
                  "error": {
                    "code": 403,
                    "message": "Historia wysyłek dostępna po migracji konta do panelu klienta. Aktywacja: support@przypominamy.com."
                  }
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Wewnętrzny błąd platformy",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 500,
                "message": "Błąd odczytu historii wysyłek"
              }
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Funkcjonalność tymczasowo niedostępna (np. baza danych nie skonfigurowana)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": 503,
                "message": "Endpoint dostępny po włączeniu panelu klienta. Zaloguj się w app.przypominamy.com."
              }
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Strona główna API + dokumentacja czytelna",
    "url": "https://przypominamy.com/api"
  }
}