From 83d9c1c7d0119872e0ea7a76725806ad2bf988eb Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Fri, 12 Jun 2026 22:12:57 +0330 Subject: [PATCH] fix(iab): correct Myket purchase verification to the documented POST /verify API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Myket's server-to-server validation is POST /api/partners/applications/{pkg}/purchases/products/{sku}/verify with the purchase token in the JSON body ({"tokenId": ...}) + X-Access-Token header — not a GET with the token in the path. purchaseState 0 = valid. Ref: https://myket.ir/kb/pages/server-to-server-payment-validation-api/ Co-Authored-By: Claude Opus 4.8 --- server/src/Hokm.Server/Payments/IabService.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/server/src/Hokm.Server/Payments/IabService.cs b/server/src/Hokm.Server/Payments/IabService.cs index 43ac2ff..36d8771 100644 --- a/server/src/Hokm.Server/Payments/IabService.cs +++ b/server/src/Hokm.Server/Payments/IabService.cs @@ -110,19 +110,26 @@ public sealed class IabService } /// - /// Myket: validate via the developer API (mirrors Google Play). The access - /// token comes from the Myket developer panel. + /// Myket: validate via the developer API. POST the purchase token in the body + /// (`{ "tokenId": ... }`) to the partners/verify endpoint with an X-Access-Token + /// header. The access token comes from the Myket developer panel → in-app + /// products. See https://myket.ir/kb/pages/server-to-server-payment-validation-api/ /// private async Task VerifyMyket(string productId, string token) { if (string.IsNullOrWhiteSpace(_opts.MyketAccessToken)) return _opts.AllowUnverified; - var url = $"https://developer.myket.ir/api/applications/{_opts.PackageName}/purchases/products/{Uri.EscapeDataString(productId)}/tokens/{Uri.EscapeDataString(token)}"; - using var req = new HttpRequestMessage(HttpMethod.Get, url); + var url = $"https://developer.myket.ir/api/partners/applications/{_opts.PackageName}/purchases/products/{Uri.EscapeDataString(productId)}/verify"; + using var req = new HttpRequestMessage(HttpMethod.Post, url); req.Headers.Add("X-Access-Token", _opts.MyketAccessToken); + req.Content = new StringContent( + JsonSerializer.Serialize(new { tokenId = token }), + System.Text.Encoding.UTF8, + "application/json"); var resp = await Http.SendAsync(req); if (!resp.IsSuccessStatusCode) return false; using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); + // purchaseState: 0 = successful purchase, 1 = failed. if (doc.RootElement.TryGetProperty("purchaseState", out var ps) && ps.ValueKind == JsonValueKind.Number) return ps.GetInt32() == 0; return true;