Для разработчиков

Подключение оплаты подписки через Afinapay

Приложение выбирает тариф, серверная часть создаёт оплату через Afinapay, пользователь проходит страницу оплаты у подключённого платёжного провайдера и возвращается в приложение. Доступ открывается после подтверждения оплаты на серверной стороне.

  • 1. Приложение передаёт выбранный тариф в вашу серверную часть.
  • 2. Серверная часть создаёт оплату в Afinapay и получает ссылку.
  • 3. Пользователь оплачивает и возвращается в приложение по Universal/App Link.
  • 4. Серверная часть получает уведомление об оплате и открывает доступ.

Что настроить

  1. Откройте Магазины → нужный магазин → Интеграция.
  2. Сохраните URL-адрес для уведомлений, например https://app.merchant.example/api/webhooks/afinapay.
  3. Создайте Ключ для сервера и Секретный ключ.
  4. Сохраните оба значения на серверной стороне.
  5. Подготовьте стабильный идентификатор тарифа, например premium_access.
Ключ для сервера и секретный ключ храните только на серверной стороне.

Что делает приложение

  1. Пользователь нажимает кнопку подписки.
  2. Приложение вызывает вашу серверную часть.
  3. Серверная часть создаёт оплату и возвращает ссылку.
  4. Приложение открывает ровно redirectUrl или checkoutUrl из ответа.
  5. После возврата приложение показывает экран “Проверяем оплату”.
  6. Доступ открывается после ответа вашей серверной части.
Не пересобирайте ссылку вручную, не пропускайте её через GET form и не меняйте query string перед открытием.

Что сохранить у себя

  • externalOrderId как ключ вашего заказа.
  • externalCustomerId как ключ пользователя в вашем приложении.
  • Связь между externalOrderId, пользователем и локальным доступом.
  • order.id и paymentSession.id из ответа Afinapay для диагностики.

Что читать в уведомлении

  • Для оплаты: payload.order.externalOrderId.
  • Для подписки: payload.customer.externalCustomerId.
  • Резервно для подписки: payload.subscription.externalCustomerId.
  • Возвращайте 2xx только после того, как статус реально сохранён у вас.

Частые ошибки

  • Клиент открывает не тот URL, который вернул Afinapay.
  • Приложение или redirect helper очищает query string.
  • Обработчик отвечает 200, но не записывает новый статус.
  • Статусный endpoint читает другой ключ или другое хранилище.
  • Уведомление и экран результата работают в разных окружениях.

Примеры кода

Выберите язык и используйте готовые фрагменты для создания оплаты и приёма уведомлений.

Язык примеров

Создание оплаты

import express from 'express';

const app = express();
app.use(express.json());

app.post('/api/create-subscription-checkout', async (req, res) => {
  const response = await fetch('https://afinapay.ru/sdk/subscriptions/checkout-sessions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.AFINAPAY_TOKEN}`
    },
    body: JSON.stringify({
      externalPlan: {
        externalPlanKey: 'premium_access',
        name: 'Премиум-доступ',
        amount: 990,
        currency: 'RUB',
        billingPeriod: 'month',
        billingInterval: 1
      },
      externalOrderId: req.body.orderId,
      externalCustomerId: req.body.customerId,
      merchantReturnUrl: 'myapp://payments/subscription-result'
    })
  });

  const checkout = await response.json();
  res.status(response.status).json(checkout);
});

Проверка уведомления

import crypto from 'node:crypto';

app.post('/api/webhooks/afinapay', express.text({ type: '*/*' }), (req, res) => {
  const signatureHeader = req.header('x-afinapay-signature') ?? '';
  const timestamp = req.header('x-afinapay-signature-timestamp') ?? '';
  const signature = signatureHeader.startsWith('v1=') ? signatureHeader.slice(3) : '';
  const expected = crypto
    .createHmac('sha256', process.env.AFINAPAY_WEBHOOK_SECRET ?? '')
    .update(`${timestamp}.${req.body}`, 'utf8')
    .digest('hex');

  if (!signature || signature !== expected) {
    res.status(401).json({ error: 'invalid signature' });
    return;
  }

  const event = JSON.parse(req.body);

  if (event.type === 'subscription.activated') {
    // Откройте доступ пользователю на своей серверной стороне.
  }

  res.json({ ok: true });
});
Клиент открывает ровно redirectUrl или checkoutUrl из ответа. Сервер сохраняет связь между вашим пользователем и externalOrderId.

После возврата в приложение

  • Возврат по deep link нужен только для перехода пользователя обратно в приложение.
  • Экран после возврата должен показывать “Проверяем оплату”.
  • Приложение спрашивает статус у вашей серверной части.
  • Серверная часть подтверждает результат после уведомления об оплате или серверной проверки статуса.
  • Только после этого открывайте доступ пользователю.