Skip to main content
PayHubDeveloper docs
简体中文

Create refund

API contract v1.0.0

createRefund
POST/v1/refunds
Required scopesrefunds:writeIdempotency key required

Requests a refund for a successful payment, and the funds go back to the original payment method. merchant_refund_no is unique within the merchant and environment, and an idempotency key is required. The refundable principal and funds are checked, and the funds reserved, in a single atomic operation, so concurrent partial refunds never add up to more than the refundable principal. The currency is inherited from the original payment, and a full refund also requires an explicit amount. If the merchant's funds are insufficient, the API returns 422 insufficient_refund_funds. By default, the refund goes to manual review in pending_review. If the outcome is unknown, the funds stay reserved while the platform keeps running status queries, and it never creates the refund again automatically.

Request headers

Request headersRequiredFormatDescription
AuthorizationYesBearer <API_KEY>

Server-side key, to be used only on the merchant's server. The merchant and environment are derived from the key. A missing, invalid, revoked or expired key returns 401.

Content-TypeYesapplication/json

The request body is UTF-8 encoded JSON.

Idempotency-KeyYes1–255 visible ASCII characters

Idempotency key, 1–255 visible ASCII characters. The scope is merchant, environment, method and path: with the same key and matching core business parameters, the original business object is reused, and conflicting parameters return 409 idempotency_conflict. Use one key per submission, and reuse the same key when you retry after a network error; use a new key for a new submission, such as a card change. For concurrent requests with the same key, only one request gets to run, and the others wait for its transaction to commit and then return the same object (200). If the wait times out, the API returns 409 operation_in_progress (retryable=true); retry later with the original key. A request rejected with 400, 401, 403 or 422 doesn't consume the idempotency key; fix it and submit with a new key.

Request body

JSON object CreateRefundRequest

NameTypeRequiredDescription
payment_id
stringRequired

Original payment ID.

Constraints: Length 1–64

Example: pay_demo_001

merchant_refund_no
stringRequired

Merchant refund ID: 1-64 letters, numbers, periods, underscores, colons or hyphens. Unique within one merchant and environment.

Constraints: Format ^[A-Za-z0-9._:-]{1,64}$

Example: REFUND-DEMO-001

amount
integer(int64)Required

Refund principal, in the smallest currency unit. It must not exceed the principal available to refund.

Constraints: ≥ 1

Example: 4000

reason
stringRequired

Refund reason.

Constraints: Length 1–256

Example: requested_by_customer

metadata
objectNo

Custom key-value pairs set by the merchant: up to 20 keys. A key can be up to 40 characters, and a value is a string of up to 200 characters. Don't put sensitive data such as card numbers in it. The platform checks for such data and rejects the request.

Constraints: Up to 20 keysMax value length 200

Example: {"business_order_type":"one_time_purchase"}

Request example

curl
curl -X POST 'https://api.fluxan.co/v1/refunds' \
  -H 'Authorization: Bearer <TEST_SERVER_API_KEY>' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <UNIQUE_OPERATION_KEY>' \
  -d '{
  "payment_id": "pay_demo_001",
  "merchant_refund_no": "REFUND-DEMO-001",
  "amount": 2500,
  "reason": "requested_by_customer",
  "metadata": {
    "ticket": "CS-DEMO-001"
  }
}'

Placeholders to replace

  • <TEST_SERVER_API_KEY>: Test-environment server API key, created on the "API Key" page of the merchant portal
  • <UNIQUE_OPERATION_KEY>: The idempotency key you generate for this request (for example, a UUID); reuse the same key when you retry after a network error

Note

Server API keys must only be used on your own server, so the docs site doesn't offer interactive API testing. Get a test key: API Key page in the merchant portal (opens the merchant portal in a new tab) (Test environment; Requires Owner, Admin or Developer)

Responses

An idempotent replay with the same key; returns the latest state of the original refund.

