Route payment bank callbacks through storefront domains.

ZarinPal rejects api.meshkee.com when the terminal is registered to the store apex; nginx now proxies /meshkee/payments/{gateway}/callback to a host-scoped API route.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-25 21:34:56 +03:30
co-authored by Cursor
parent 40284df880
commit e942348b1d
13 changed files with 514 additions and 22 deletions
+2 -1
View File
@@ -36,7 +36,8 @@ You are building a **Meshkee business website (storefront)**. You must use the M
5. Cart checkout with `addressId` or inline `shippingAddress` + `payment`
- For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`
- Response includes `payment.redirect` `{ method, url, fields }` — POST/redirect shopper to the bank
- Bank callback hits API then redirects to `returnUrl?status=success|failed&orderId=…`
- Meshkee registers the bank `callback_url` on the **store apex** (`https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback`), which nginx proxies to the API. ZarinPal/Mellat domain checks must match the store domain, not `api.meshkee.com`.
- After verify, API redirects the browser to `returnUrl?status=success|failed&orderId=…`
- Enabled gateways: `GET /tenants/{domain}` → `ePayment`, or `GET /businesses/{businessId}/payments/methods`
### User products (customer listings)
@@ -1705,7 +1705,7 @@
"raw": "{\n \"addressId\": \"{{addressId}}\",\n \"customerNotes\": \"Deliver after 5pm\",\n \"returnUrl\": \"https://YOUR_WEBSITE_DOMAIN/checkout/result\",\n \"payment\": {\n \"type\": \"e_payment_gate\",\n \"gatewayType\": \"mellat\"\n }\n}"
},
"url": "{{baseUrl}}/businesses/{{businessId}}/cart/checkout",
"description": "Creates a pending order + transaction, calls the gateway, and returns payment.redirect { method, url, fields }. Website must POST/redirect the shopper to the bank. After payment, bank hits /payments/{gateway}/callback which redirects to returnUrl?status=success|failed."
"description": "Creates a pending order + transaction, calls the gateway, and returns payment.redirect { method, url, fields }. Website must POST/redirect the shopper to the bank. After payment, the bank hits https://YOUR_WEBSITE_DOMAIN/meshkee/payments/{gateway}/callback (nginx → API), which redirects to returnUrl?status=success|failed."
}
}
]
@@ -1723,7 +1723,64 @@
}
},
{
"name": "Mellat callback (bank → API)",
"name": "Mellat callback via store domain (gateway → nginx → API)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "ResCode",
"value": "0"
},
{
"key": "SaleOrderId",
"value": "{{transactionId}}"
},
{
"key": "SaleReferenceId",
"value": "123456789"
},
{
"key": "RefId",
"value": "{{refId}}"
}
]
},
"url": "https://YOUR_WEBSITE_DOMAIN/meshkee/payments/mellat/callback",
"description": "Preferred path: bank posts to the store apex; nginx proxies to /tenants/{host}/payments/mellat/callback. Verifies+settles, then 303 redirect to returnUrl."
}
},
{
"name": "ZarinPal callback via store domain (gateway → nginx → API)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://YOUR_WEBSITE_DOMAIN/meshkee/payments/zarinpal/callback?Authority={{authority}}&Status=OK",
"host": ["YOUR_WEBSITE_DOMAIN"],
"path": [
"meshkee",
"payments",
"zarinpal",
"callback"
],
"query": [
{ "key": "Authority", "value": "{{authority}}" },
{ "key": "Status", "value": "OK" }
]
},
"description": "Preferred path: ZarinPal redirects to the store apex; nginx proxies to /tenants/{host}/payments/zarinpal/callback. Verifies payment, then 303 redirect to returnUrl."
}
},
{
"name": "Mellat callback (legacy API URL)",
"request": {
"method": "POST",
"header": [
@@ -1754,11 +1811,11 @@
]
},
"url": "{{baseUrl}}/businesses/{{businessId}}/payments/mellat/callback",
"description": "Called by Mellat (not by the website). Verifies+settles, then 303 redirect to returnUrl."
"description": "Legacy direct API callback. Prefer store-domain /meshkee/payments/... so gateway domain checks match the merchant terminal."
}
},
{
"name": "ZarinPal callback (gateway → API)",
"name": "ZarinPal callback (legacy API URL)",
"request": {
"method": "GET",
"header": [],
@@ -1777,7 +1834,7 @@
{ "key": "Status", "value": "OK" }
]
},
"description": "Called by ZarinPal (not by the website). Verifies payment, then 303 redirect to returnUrl."
"description": "Legacy direct API callback. Prefer store-domain /meshkee/payments/... so ZarinPal domain checks match the merchant terminal."
}
}
]
+93 -2
View File
@@ -3210,7 +3210,8 @@
"tags": [
"Payments"
],
"summary": "Bank payment callback (public; redirects browser to returnUrl)",
"summary": "Bank payment callback — legacy direct API URL (prefer store-domain path)",
"description": "Still supported. Prefer https://{store}/meshkee/payments/{gateway}/callback so gateway domain checks match the merchant terminal.",
"parameters": [
{
"$ref": "#/components/parameters/businessId"
@@ -3255,7 +3256,7 @@
"tags": [
"Payments"
],
"summary": "Bank payment callback (GET variant)",
"summary": "Bank payment callback GET — legacy direct API URL",
"parameters": [
{
"$ref": "#/components/parameters/businessId"
@@ -3282,6 +3283,96 @@
}
}
}
},
"/tenants/{host}/payments/{gateway}/callback": {
"post": {
"tags": [
"Payments"
],
"summary": "Bank payment callback via store host (nginx proxies /meshkee/payments/... here)",
"description": "Public. Resolves business from host. Used after storefront nginx proxies https://{store}/meshkee/payments/{gateway}/callback.",
"parameters": [
{
"name": "host",
"in": "path",
"required": true,
"schema": {
"type": "string",
"example": "YOUR_WEBSITE_DOMAIN"
}
},
{
"name": "gateway",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
]
}
}
],
"requestBody": {
"required": true,
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"responses": {
"303": {
"description": "Redirect to checkout returnUrl with status=success|failed"
}
}
},
"get": {
"tags": [
"Payments"
],
"summary": "Bank payment callback GET via store host (ZarinPal)",
"parameters": [
{
"name": "host",
"in": "path",
"required": true,
"schema": {
"type": "string",
"example": "YOUR_WEBSITE_DOMAIN"
}
},
{
"name": "gateway",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
]
}
}
],
"responses": {
"303": {
"description": "Redirect to checkout returnUrl with status=success|failed"
}
}
}
}
}
}