Skip to content
StraventaDocs
Recipes

Issue a refund

Refund a completed payment attempt — full or partial

Execute curls interactively

To run these API calls directly from your browser, open the Payments API reference and use the built-in Try-it playground. Paste your sk_test_… key in the Auth panel, pick Sandbox, and send without leaving the docs.

Refunds in 505pay are issued against a specific payment attempt (not the intent itself), because one intent can have multiple attempts and only the captured one is refundable. The route mirrors that structure: POST /v1/payments/intents/{id}/attempts/{attemptId}/refunds.

Not all methods support refunds

QRIS Dynamic and QRIS Static do not support programmatic refunds — issue a manual bank transfer to the customer instead. The list of method × refund support is in Payment lifecycle.


Find the completed attempt

curl "https://sandbox.505pay.link/v1/payments/intents/$INTENT_ID/attempts" \
  -H "Authorization: Bearer $PAYOPS_SECRET_KEY"
# → [{ "id": "018f...", "status": "captured", "amount_idr": 50000, ... }]

Pick the attempt with status: "captured".

Create the refund

# Full refund
curl -X POST \
  "https://sandbox.505pay.link/v1/payments/intents/$INTENT_ID/attempts/$ATTEMPT_ID/refunds" \
  -H "Authorization: Bearer $PAYOPS_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-$INTENT_ID-full" \
  -d '{"amount_idr": 50000, "reason": "customer_request"}'

# Partial refund — pass any positive amount_idr ≤ remaining
curl -X POST \
  "https://sandbox.505pay.link/v1/payments/intents/$INTENT_ID/attempts/$ATTEMPT_ID/refunds" \
  -H "Authorization: Bearer $PAYOPS_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-$INTENT_ID-partial-1" \
  -d '{"amount_idr": 25000, "reason": "partial_fulfillment"}'
const refund = await fetch(
  `https://sandbox.505pay.link/v1/payments/intents/${intentId}/attempts/${attemptId}/refunds`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PAYOPS_SECRET_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `refund-${intentId}-full`,
    },
    body: JSON.stringify({ amount_idr: 50_000, reason: 'customer_request' }),
  },
).then((r) => r.json());
refund = requests.post(
    f'https://sandbox.505pay.link/v1/payments/intents/{intent_id}/attempts/{attempt_id}/refunds',
    headers={
        'Authorization': f"Bearer {os.environ['PAYOPS_SECRET_KEY']}",
        'Idempotency-Key': f'refund-{intent_id}-full',
    },
    json={'amount_idr': 50_000, 'reason': 'customer_request'},
).json()
$refund = json_decode($client->post(
    "https://sandbox.505pay.link/v1/payments/intents/{$intentId}/attempts/{$attemptId}/refunds",
    [
        'headers' => [
            'Authorization' => 'Bearer ' . getenv('PAYOPS_SECRET_KEY'),
            'Idempotency-Key' => "refund-{$intentId}-full",
        ],
        'json' => ['amount_idr' => 50000, 'reason' => 'customer_request'],
    ]
)->getBody(), true);
body, _ := json.Marshal(map[string]any{"amount_idr": 50_000, "reason": "customer_request"})
req, _ := http.NewRequest("POST",
    fmt.Sprintf("https://sandbox.505pay.link/v1/payments/intents/%s/attempts/%s/refunds", intentID, attemptID),
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("PAYOPS_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "refund-"+intentID+"-full")
resp, _ := http.DefaultClient.Do(req)

Response (202 Accepted): the refund is enqueued; final state arrives via webhook.

{
  "id": "018f...",
  "intent_id": "018f...",
  "attempt_id": "018f...",
  "amount_idr": 50000,
  "reason": "customer_request",
  "status": "pending",
  "created_at": "2026-05-17T12:30:00Z"
}

Confirm via webhook

Listen for payment.refunded (full refund) or watch the intent status flip to partially_refunded (partial). The webhook payload mirrors the response above with status: "succeeded" and a completed_at timestamp.

{
  "event": "payment.refunded",
  "intent_id": "018f...",
  "refund_id": "018f...",
  "amount_idr": 50000,
  "remaining_refundable_idr": 0
}

Refund reasons

ReasonUse when
customer_requestCustomer asked for a refund
duplicatePayment was charged twice
fraudulentSuspected fraudulent transaction
partial_fulfillmentOnly part of the order was delivered

Was this page helpful?

On this page