Содержание
От запроса
до оплаченного заказа.
Создайте платёж на своём сервере, откройте покупателю ссылку и получите подтверждение через подписанный вебхук.
https://api.shiftpay.cc/api/v1- Подготовьте магазин
Менеджер подключает магазин и способы оплаты. В кабинете откройте «Ещё → Интеграция», выпустите ключ с нужными правами и сохраните секрет на сервере. Он выдаётся один раз.
- Создайте платёж
Отправьте подписанный POST с постоянным ключом идемпотентности для этой операции. Сохраните ID платежа рядом с заказом и передайте покупателю checkout_url.
- Подтвердите результат
Проверьте подпись вебхука и сопоставьте платёж с заказом. Переход покупателя обратно на ваш сайт не является доказательством оплаты.
Подпись каждого запроса
API предназначен для запросов с вашего backend. Секрет нельзя встраивать в браузер, приложение покупателя или URL. Тело POST передаётся как UTF-8 JSON с Content-Type: application/json.
| Заголовок | Значение |
|---|---|
X-Project-Id | ID магазина из кабинета |
X-Key-Id | ID активного API-ключа этого магазина |
X-Timestamp | Unix time в секундах; допуск ±300 секунд |
X-Nonce | Новая случайная строка для каждой попытки: 16–128 символов A–Z, a–z, 0–9, _ или - |
X-Signature | v1= и hex HMAC-SHA256 подпись |
Idempotency-Key | Для изменяющих POST: 8–128 символов A–Z, a–z, 0–9, _, ., :, -. Повтор операции использует тот же ключ. |
METHOD
/api/v1/path?query=value
TIMESTAMP
NONCE
SHA256_HEX_OF_EXACT_BODYРазделитель строк: \n. Метод в верхнем регистре, путь включает исходную query string. Для GET хешируется пустая строка. Сначала сформируйте байты тела, затем подпишите и отправьте именно их.
Первый запрос: чтение платежей, Node.js
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 магазина. Ключ и секрет читаются из переменных окружения вашего сервера.
Создание платёжной ссылки
/paymentspayments:writeСумма задаётся строкой. Комиссия и доля покупателя берутся из условий магазина; их нельзя переопределить произвольными полями в запросе. Не указывайте method, если способ выбирает покупатель.
{
"amount": "100.00",
"currency": "RUB",
"merchant_order_id": "YOUR_ORDER_ID",
"description": "Оплата заказа"
}| Поле | Тип | Описание |
|---|---|---|
amount | string · обязательно | Положительная десятичная строка, например "100.00". Не число с плавающей точкой. |
currency | string · обязательно | Валюта суммы заказа. Доступность приёма зависит от магазина и провайдера. |
merchant_order_id | string · обязательно | Непустой ID заказа в вашей системе, до 128 символов. |
method | string | null | Подключённый способ оплаты. Не передавайте поле для общей формы с выбором покупателем. |
description | string | Описание заказа, до 512 символов. |
customer | object | Необязательные строковые поля external_id, email, telegram_id. |
metadata | object | До 20 полей: строки, целые безопасные числа или boolean. Без вложенных объектов. |
Полный пример создания оплаты, Node.js
Это пример реального изменяющего запроса. Он не выполняется на этой странице. Сначала настройте среду и сохраните ключ идемпотентности операции.
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. Создание ссылки не означает оплату или готовность всех провайдеров.
Когда заказ оплачен
Используйте 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- Возврат ожидает завершения или завершён.
Методы API
Все пути ниже относительно базового адреса. Доступ ограничен магазином и средой ключа. Финансовые операции дополнительно проверяют права владельца ключа и условия магазина.
| Метод и путь | Права и назначение |
|---|---|
GET/payment-methods | payments:readМетоды магазина. Готовность провайдера дополнительно проверяется при открытии оплаты. |
POST/payments | payments:writeСоздать платёж. Обязателен Idempotency-Key. |
GET/payments | payments:readСписок: limit 1–100 (по умолчанию 20), cursor, status. Ответ: data, has_more, next_cursor. |
GET/payments/{id} | payments:readАктуальное состояние платежа. |
POST/payments/{id}/cancel | payments:writeОтмена только awaiting_method или pending. Тело {} и Idempotency-Key. |
GET/payments/{id}/events | payments:read + webhooks:readСобытия и попытки доставки: limit, cursor. |
GET/balance | balance:readБаланс по валютам: available, hold, reserve, awaiting_conversion, withdrawal_pending, refund_pending. |
GET/webhook-events/{id} | webhooks:readСобытие по его ID в пределах вашего магазина. |
POST / GET/refunds | refunds:write / refunds:readЗапросы на возврат и история. Создание заявки не означает, что провайдер уже вернул деньги. |
GET/refunds/{id} | refunds:readСостояние конкретной заявки на возврат. |
GET/withdrawals/terms | withdrawals:readНастроенные условия вывода. Публичная очередь заявок ограничена тестовой средой. |
POST/withdrawals/quote | withdrawals:readРасчёт: currency, network, amount. Не резервирует средства; ключ идемпотентности не требуется. |
POST / GET/withdrawals | withdrawals:write / withdrawals:readСоздание заявки или список. Создание требует currency, network, amount, address, terms_revision и Idempotency-Key; note необязательно. |
GET/withdrawals/{id} | withdrawals:readЗаявка и согласования. approved не означает выполненную выплату. |
Приём вебхуков
В кабинете «Интеграция» задайте HTTPS-адрес обработчика, сохраните отдельный секрет вебхуков и активируйте адрес до создания платежей. События, созданные до подключения обработчика, автоматически не досылаются.
Доставка выполняется как минимум один раз: возможны повторы и иной порядок событий. Заголовки ShiftPay-Event-ID и ShiftPay-Event идентифицируют событие; ShiftPay-Timestamp содержит Unix time; ShiftPay-Signature содержит v1=HMAC-SHA256(secret, timestamp + "." + raw_body).
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.
Ошибки и безопасные повторы
Ошибки возвращаются как 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 означает, что боевой режим не включён.