shiftpay.для разработчиков
На главнуюКлючи API
Содержание
SHIFTPAY API

От запроса
до оплаченного заказа.

Создайте платёж на своём сервере, откройте покупателю ссылку и получите подтверждение через подписанный вебхук.

Базовый адресhttps://api.shiftpay.cc/api/v1
  1. Подготовьте магазин

    Менеджер подключает магазин и способы оплаты. В кабинете откройте «Ещё → Интеграция», выпустите ключ с нужными правами и сохраните секрет на сервере. Он выдаётся один раз.

  2. Создайте платёж

    Отправьте подписанный POST с постоянным ключом идемпотентности для этой операции. Сохраните ID платежа рядом с заказом и передайте покупателю checkout_url.

  3. Подтвердите результат

    Проверьте подпись вебхука и сопоставьте платёж с заказом. Переход покупателя обратно на ваш сайт не является доказательством оплаты.

01 / БЕЗОПАСНОСТЬ

Подпись каждого запроса

API предназначен для запросов с вашего backend. Секрет нельзя встраивать в браузер, приложение покупателя или URL. Тело POST передаётся как UTF-8 JSON с Content-Type: application/json.

ЗаголовокЗначение
X-Project-IdID магазина из кабинета
X-Key-IdID активного API-ключа этого магазина
X-TimestampUnix time в секундах; допуск ±300 секунд
X-NonceНовая случайная строка для каждой попытки: 16–128 символов A–Z, a–z, 0–9, _ или -
X-Signaturev1= и hex HMAC-SHA256 подпись
Idempotency-KeyДля изменяющих POST: 8–128 символов A–Z, a–z, 0–9, _, ., :, -. Повтор операции использует тот же ключ.
Строка для HMAC-SHA256
METHOD
/api/v1/path?query=value
TIMESTAMP
NONCE
SHA256_HEX_OF_EXACT_BODY

Разделитель строк: \n. Метод в верхнем регистре, путь включает исходную query string. Для GET хешируется пустая строка. Сначала сформируйте байты тела, затем подпишите и отправьте именно их.

Первый запрос: чтение платежей, Node.js
Node.js · payments:read
import { createHash, createHmac, randomBytes } from "node:crypto";

const keyId = process.env.SHIFTPAY_KEY_ID;
const secret = process.env.SHIFTPAY_API_SECRET;
if (!keyId || !secret) throw new Error("Configure server-side API credentials");
const apiOrigin = process.env.SHIFTPAY_API_BASE || "https://api.shiftpay.cc";
if (!apiOrigin) throw new Error("Configure SHIFTPAY_API_BASE with your API origin");
const method = "GET";
const path = "/api/v1/payments?limit=1";
const body = "";
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomBytes(24).toString("hex");
const digest = createHash("sha256").update(body).digest("hex");
const canonical = [method, path, timestamp, nonce, digest].join("\n");
const signature = createHmac("sha256", secret).update(canonical).digest("hex");
const response = await fetch(new URL(path, apiOrigin), {
  method, redirect: "error", signal: AbortSignal.timeout(15000),
  headers: {
    "X-Project-Id": "YOUR_PROJECT_ID",
    "X-Key-Id": keyId, "X-Timestamp": timestamp,
    "X-Nonce": nonce, "X-Signature": "v1=" + signature
  }
});
console.log("HTTP status:", response.status);

Замените YOUR_PROJECT_ID на ID магазина. Ключ и секрет читаются из переменных окружения вашего сервера.

02 / ПЛАТЕЖИ

Создание платёжной ссылки

POST/paymentspayments:write

Сумма задаётся строкой. Комиссия и доля покупателя берутся из условий магазина; их нельзя переопределить произвольными полями в запросе. Не указывайте method, если способ выбирает покупатель.

JSON · пример тела запроса
{
  "amount": "100.00",
  "currency": "RUB",
  "merchant_order_id": "YOUR_ORDER_ID",
  "description": "Оплата заказа"
}
ПолеТипОписание
amountstring · обязательноПоложительная десятичная строка, например "100.00". Не число с плавающей точкой.
currencystring · обязательноВалюта суммы заказа. Доступность приёма зависит от магазина и провайдера.
merchant_order_idstring · обязательноНепустой ID заказа в вашей системе, до 128 символов.
methodstring | nullПодключённый способ оплаты. Не передавайте поле для общей формы с выбором покупателем.
descriptionstringОписание заказа, до 512 символов.
customerobjectНеобязательные строковые поля external_id, email, telegram_id.
metadataobjectДо 20 полей: строки, целые безопасные числа или boolean. Без вложенных объектов.
Полный пример создания оплаты, Node.js

