Апелляция по платежу (диспут)
Открывает спор по платежу, переведённому в статус failed: вы передаёте обоснование и вложение. В UI это может отображаться как диспут/апелляция.
Доступно только при ключе интеграции, привязанном к мерчанту; платёж в failed, окно ~24 ч после смены статуса, при назначенном трейдере (см. ответы 422 при нарушении условий).
POST https://api.protopays.io/api/v1/payments/{payment}/appeal
payment — числовой id записи external_payments (как в кабинете/API).
Заголовки запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
| Content-Type | multipart/form-data | да | Формируется клиентом при отправке FormData (boundary). |
| X-Merchant-Secret | string | да | Секрет ключа. |
| Signature | string (hex) | да | HMAC-SHA256 от канонической строки (см. ниже), не от сырого тела. |
| X-Merchant-Key-Id | string | да | Публичный key_id строки API-ключа в кабинете. |
Общая схема заголовков: аутентификация.
Подпись для multipart
Сырое тело multipart недоступно для повторного HMAC — подпись считается от канонической строки (UTF-8, переводы строк LF):
appeal-v1
{reason}
{originalFileName}
{sizeBytes}sizeBytes— размер файла в байтах, как у загружаемого файла.- HMAC-SHA256 канонической строки, hex; ключ — сырой секрет.
Поля формы
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
| reason | string | да | Текст причины, до 10000 символов. |
| file | file | да | Вложение: pdf, jpeg, jpg, png, webp, doc, docx; размер до лимита валидации на сервере. |
Пример (Node.js)
Сначала соберите каноническую строку с теми же reason, именем файла и размером, что пойдут в FormData.
import crypto from "node:crypto";
import fs from "node:fs";
const secret = "<SECRET>";
const paymentId = 1001;
const reason = "Клиент оплатил, статус failed ошибочно";
const filePath = "./receipt.pdf";
const buf = fs.readFileSync(filePath);
const originalName = "receipt.pdf";
const sizeBytes = String(buf.length);
const canonical = `appeal-v1\n${reason}\n${originalName}\n${sizeBytes}`;
const signature = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");
const form = new FormData();
form.append("reason", reason);
form.append("file", new File([buf], originalName, { type: "application/pdf" }));
const res = await fetch(`https://api.protopays.io/api/v1/payments/${paymentId}/appeal`, {
method: "POST",
headers: {
"X-Merchant-Secret": secret,
Signature: signature,
},
body: form,
});
console.log(res.status, await res.text());
Ответ 201
{
"message": "Appeal opened",
"data": {
"appealId": 1,
"appealPublicId": "…",
"paymentId": 1001,
"paymentStatus": "appeal",
"appealStatus": "open",
"createdAt": "2026-04-20T12:00:00+00:00"
}
}Ошибки
- 403 —
merchant_scope_required(нет привязки ключа к мерчанту). - 409 —
appeal_already_exists. - 422 — условия не выполнены (не
failed, срок, трейдер и т.д.).
Подробности в OpenAPI: /openapi/api-docs.json