openapi: 3.1.0 info: title: TextMeFlow API version: "0.1.0" summary: Self-hosted WhatsApp messaging API for the Fades Management product family. description: | TextMeFlow is a drop-in replacement for TextMeBot, plus a modern REST API. ## Authentication All `/v1/*` endpoints (except `POST /v1/accounts` and the Stripe webhook) require a Bearer token: ``` Authorization: Bearer tmf_ ``` The legacy `GET /send.php` endpoint accepts the api key as the `?apikey=…` query parameter for TextMeBot drop-in compatibility. servers: - url: https://api.textmeflow.eu description: Production - url: http://localhost:3000 description: Local tags: - name: Compat description: TextMeBot drop-in compatibility endpoints. - name: Accounts description: Account, plan and pairing flows. - name: Messages description: Send + status of outbound messages. - name: Webhooks description: Inbound webhooks from Stripe. - name: Admin description: Super-admin (HTTP Basic auth). paths: /healthz: get: summary: Liveness probe. tags: [] responses: "200": { description: ok } /send.php: get: tags: [Compat] summary: TextMeBot drop-in send. description: | Mirrors `https://api.textmebot.com/send.php`. Returns an HTML body containing `Result: Success` or `Result: Failed`. Existing BedFlow code that does substring-matching on that string keeps working unmodified. parameters: - in: query name: recipient required: true schema: { type: string, example: "+32472932208" } - in: query name: apikey required: true schema: { type: string } - in: query name: text required: true schema: { type: string } responses: "200": description: TextMeBot-formatted plain-HTML body. content: text/html: schema: { type: string } /v1/accounts: post: tags: [Accounts] summary: Register a new account. requestBody: required: true content: application/json: schema: type: object required: [email, plan_code] properties: email: { type: string, format: email } plan_code: type: string enum: [free, starter, pro, business, enterprise] interval: type: string enum: [monthly, yearly] default: monthly responses: "201": description: Created. content: application/json: schema: type: object properties: account_id: { type: string, format: uuid } api_key: { type: string } plan: { type: string } checkout_url: type: [string, "null"] description: Present for paid plans, null for free. trial_ends_at: { type: [string, "null"], format: date-time } /v1/accounts/me: get: tags: [Accounts] summary: Current account profile + usage. security: [{ bearerAuth: [] }] responses: "200": description: Account data. /v1/accounts/me/pair: post: tags: [Accounts] summary: Start a QR-pairing flow. security: [{ bearerAuth: [] }] requestBody: content: application/json: schema: type: object properties: label: { type: string } responses: "201": description: | Returns `qr_png_b64` (data URI) + `pair_id`. Poll `/v1/accounts/me/pair/{pair_id}/status` to see when WhatsApp confirms the link. /v1/accounts/me/pair/{pair_id}/status: get: tags: [Accounts] summary: Poll the pairing status. security: [{ bearerAuth: [] }] parameters: - in: path name: pair_id required: true schema: { type: string, format: uuid } responses: "200": description: pending | connected | expired | failed /v1/accounts/me/session: delete: tags: [Accounts] summary: Log out of WhatsApp Web on this account. security: [{ bearerAuth: [] }] responses: "200": { description: Disconnected. } /v1/accounts/me/billing-portal: get: tags: [Accounts] summary: Stripe customer-portal URL for self-service. security: [{ bearerAuth: [] }] responses: "200": description: Portal URL. /v1/messages: post: tags: [Messages] summary: Queue an outbound message. security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [to, text] properties: to: type: string description: E.164 phone number (must start with `+`). example: "+32472932208" text: { type: string } media_url: { type: string, format: uri } urgent: type: boolean description: Bypass quiet-hours shaping. Use sparingly. default: false responses: "202": description: Queued. content: application/json: schema: type: object properties: message_id: { type: string, format: uuid } status: { type: string, enum: [queued] } "429": description: Quota or rate limit exceeded. /v1/messages/{id}: get: tags: [Messages] summary: Fetch a message by id. security: [{ bearerAuth: [] }] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": description: Message record. /v1/webhooks/stripe: post: tags: [Webhooks] summary: Stripe webhook receiver. description: | Validates the `Stripe-Signature` header. All other auth bypassed — signed payload is the credential. responses: "200": description: received /admin/accounts: get: tags: [Admin] summary: List accounts (filterable). security: [{ basicAuth: [] }] parameters: - in: query name: status schema: { type: string, enum: [active, suspended, cancelled, past_due] } - in: query name: plan schema: { type: string } - in: query name: q schema: { type: string } responses: "200": { description: ok } /admin/accounts/{id}/suspend: post: tags: [Admin] summary: Suspend an account. security: [{ basicAuth: [] }] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": { description: suspended } /admin/accounts/{id}/reinstate: post: tags: [Admin] summary: Reinstate a suspended account. security: [{ basicAuth: [] }] parameters: - in: path name: id required: true schema: { type: string, format: uuid } responses: "200": { description: active } /admin/metrics: get: tags: [Admin] summary: Prometheus exposition. security: [{ basicAuth: [] }] responses: "200": description: text/plain Prometheus metrics. content: text/plain: schema: { type: string } components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: tmf_ basicAuth: type: http scheme: basic