Languages without an SDK¶
Integrating from a language other than Node or Go (PHP, Python, Java, C#, Ruby …) is straightforward: the API is plain HTTPS + JSON, authentication is HTTP Basic, and a webhook signature is one HMAC. With this page you write a small client and a webhook verifier in your own language — 30 to 60 minutes.
- You need: an API key of the app — a
client_id(gk_test_…/gk_live_…, or the «default key»'smapp_test_…/mapp_…) and its secret (gks_…/gma_cs_…), the webhook endpoint's secret (whsec_…), an HTTP client, and a crypto library with HMAC-SHA256.
1. The HTTP API in brief¶
| Item | Rule |
|---|---|
| Base URL | https://geregeapp.mn/api/v1/miniapp |
| Authentication | Authorization: Basic base64(client_id:secret) — the only way in. Every key has scopes: a key missing a route's scope gets 403 insufficient_scope with required_scope (the «default key» has every scope of its environment). Test and live keys are separate — only the key's environment is visible (livemode) |
| A mini app's person token | Authorization: Bearer gma_at_… (creating an app intent, /me) — launch and tokens |
| Body | Content-Type: application/json, UTF-8. Sending an unknown field answers 400 invalid_request — send only documented fields |
| Money | Whole tögrög (3500 = 3,500₮), currency is MNT |
| Time | RFC 3339, UTC (2026-10-22T09:00:00Z) |
Idempotency-Key |
Required to create an intent and to refund: 1–128 printable ASCII, no spaces. On a network error, 429 or 5xx retry with the same key |
| Errors | The module's {"error": "<Mongolian text>", "code": "<code>", …} or OAuth's {"error": "<code>", "error_description": "…"} — branch on the code (Error codes) |
| Pagination, limits | API conventions, Limits |
curl -s -u "$GEREGE_CLIENT_ID:$GEREGE_CLIENT_SECRET" \
-H "Content-Type: application/json" -H "Idempotency-Key: order_8812" \
-d '{"amount": 3500, "description": "Ус 0.5л", "reference": "order_8812", "presentment": "qr"}' \
https://geregeapp.mn/api/v1/miniapp/payment-intents
When writing the client:
- Reading an error: if there is a
codefield, that is the code anderroris a message you may show a person; if there is nocodebuterrorlooks like[a-z_]+(OAuth),erroris the code; if neither ({"error": "not found"},429,500), name the code after the HTTP status (http_404and so on) — exactly what the SDKs do. - Log the
X-Request-Idresponse header — Support will want it. - A 20–30 second timeout; retry with exponential backoff and the same
Idempotency-Key. - The client secret lives on servers only — never in a browser, a mobile app or a terminal (Security).
2. Verifying webhooks¶
The algorithm, the headers, the details and ready-made functions for Node, Go, Python and PHP are on
Verifying signatures. Java and C# below — the same logic (|now − t| ≤ 300, any v1,
constant time, lower-case hex):
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class GeregeWebhook {
public static boolean verify(byte[] rawBody, String header, List<String> secrets, long now) throws Exception {
long t = -1;
List<byte[]> sigs = new ArrayList<>();
for (String part : (header == null ? "" : header).split(",")) {
String p = part.trim();
int i = p.indexOf('=');
String key = i < 0 ? p : p.substring(0, i);
String value = i < 0 ? "" : p.substring(i + 1);
if (key.equals("t") && value.matches("[0-9]{1,18}")) t = Long.parseLong(value);
else if (key.equals("v1")) sigs.add(value.getBytes(StandardCharsets.US_ASCII));
}
if (t < 0 || sigs.isEmpty() || Math.abs(now - t) > 300) return false;
boolean ok = false;
for (String secret : secrets) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
mac.update((t + ".").getBytes(StandardCharsets.UTF_8));
byte[] want = HexFormat.of().formatHex(mac.doFinal(rawBody)).getBytes(StandardCharsets.US_ASCII);
for (byte[] sig : sigs) ok = MessageDigest.isEqual(sig, want) | ok;
}
return ok;
}
}
using System.Linq;
using System.Security.Cryptography;
using System.Text;
public static class GeregeWebhook
{
public static bool Verify(byte[] rawBody, string? header, IEnumerable<string> secrets, long now, long tolerance = 300)
{
long t = -1;
var sigs = new List<byte[]>();
foreach (var part in (header ?? "").Split(','))
{
var p = part.Trim();
var i = p.IndexOf('=');
var key = i < 0 ? p : p[..i];
var value = i < 0 ? "" : p[(i + 1)..];
if (key == "t" && value.Length is > 0 and <= 18 && value.All(char.IsAsciiDigit)) t = long.Parse(value);
else if (key == "v1") sigs.Add(Encoding.ASCII.GetBytes(value));
}
if (t < 0 || sigs.Count == 0 || Math.Abs(now - t) > tolerance) return false;
var ok = false;
foreach (var secret in secrets)
{
using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var message = Encoding.UTF8.GetBytes($"{t}.").Concat(rawBody).ToArray();
var want = Encoding.ASCII.GetBytes(Convert.ToHexString(mac.ComputeHash(message)).ToLowerInvariant());
foreach (var sig in sigs) ok = CryptographicOperations.FixedTimeEquals(sig, want) | ok;
}
return ok;
}
}
Take the body as bytes before your framework parses it (Spring: @RequestBody byte[]; ASP.NET Core:
Request.EnableBuffering() and copy Request.Body into a MemoryStream).
3. Testing your code¶
- The test vectors generated from the platform's own code (
vectors.json: 20 signing and verification cases) ship with the SDKs. Until the SDKs are published, get the file from Support; load it in your unit tests and check that eachwebhook.verification[]case matches itsvalid(example). - Without the vectors: the secret,
tand body of the «worked example» on Verifying signatures must giveaab41441…c9821. - With a real delivery: the console's «Send test» (
webhook.test) — through a tunnel to your machine (Local webhooks).