На странице
Создать
Оформите полный возврат или возврат переплаты по оплаченному инвойсу обратно на адрес первого подтверждённого отправителя.
API-ключ: Payment
Создаёт ончейн-возврат по инвойсу в статусе paid или paid_over в течение 14 дней с момента последнего подтверждённого депозита. Адрес получателя и сумма рассчитываются сервером автоматически: средства возвращаются на адрес отправителя первого подтверждённого депозита (first_sender), а с баланса мерчанта списывается amount + fee.
Тело запроса
| Параметр | Тип | Обязателен | Описание |
|---|---|---|---|
invoice_id |
string | Да | Идентификатор инвойса с префиксом (inv_...), принадлежащего проекту в области видимости ключа |
mode |
string | Да | "full" (возвращает весь оставшийся доступный к возврату баланс инвойса) или "overpayment" (возвращает только сумму переплаты сверх запрошенной суммы инвойса) |
external_order_id |
string | Да | Ваш ключ идемпотентности (до 200 символов), уникальный в рамках мерчанта и окружения |
Пример запроса
{
"invoice_id": "inv_7K2M9P4Q8R1X5Z3A0bC2dE",
"mode": "full",
"external_order_id": "refund-1042-full"
}
Примеры кода
Каждая вкладка использует официальный SDK с платёжным ключом (pk_). Подпись запроса, сериализация, безопасные повторы и типизированные ошибки обрабатываются внутри SDK.
API_KEY_ID="pk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
BODY='{"invoice_id":"inv_7K2M9P4Q8R1X5Z3A0bC2dE","mode":"full","external_order_id":"refund-1042-full"}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/refunds '' "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
curl -sS https://api.paymos.io/v1/refunds \
-H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
-H "X-Request-Timestamp: $TS" \
-H "Content-Type: application/json" \
-d "$BODY"
import { Paymos, externalOrderId } from '@paymos/sdk';
const paymos = new Paymos({
apiKey: process.env.PAYMOS_API_KEY,
apiSecret: process.env.PAYMOS_API_SECRET,
});
const refund = await paymos.refunds.create({
invoiceId: 'inv_7K2M9P4Q8R1X5Z3A0bC2dE',
mode: 'full',
externalOrderId: externalOrderId('refund'),
});
console.log(refund.withdrawalId, refund.status);
import os
from paymos import Paymos
paymos = Paymos(
api_key=os.environ["PAYMOS_API_KEY"],
api_secret=os.environ["PAYMOS_API_SECRET"],
)
refund = paymos.refunds.create(
invoice_id="inv_7K2M9P4Q8R1X5Z3A0bC2dE",
mode="full",
external_order_id="refund-1042-full",
)
print(refund["withdrawal_id"], refund["status"])
<?php
use Paymos\Client;
use Paymos\ClientConfig;
use Paymos\IdempotencyKey;
$paymos = new Client(new ClientConfig(
getenv('PAYMOS_API_KEY'),
getenv('PAYMOS_API_SECRET')
));
$refund = $paymos->refunds()->create(array(
'invoice_id' => 'inv_7K2M9P4Q8R1X5Z3A0bC2dE',
'mode' => 'full',
'external_order_id' => IdempotencyKey::externalOrderId('refund'),
));
echo $refund['withdrawal_id'] . ' ' . $refund['status'];
package main
import (
"context"
"fmt"
"os"
paymos "github.com/Paymos-labs/go-sdk/v2"
)
func main() {
client, err := paymos.NewClient(os.Getenv("PAYMOS_API_KEY"), os.Getenv("PAYMOS_API_SECRET"))
if err != nil {
panic(err)
}
refund, err := client.Refunds.Create(context.Background(), paymos.CreateRefundParams{
InvoiceID: "inv_7K2M9P4Q8R1X5Z3A0bC2dE",
Mode: paymos.RefundModeFull,
ExternalOrderID: "refund-1042-full",
})
if err != nil {
panic(err)
}
fmt.Println(refund.WithdrawalID, refund.Status)
}
using Paymos;
using var paymos = new PaymosClient(
Environment.GetEnvironmentVariable("PAYMOS_API_KEY")!,
Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);
var refund = await paymos.Refunds.CreateAsync(new CreateRefundRequest(
InvoiceId: "inv_7K2M9P4Q8R1X5Z3A0bC2dE",
Mode: RefundMode.Full,
ExternalOrderId: "refund-1042-full"));
Console.WriteLine($"{refund.WithdrawalId} {refund.Status}");
import io.paymos.CreateRefundRequest;
import io.paymos.PaymosClient;
import io.paymos.Refund;
import io.paymos.RefundMode;
PaymosClient paymos = new PaymosClient(
System.getenv("PAYMOS_API_KEY"),
System.getenv("PAYMOS_API_SECRET"));
Refund refund = paymos.refunds.create(new CreateRefundRequest(
"inv_7K2M9P4Q8R1X5Z3A0bC2dE",
RefundMode.FULL,
"refund-1042-full"));
System.out.println(refund.withdrawalId() + " " + refund.status());
require 'paymos'
paymos = Paymos::Client.new(
api_key: ENV.fetch('PAYMOS_API_KEY'),
api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
refund = paymos.refunds.create(
invoice_id: 'inv_7K2M9P4Q8R1X5Z3A0bC2dE',
mode: 'full',
external_order_id: 'refund-1042-full'
)
puts "#{refund.withdrawal_id} #{refund.status}"
use paymos::{CreateRefundRequest, PaymosClient, RefundMode};
let paymos = PaymosClient::new(
std::env::var("PAYMOS_API_KEY")?,
std::env::var("PAYMOS_API_SECRET")?,
)?;
let refund = paymos
.refunds()
.create(&CreateRefundRequest {
invoice_id: "inv_7K2M9P4Q8R1X5Z3A0bC2dE".to_owned(),
mode: RefundMode::Full,
external_order_id: "refund-1042-full".to_owned(),
})
.await?;
println!("{} {}", refund.withdrawal_id, refund.status);
Ответ (201 Created / 200 при идемпотентном совпадении)
{
"withdrawal_id": "wdr_6Qm2fK8xR4vN7cT1bY9dL5",
"invoice_id": "inv_7K2M9P4Q8R1X5Z3A0bC2dE",
"mode": "full",
"external_order_id": "refund-1042-full",
"status": "created",
"is_final": false,
"is_test": false,
"amount": "100.00",
"fee": "1.00",
"currency": "USDT",
"network": "TRC20",
"destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"first_sender": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"created_at": 1767225600,
"type": "refund",
"project_id": "prj_3F9xL2mN8pQ4rT6vW1yZ5b"
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
withdrawal_id |
string | Идентификатор выплаты с префиксом (wdr_...), представляющий данный возврат |
invoice_id |
string | Идентификатор инвойса (inv_...), по которому оформлен возврат |
mode |
string | Режим возврата: "full" или "overpayment" |
external_order_id |
string | Ваш идентификатор операции, переданный при создании запроса |
status |
string | Текущий статус (created, signed, pending_review, cancelling, completed, failed или cancelled) |
is_final |
boolean | true, когда возврат перешёл в финальный статус (completed, failed, cancelled) |
is_test |
boolean | true для возвратов в песочнице |
amount |
string | Точная сумма токенов, возвращаемая плательщику |
fee |
string? | Фиксированная сетевая комиссия за отправку токена, списываемая с баланса мерчанта сверх amount. Опускается, если равна нулю |
currency |
string | Символ криптоактива (совпадает с валютой оплаты инвойса) |
network |
string | Код блокчейн-сети (совпадает с сетью оплаты инвойса) |
destination_address |
string | Адрес назначения выплаты (всегда совпадает с first_sender) |
first_sender |
string | Канонический адрес отправителя первого подтверждённого депозита по инвойсу |
tx_hash |
string? | Хеш транзакции в блокчейне, заполняется при переходе в статус completed |
explorer_url |
string? | Ссылка на транзакцию в блокчейн-эксплорере, заполняется при наличии tx_hash |
created_at |
integer | Unix-время создания в секундах |
completed_at |
integer? | Unix-время завершения в секундах |
failed_at |
integer? | Unix-время перехода в статус ошибки в секундах |
cancelled_at |
integer? | Unix-время отмены в секундах |
type |
string | Всегда "refund" |
project_id |
string | Идентификатор проекта (prj_...), которому принадлежит инвойс |
Идемпотентность
Всегда передавайте детерминированный external_order_id для каждой попытки возврата. Повторный вызов POST /v1/refunds с тем же набором (invoice_id, mode, external_order_id) вернёт уже созданный возврат со статусом 200 OK без повторного списания средств. Повторное использование external_order_id с другим invoice_id или mode отклоняется с ошибкой 409 idempotency_conflict.
Одновременно по одному инвойсу может выполняться только один незавершённый возврат. Вы можете сначала оформить возврат переплаты ("overpayment"), а после его перехода в completed — полный возврат оставшейся суммы ("full").
Ошибки
| Код | HTTP | Когда возникает |
|---|---|---|
payment_key_required |
403 | Запрос подписан ключом Payout (rk_) вместо ключа Payment (pk_) |
refunds_not_enabled |
503 | Возвраты по инвойсам отключены в данном окружении |
invoice_not_found |
404 | invoice_id не существует или не входит в область видимости проектов API-ключа |
refund_not_eligible |
409 | Инвойс не находится в статусе paid или paid_over, либо его депозиты не подходят для возврата |
refund_no_overpayment |
409 | Выбран режим "overpayment", но по инвойсу нет доступной к возврату переплаты |
refund_sender_unknown |
409 | Адрес отправителя первого подтверждённого депозита недоступен |
refund_window_expired |
409 | С момента последнего подтверждённого депозита по инвойсу прошло более 14 дней |
refund_network_unsupported |
409 | Выплаты в сети оплаты инвойса не поддерживаются |
refund_in_flight |
409 | По этому инвойсу уже выполняется другой незавершённый возврат |
invoice_already_refunded |
409 | Инвойс уже полностью возвращён |
refund_source_inconsistent |
409 | История депозитов или возвратов по инвойсу не прошла проверку целостности |
insufficient_balance |
409 | На балансе мерчанта недостаточно средств для покрытия amount + fee |
Полный справочник приведён в разделе Коды ошибок.