Replay with the same key: the latest state of the original refund is returned
{
  "id": "ref_demo_001",
  "version": 1,
  "payment_id": "pay_demo_001",
  "merchant_refund_no": "REFUND-DEMO-001",
  "amount": 2500,
  "currency": "USD",
  "status": "processing",
  "reason": "requested_by_customer",
  "returned_amount": 0,
  "failure": null,
  "metadata": {
    "ticket": "CS-DEMO-001"
  },
  "created_at": "2026-09-24T02:00:00Z",
  "completed_at": null,
  "request_id": "req_demo_023"
}

Response fields

Returns an object. See the object section for field descriptions.

NameTypeRequiredDescription
id
stringRequired

Platform refund ID.

Example: ref_demo_001

version
integer(int64)Required

Object version, which increases with every effective business change (status, failure reason, returned amount). Compare versions to discard older events, and don't order events by status name.

Constraints: ≥ 1

Example: 1

payment_id
stringRequired

Original payment ID.

Example: pay_demo_001

merchant_refund_no
stringRequired

Merchant refund ID, unique within one merchant and environment.

Example: REFUND-DEMO-001

amount
integer(int64)Required

Refund principal, in the smallest currency unit.

Constraints: ≥ 1

Example: 4000

currency
stringRequired

Refund currency, inherited from the original payment.

Constraints: Format ^[A-Z]{3}$

Example: USD

status
stringRequired

Refund status.

  • pending_review: accepted and waiting for review; the funds stay reserved
  • processing: approved and submitted, or the provider's result is being confirmed; the funds stay reserved
  • succeeded: the provider confirmed the refund succeeded; the reserved funds become a final deduction
  • failed: the refund clearly did not succeed; the reserved funds are released
  • rejected: rejected in review and not submitted to the provider; the reserved funds are released
  • canceled: canceled by an authorized person before submission to the provider; the reserved funds are released
  • returned: all or part of the funds were actually returned after a refund that had been confirmed as succeeded
  • pending_review: Accepted and waiting for review; the funds stay reserved
  • processing: Approved and submitted, or the provider's result is being confirmed; the funds stay reserved
  • succeeded: The provider confirmed the refund succeeded; the reserved funds become a final deduction
  • failed: The refund clearly did not succeed; the reserved funds are released
  • rejected: Rejected in review and not submitted to the provider; the reserved funds are released
  • canceled: Canceled by an authorized person before submission to the provider; the reserved funds are released
  • returned: All or part of the funds were actually returned after a refund that had been confirmed as succeeded
  • More values may be added in the future. Don't treat a status you don't recognize as success.

Example: pending_review

reason
stringRequired

Refund reason.

Example: requested_by_customer

returned_amount
integer(int64)Required

The cumulative principal actually returned after the refund succeeded.

Constraints: ≥ 0

Example: 0

failure
RefundFailureNullableRequired

The reason when status is failed or rejected; otherwise null.

code
stringRequired

Failure reason code.

Example: refund_rejected

message
stringRequired

Sanitized description.

Example: The refund request didn't pass review.

metadata
objectNo

Custom key-value pairs set by the merchant: up to 20 keys. A key can be up to 40 characters, and a value is a string of up to 200 characters. Don't put sensitive data such as card numbers in it. The platform checks for such data and rejects the request.

Constraints: Up to 20 keysMax value length 200

Example: {"business_order_type":"one_time_purchase"}

created_at
string(date-time)Required

The time the request was accepted.

Example: 2026-09-23T10:00:00Z

completed_at
string(date-time)NullableRequired

The time the refund entered succeeded, failed, rejected or canceled; null if it hasn't ended yet.

Example: null

request_id
stringRequired

Unique identifier of this request, used for troubleshooting and support.

Example: req_demo_001

Possible error codes

