Два сценария, которые покрывают 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 обрабатывается: алерт, а не бесконечные ретраи.

Следующий шаг

Первые 50 подтверждённых оплат — за наш счёт.

Получить $5 на старт ↗