diff --git a/.env.example b/.env.example
index 793313e..ef35eef 100644
--- a/.env.example
+++ b/.env.example
@@ -83,6 +83,14 @@ SEED_ADMIN_PASSWORD=change-me-strong-admin-password
ZARINPAL_MERCHANT_ID=
ZARINPAL_SANDBOX=false
+# ── Payment: FlatRender Pay (ZarinPal broker) ─────────────────────────────────
+# Broker keys from the FlatRender dashboard. Webhook is registered at the broker as
+# https://api.meezi.ir/api/payment/webhook. Keep the live secret OUT of git.
+FLATPAY_API_KEY=
+FLATPAY_SECRET=
+FLATPAY_BASE_URL=https://pay.flatrender.ir
+FLATPAY_RETURN_URL=https://meezi.ir/payment/return
+
# ── SMS: Kavenegar ────────────────────────────────────────────────────────────
# Empty = OTP is logged to API console (fine for dev, not for production)
KAVENEGAR_API_KEY=4C30786935496261332B41685870444E47657A5367453369374F6E2F43334672576B526F5A4B4B795665493D
diff --git a/docker-compose.yml b/docker-compose.yml
index 7741582..6984262 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -94,6 +94,10 @@ services:
Snappfood__WebhookSecret: "${SNAPPFOOD_WEBHOOK_SECRET:-meezi-dev-snappfood-secret}"
ZarinPal__MerchantId: "${ZARINPAL_MERCHANT_ID:-}"
ZarinPal__Sandbox: "${ZARINPAL_SANDBOX:-true}"
+ FlatPay__ApiKey: "${FLATPAY_API_KEY:-}"
+ FlatPay__Secret: "${FLATPAY_SECRET:-}"
+ FlatPay__BaseUrl: "${FLATPAY_BASE_URL:-https://pay.flatrender.ir}"
+ FlatPay__ReturnUrl: "${FLATPAY_RETURN_URL:-https://meezi.ir/payment/return}"
Seed__SystemAdminPhone: "${SEED_ADMIN_PHONE:-}"
Seed__SystemAdminUsername: "${SEED_ADMIN_USERNAME:-admin}"
Seed__SystemAdminPassword: "${SEED_ADMIN_PASSWORD:-}"
diff --git a/src/Meezi.API/Controllers/PaymentController.cs b/src/Meezi.API/Controllers/PaymentController.cs
new file mode 100644
index 0000000..9a9df8c
--- /dev/null
+++ b/src/Meezi.API/Controllers/PaymentController.cs
@@ -0,0 +1,129 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Meezi.API.Models.Payments;
+using Meezi.API.Services;
+using Meezi.API.Services.Payments;
+using Meezi.Core.Authorization;
+using Meezi.Core.Enums;
+using Meezi.Core.Interfaces;
+using Meezi.Shared;
+
+namespace Meezi.API.Controllers;
+
+/// FlatRender Pay (ZarinPal broker) checkout + webhook.
+[ApiController]
+public class PaymentController : CafeApiControllerBase
+{
+ private readonly IBillingService _billing;
+ private readonly IFlatPayService _flatPay;
+ private readonly ILogger _logger;
+
+ public PaymentController(IBillingService billing, IFlatPayService flatPay, ILogger logger)
+ {
+ _billing = billing;
+ _flatPay = flatPay;
+ _logger = logger;
+ }
+
+ /// Start a FlatPay checkout for a plan bundle; returns the URL to redirect the buyer to.
+ [Authorize]
+ [HttpPost("api/payment/request")]
+ public async Task CreatePayment(
+ [FromBody] PaymentRequestDto request,
+ ITenantContext tenant,
+ CancellationToken ct)
+ {
+ if (EnsurePermission(tenant, Permission.ManageBilling) is { } permDenied) return permDenied;
+ if (string.IsNullOrEmpty(tenant.CafeId)) return Unauthorized();
+
+ if (request?.ProductId is null || !TryParseProduct(request.ProductId, out var tier, out var months))
+ return BadRequest(new ApiResponse