Это пример реального изменяющего запроса. Он не выполняется на этой странице. Сначала настройте среду и сохраните ключ идемпотентности операции.

Node.js · payments:write
import { createHash, createHmac, randomBytes } from "node:crypto";

const { SHIFTPAY_PROJECT_ID: projectId, SHIFTPAY_KEY_ID: keyId,
  SHIFTPAY_API_SECRET: secret, SHIFTPAY_ORDER_ID: orderId,
  SHIFTPAY_IDEMPOTENCY_KEY: idempotencyKey } = process.env;
if (!projectId || !keyId || !secret || !orderId || !idempotencyKey)
  throw new Error("Configure credentials, order ID and a persistent idempotency key");

const path = "/api/v1/payments";
const body = JSON.stringify({ amount: "100.00", currency: "RUB",
  merchant_order_id: orderId, description: "Order payment" });
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomBytes(24).toString("hex");
const digest = createHash("sha256").update(body).digest("hex");
const canonical = ["POST", path, timestamp, nonce, digest].join("\n");
const signature = createHmac("sha256", secret).update(canonical).digest("hex");
const response = await fetch("https://api.shiftpay.cc" + path, {
  method: "POST", redirect: "error", signal: AbortSignal.timeout(15000),
  headers: { "Content-Type": "application/json", "X-Project-Id": projectId,
    "X-Key-Id": keyId, "X-Timestamp": timestamp, "X-Nonce": nonce,
    "X-Signature": "v1=" + signature, "Idempotency-Key": idempotencyKey },
  body,
});
if (!response.ok) {
  const problem = await response.json();
  throw new Error("HTTP " + response.status + ": " + problem.code);
}
const payment = await response.json();
// Store payment.id with your order. Redirect the buyer to payment.checkout_url.
// The order is not paid just because a checkout link was created. Record payment
// only after a verified payment.succeeded event and reconciliation with GET.

Ответ 201

Ответ содержит id, status, checkout_url, amount, currency, payer_total, merchant_amount, fee, merchant_order_id, expires_at и livemode. Ссылка живёт до expires_at. Создание ссылки не означает оплату или готовность всех провайдеров.

03 / СОСТОЯНИЕ

Когда заказ оплачен

Используйте GET /payments/{id} для актуального состояния. Событие успешного завершения: payment.succeeded, статус платежа: paid.

awaiting_method / pending
Выбор способа или ожидание оплаты.
detected / confirming
Поступление обнаружено, подтверждение или распределение ещё не закончено. Не выдавайте заказ как оплаченный.
paid
Оплата завершена в учёте. Деньги могут оставаться в hold или reserve; доступную сумму проверяйте через /balance.
underpaid / overpaid / manual_review / aml_hold
Требуется дополнительная обработка. Не приравнивайте эти состояния к paid.
expired / cancelled / failed
Срок истёк, платёж отменён или завершился ошибкой.
refund_pending / refunded
Возврат ожидает завершения или завершён.
04 / СПРАВОЧНИК

Методы API

Все пути ниже относительно базового адреса. Доступ ограничен магазином и средой ключа. Финансовые операции дополнительно проверяют права владельца ключа и условия магазина.

Метод и путьПрава и назначение
GET/payment-methodspayments:read

Методы магазина. Готовность провайдера дополнительно проверяется при открытии оплаты.

POST/paymentspayments:write

Создать платёж. Обязателен Idempotency-Key.

GET/paymentspayments:read

Список: limit 1–100 (по умолчанию 20), cursor, status. Ответ: data, has_more, next_cursor.

GET/payments/{id}payments:read

Актуальное состояние платежа.

POST/payments/{id}/cancelpayments:write

Отмена только awaiting_method или pending. Тело {} и Idempotency-Key.

GET/payments/{id}/eventspayments:read + webhooks:read

События и попытки доставки: limit, cursor.

GET/balancebalance:read

Баланс по валютам: available, hold, reserve, awaiting_conversion, withdrawal_pending, refund_pending.

GET/webhook-events/{id}webhooks:read