HTTPError codeWhen it occursHow to handle
400invalid_requestIdempotency-Key is missing; the order ID contains invalid characters; payment_method_data and payment_method_id are submitted together; confirm=false carries card details; confirm=true has no return_url; a checkout session request sends both groups of identity parameters, or neither; a browser_info / billing_details field the provider requires is missing; the cursor, time format or limit is invalidFix the request according to error.field and resubmit it; don't charge again automatically
400sensitive_data_rejectedDigits that look like a card number, or similar data, appear in description or metadataRemove the sensitive content and resubmit; check why your system wrote card data into these fields
403capability_disabledDirect card isn't enabled; the payment or refund request capability has been turned off; the currency isn't enabledUse hosted checkout instead, or contact platform Operations
404not_foundAn incorrect ID; using a test key to look up a live object; looking up a platform exception refund (not returned by the open API)Check the ID and the environment of the key you used; the platform doesn't reveal whether another merchant's object exists
409idempotency_conflictThe key was reused but the amount, currency, order ID or similar parameters differThis is an integration bug: check your idempotency key generation logic; don't change the parameters and replay the original key; use a new key for a new operation
409operation_in_progressRequests with the same key were sent concurrentlyRetry later with the same key, or retrieve related_resource_id; don't switch to a new key
409refund_no_conflictThe same refund ID is submitted again with a different idempotency keyRetrieve the refund that related_resource_id points to
422refund_amount_exceedsPrincipal available to refund = captured amount − successfully refunded principal − principal reserved by unfinished refunds − principal claimed by disputes; concurrent partial refunds exceed it in totalRetrieve the payment's captured_amount, refunded_amount and refund_reserved_amount, then request the refund again with the refundable amount
422insufficient_refund_fundsThe funds haven't been settled yet or are already reserved for a payout; for example, with a payment of 100.00, a fee of 3.50 and only the net amount of 96.50 available, a full refund is short by 3.50message gives the shortfall, and no refund is created this time; after you top up offline and platform Finance records it, or after the platform approves using your reserve / held funds (the approval is valid for 7 days), resubmit with the same merchant refund ID and amount and a new idempotency key; the platform doesn't reduce the refund amount automatically or cover the shortfall for you
422refund_not_allowedThe payment didn't succeed; the refund window the provider allows has passed; the transaction is in dispute and the provider doesn't allow refundsFollow message; handle disputed transactions on the Disputes page in the merchant portal
401unauthenticatedCommonNo Authorization header was sent; the key was copied incompletely; the old key expired after a rotation; the key was revokedCheck your credentials: on the API Key page in the merchant portal, verify the key's status and environment
403forbiddenCommonCreating a refund with a read-only key; calling getEvent without the read scope for the event's business area; if the request's source IP isn't on the key's allowlist, the more specific ip_not_allowed is returnedUse a key that has the required scope; don't work around the restriction
429rate_limitedCommonA rate limit per merchant and per key; switching keys doesn't get around the merchant's overall limitWait for Retry-After seconds, then retry with the original idempotency key
500internal_errorCommon—Retry with the original idempotency key, or query by merchant order ID / refund ID; never charge again with a new key
502 / 503upstream_unavailableCommonA provider or platform dependency failed, and the platform can't confirm whether this request took effectSame as above

Notes

  • merchant_refund_no is unique within the merchant and environment. Resubmitting the same refund ID with a different key returns 409 refund_no_conflict.
  • The currency is inherited from the original payment. A full refund also requires an explicit amount.
  • Refundable principal = captured amount − principal of succeeded refunds − principal reserved by unfinished refunds − principal held exclusively by disputes. Exceeding it returns 422 refund_amount_exceeds.
  • Refundable principal and your funds are two separate checks. If your funds fall short, the platform doesn't accept the request and creates no refund. The API returns 422 insufficient_refund_funds, gives the shortfall in message, and adds shortfall (in the smallest currency unit) and currency to error.details. The platform doesn't reduce the refund amount for you or cover the shortfall.
  • In the live environment, refunds go to manual review (pending_review) by default. In the test environment, they're approved automatically by default and return processing right away.
  • Refunds go back to the original payment method, and other card numbers aren't accepted. If the outcome is unknown after submission to the provider, the platform keeps running status queries and never creates the refund again automatically.
Was this page helpful?