Два сценария, которые покрывают 90% интеграции: создать счёт и принять webhook с проверкой подписи. Замените gp_live_xxx на ваш API-токен, а GRAMPAY_WEBHOOK_SECRET — на webhook-секрет проекта.
createInvoice: curl
curl -X POST https://app.grampaybot.com/api/createInvoice \
-H 'GramPay-API-Token: gp_live_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order_184' \
-d '{"amount":"10.00","public_description":"Доступ на 30 дней","payload":"order_184"}'
createInvoice: Node.js
const res = await fetch('https://app.grampaybot.com/api/createInvoice', {
method: 'POST',
headers: {
'GramPay-API-Token': process.env.GRAMPAY_TOKEN,
'Content-Type': 'application/json',
'Idempotency-Key': `order_${orderId}`,
},
body: JSON.stringify({
amount: '10.00',
public_description: 'Доступ на 30 дней',
payload: `order_${orderId}`,
}),
});
const data = await res.json();
if (!data.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.result.web_app_invoice_url);
createInvoice: Python
import os, requests
res = requests.post(
"https://app.grampaybot.com/api/createInvoice",
headers={
"GramPay-API-Token": os.environ["GRAMPAY_TOKEN"],
"Idempotency-Key": f"order_{order_id}",
},
json={
"amount": "10.00",
"public_description": "Доступ на 30 дней",
"payload": f"order_{order_id}",
},
timeout=10,
)
data = res.json()
if not data["ok"]:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["result"]["web_app_invoice_url"])
createInvoice: Go
body, _ := json.Marshal(map[string]string{
"amount": "10.00",
"public_description": "Доступ на 30 дней",
"payload": "order_" + orderID,
})
req, _ := http.NewRequest("POST", "https://app.grampaybot.com/api/createInvoice", bytes.NewReader(body))
req.Header.Set("GramPay-API-Token", os.Getenv("GRAMPAY_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "order_"+orderID)
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
var out struct {
Ok bool `json:"ok"`
Result struct {
WebAppInvoiceURL string `json:"web_app_invoice_url"`
} `json:"result"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&out)
if !out.Ok { log.Fatalf("%s: %s", out.Error.Code, out.Error.Message) }
fmt.Println(out.Result.WebAppInvoiceURL)
createInvoice: PHP
$ch = curl_init('https://app.grampaybot.com/api/createInvoice');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'GramPay-API-Token: ' . getenv('GRAMPAY_TOKEN'),
'Content-Type: application/json',
'Idempotency-Key: order_' . $orderId,
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => '10.00',
'public_description' => 'Доступ на 30 дней',
'payload' => 'order_' . $orderId,
]),
]);
$data = json_decode(curl_exec($ch), true);
if (!$data['ok']) {
throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
}
echo $data['result']['web_app_invoice_url'];
Проверка подписи webhook
Общий принцип на всех языках: HMAC-SHA256 от сырого тела запроса с webhook-секретом, сравнение константной по времени функцией с заголовком GramPay-Signature (формат sha256=<hex>).
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/grampay', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' +
crypto.createHmac('sha256', process.env.GRAMPAY_WEBHOOK_SECRET).update(req.body).digest('hex');
const given = req.headers['grampay-signature'] || '';
const valid = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!valid) return res.status(401).end();
const event = JSON.parse(req.body);
// обработка event.update_type / event.payload — идемпотентно
res.status(200).end();
});
Python (Flask)
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["GRAMPAY_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/grampay")
def grampay_webhook():
raw = request.get_data() # сырое тело, до парсинга
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("GramPay-Signature", ""), expected):
abort(401)
event = request.get_json()
# обработка события — идемпотентно
return "", 200
Go
func verify(rawBody []byte, header, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(header), []byte(expected))
}
func handler(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body)
if !verify(raw, r.Header.Get("GramPay-Signature"), os.Getenv("GRAMPAY_WEBHOOK_SECRET")) {
w.WriteHeader(http.StatusUnauthorized)
return
}
// json.Unmarshal(raw, &event) и идемпотентная обработка
w.WriteHeader(http.StatusOK)
}
PHP
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('GRAMPAY_WEBHOOK_SECRET'));
$given = $_SERVER['HTTP_GRAMPAY_SIGNATURE'] ?? '';
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// обработка события — идемпотентно
http_response_code(200);
Чек-лист перед продакшеном
Idempotency-Keyна каждомcreateInvoice— от дублей при таймаутах.- Проверка подписи от сырого тела, константное сравнение,
401при провале. webhook_testпринимается с2xx, но без бизнес-логики.- Обработка
invoice_paidидемпотентна: повторная доставка не выдаёт товар дважды. 402 account_not_serviceableобрабатывается: алерт, а не бесконечные ретраи.