Событие по его ID в пределах вашего магазина.

POST / GET/refundsrefunds:write / refunds:read

Запросы на возврат и история. Создание заявки не означает, что провайдер уже вернул деньги.

GET/refunds/{id}refunds:read

Состояние конкретной заявки на возврат.

GET/withdrawals/termswithdrawals:read

Настроенные условия вывода. Публичная очередь заявок ограничена тестовой средой.

POST/withdrawals/quotewithdrawals:read

Расчёт: currency, network, amount. Не резервирует средства; ключ идемпотентности не требуется.

POST / GET/withdrawalswithdrawals:write / withdrawals:read

Создание заявки или список. Создание требует currency, network, amount, address, terms_revision и Idempotency-Key; note необязательно.

GET/withdrawals/{id}withdrawals:read

Заявка и согласования. approved не означает выполненную выплату.

05 / УВЕДОМЛЕНИЯ

Приём вебхуков

В кабинете «Интеграция» задайте HTTPS-адрес обработчика, сохраните отдельный секрет вебхуков и активируйте адрес до создания платежей. События, созданные до подключения обработчика, автоматически не досылаются.

Доставка выполняется как минимум один раз: возможны повторы и иной порядок событий. Заголовки ShiftPay-Event-ID и ShiftPay-Event идентифицируют событие; ShiftPay-Timestamp содержит Unix time; ShiftPay-Signature содержит v1=HMAC-SHA256(secret, timestamp + "." + raw_body).

Node.js · проверка подписи без изменения тела
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody is a Buffer containing the ORIGINAL request bytes, before JSON parsing.
// headers is a Headers object. Use the webhook secret, not the API key secret.
export function verifyWebhook(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== "string" || !secret) return false;
  const timestamp = headers.get("ShiftPay-Timestamp") ?? "";
  const signature = headers.get("ShiftPay-Signature") ?? "";
  if (!/^\d{10}$/.test(timestamp) || !/^v1=[a-fA-F0-9]{64}$/.test(signature))
    return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300)
    return false;
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".").update(rawBody).digest();
  const received = Buffer.from(signature.slice(3), "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}
// After verification, parse JSON and reconcile the event with your stored order.
// Deduplicate ShiftPay-Event-ID durably in the same transaction as the order update.
// Respond with 2xx only after that transaction commits. Replays must be harmless.

После проверки подписи сопоставьте платёж, магазин, валюту и сумму с вашим заказом. Сохраняйте обработанный ID события и изменение заказа атомарно. Повтор уже обработанного события должен безопасно возвращать 2xx, без повторной выдачи товара.

Повторы, адрес обработчика и журнал доставки

Нужен HTTPS на порту 443 с действительным сертификатом, без перенаправлений. Локальные адреса, IP вместо домена и небезопасные DNS-ответы не допускаются. Ответьте в пределах 10 секунд.

Повторы планируются через 10, 30, 60, 90, 180, 360, 480, 720, 1080, 1440, 2160, 2880 и 4320 минут от первой попытки. Пропущенные интервалы не выполняются одновременно. После исчерпания попыток требуется разбор ошибки и повтор из кабинета.

При повторе ID события и байты тела остаются прежними, timestamp и подпись обновляются. Историю смотрите в деталях платежа или GET /payments/{id}/events.

06 / ВОССТАНОВЛЕНИЕ

Ошибки и безопасные повторы

Ошибки возвращаются как application/problem+json с HTTP-статусом и полем code. Для обращения в поддержку сохраните X-Correlation-Id и идентификатор запроса из ответа. Не отправляйте секреты или подписи.

400 / 415 / 422
Проверьте формат JSON, строковую сумму, Content-Type, поля и доступность метода.
401
Проверьте ключ, часы сервера, исходные байты тела и путь. nonce_reused требует нового nonce и подписи, но не нового ключа идемпотентности.
403 / 404
Проверьте права, активность магазина и принадлежность объекта. Чужие платежи недоступны.
409
Конфликт состояния или другая команда под тем же ключом идемпотентности. Сначала прочитайте состояние заказа.
429
Снизьте частоту. Текущий лимит: 180 аутентифицированных запросов в минуту на ключ. Учитывайте Retry-After.
501 / 503
Операция ещё недоступна, не настроена или сервис временно недоступен. live_not_enabled означает, что боевой режим не включён.