Back to Projects
6 challenges · about 10 minutes
Which endpoints do you need? Six real product requests and a page of payments API docs. For each one, tick the endpoints you'd need to get the result, then check your answer and see the calls, in order, with what comes back.
How it works 1 Read the goal and the result it needs. That's the product request, in the words you'd hear it.2 Tick the endpoints you'd need. They're all real, from Stripe's API reference , and some are there to tempt you.3 Check your answer to see the calls in order, what each one returns and the outcome.New to reading docs? How to read API documentation covers everything you need, in about fifteen minutes.
1 Refund one item (answered) 2 Refund history on an order (answered) 3 Alert on failed refunds (answered) 4 Yesterday's refunds for finance (answered) 5 Undo a stuck refund (answered) 6 Spot chargebacks early (answered) Challenge 1 of 6
Refund one item
I want to… A customer emails support asking for £12.50 back for one item from their last order. The agent has their email address, and nothing else.
The result I need A refund of £12.50 against the right payment, and a refund ID to put in the ticket. Which endpoints do you need? Tick every one. GET /v1/customers List all customers Customers, newest first. Filter by email or created date. POST /v1/customers Create a customer Adds a customer: name, email, metadata. GET /v1/payment_intents List all PaymentIntents Payments, newest first. Filter by customer or created date. GET /v1/payment_intents/:id Retrieve a PaymentIntent One payment, by its ID: amount, currency, status. Retrieving a payment needs its ID, which is exactly what you don't have yet. Listing by customer finds it. POST /v1/refunds Create a refund Refunds all or part of a payment. Needs a payment_intent (or charge); amount is optional. GET /v1/refunds List all refunds Refunds, newest first. Filter by payment_intent, charge or created date. A page at a time. GET /v1/refunds/:id Retrieve a refund One refund, by its ID, with its status. You'll get the refund back from creating it, so there's no need to fetch it again. POST /v1/refunds/:id/cancel Cancel a refund Cancels a refund. Only works while its status is requires_action. GET /v1/disputes List all disputes Chargebacks raised through a customer's bank. Filter by payment_intent or created date. EVENT refund.failed Webhook event Stripe sends this to your webhook address when a refund fails, with the refund inside. EVENT charge.dispute.created Webhook event Stripe sends this when a customer disputes a payment, with the dispute inside. Check my answer Try again
Show the answer The calls, in order
1 GET /v1/customers?email=(their email)
Turn the email into a customer ID.
Returns "id": "cus_DEMO42"
2 GET /v1/payment_intents?customer=cus_DEMO42&limit=1
Find their latest payment. Lists come newest first, so one is enough.
Returns "id": "pi_DEMO123", "amount": 4997
3 POST /v1/refunds payment_intent=pi_DEMO123 amount=1250
Refund just the item. Leave out amount and the whole £49.97 goes back.
Returns "id": "re_DEMO123", "status": "pending"
The outcome: Three calls, and the order matters: each one needs an ID from the one before. If your own order system already stored the payment intent ID, the first two calls disappear, which is a good argument for saving it.
Next challenge: Refund history on an order Challenge 2 of 6
Refund history on an order
I want to… On the order page in your admin tool, show every refund made against the order. Your order system already saves each order's payment intent ID.
The result I need A list of refunds for one payment: amount, reason, status and date for each. Which endpoints do you need? Tick every one. GET /v1/customers List all customers Customers, newest first. Filter by email or created date. POST /v1/customers Create a customer Adds a customer: name, email, metadata. GET /v1/payment_intents List all PaymentIntents Payments, newest first. Filter by customer or created date. GET /v1/payment_intents/:id Retrieve a PaymentIntent One payment, by its ID: amount, currency, status. The payment itself doesn't list its refunds. Ask the refunds list instead. POST /v1/refunds Create a refund Refunds all or part of a payment. Needs a payment_intent (or charge); amount is optional. GET /v1/refunds List all refunds Refunds, newest first. Filter by payment_intent, charge or created date. A page at a time. GET /v1/refunds/:id Retrieve a refund One refund, by its ID, with its status. That fetches one refund you already know the ID of. Here you need all of them for a payment. POST /v1/refunds/:id/cancel Cancel a refund Cancels a refund. Only works while its status is requires_action. GET /v1/disputes List all disputes Chargebacks raised through a customer's bank. Filter by payment_intent or created date. EVENT refund.failed Webhook event Stripe sends this to your webhook address when a refund fails, with the refund inside. EVENT charge.dispute.created Webhook event Stripe sends this when a customer disputes a payment, with the dispute inside. Check my answer Try again
Show the answer The calls, in order
1 GET /v1/refunds?payment_intent=pi_DEMO123
One call, filtered to this payment.
Returns "data": [ { "id": "re_DEMO123", "amount": 1250, "status": "succeeded" } ], "has_more": false
The outcome: One call. The filter does the work, and the payment intent ID you already store is the key. has_more tells you if there's another page (unlikely for one order).
Next challenge: Alert on failed refunds Challenge 3 of 6
Alert on failed refunds
I want to… Sometimes a refund fails days later, for example because the card was cancelled. Support should find out straight away so they can refund the customer another way.
The result I need A message to the support team the moment any refund fails, with the amount and customer. Which endpoints do you need? Tick every one. GET /v1/customers List all customers Customers, newest first. Filter by email or created date. POST /v1/customers Create a customer Adds a customer: name, email, metadata. GET /v1/payment_intents List all PaymentIntents Payments, newest first. Filter by customer or created date. GET /v1/payment_intents/:id Retrieve a PaymentIntent One payment, by its ID: amount, currency, status. POST /v1/refunds Create a refund Refunds all or part of a payment. Needs a payment_intent (or charge); amount is optional. GET /v1/refunds List all refunds Refunds, newest first. Filter by payment_intent, charge or created date. A page at a time. Polling the list would work, but late and with thousands of wasted calls. The event arrives the moment it happens. GET /v1/refunds/:id Retrieve a refund One refund, by its ID, with its status. You'd have to keep asking about every refund. Let Stripe tell you instead. POST /v1/refunds/:id/cancel Cancel a refund Cancels a refund. Only works while its status is requires_action. GET /v1/disputes List all disputes Chargebacks raised through a customer's bank. Filter by payment_intent or created date. EVENT refund.failed Webhook event Stripe sends this to your webhook address when a refund fails, with the refund inside. EVENT charge.dispute.created Webhook event Stripe sends this when a customer disputes a payment, with the dispute inside. Check my answer Try again
Show the answer The calls, in order
1 Listen for refund.failed at your webhook address
Stripe tells you, so nobody has to keep asking.
Returns "type": "refund.failed", "data": { "object": { "id": "re_DEMO123", "failure_reason": "expired_or_canceled_card" } }
The outcome: No calls at all: an event. You could check every refund every few minutes with the list endpoint, but that's slow, wasteful and still late. When something happens later, look for an event first.
Next challenge: Yesterday's refunds for finance Challenge 4 of 6
Yesterday's refunds for finance
I want to… Finance wants a spreadsheet every morning of every refund made the day before. On a busy day there are hundreds.
The result I need Every refund created between midnight and midnight yesterday, not just the first page. Which endpoints do you need? Tick every one. GET /v1/customers List all customers Customers, newest first. Filter by email or created date. POST /v1/customers Create a customer Adds a customer: name, email, metadata. GET /v1/payment_intents List all PaymentIntents Payments, newest first. Filter by customer or created date. GET /v1/payment_intents/:id Retrieve a PaymentIntent One payment, by its ID: amount, currency, status. POST /v1/refunds Create a refund Refunds all or part of a payment. Needs a payment_intent (or charge); amount is optional. GET /v1/refunds List all refunds Refunds, newest first. Filter by payment_intent, charge or created date. A page at a time. GET /v1/refunds/:id Retrieve a refund One refund, by its ID, with its status. One at a time would need every ID first, and you'd have to list them to get those anyway. POST /v1/refunds/:id/cancel Cancel a refund Cancels a refund. Only works while its status is requires_action. GET /v1/disputes List all disputes Chargebacks raised through a customer's bank. Filter by payment_intent or created date. EVENT refund.failed Webhook event Stripe sends this to your webhook address when a refund fails, with the refund inside. EVENT charge.dispute.created Webhook event Stripe sends this when a customer disputes a payment, with the dispute inside. Check my answer Try again
Show the answer The calls, in order
1 GET /v1/refunds?created[gte]=1793836800&created[lt]=1793923200&limit=100
Filter by date (in seconds since 1970) and ask for the biggest page.
Returns "data": [ …100 refunds… ], "has_more": true
2 GET /v1/refunds?created[gte]=…&starting_after=re_LAST&limit=100
Keep asking for the next page, starting after the last one, until has_more is false.
Returns "data": [ …the rest… ], "has_more": false
The outcome: One endpoint, called several times. The trap is pagination: a report that only reads the first page looks right on a quiet day and silently misses refunds on a busy one. Write "every page" into the acceptance criteria.
Next challenge: Undo a stuck refund Challenge 5 of 6
Undo a stuck refund
I want to… An agent started a refund to a payment method that needs the customer to act, then the customer paid by another route. The agent wants to undo the refund before it goes through.
The result I need The refund cancelled, or a clear message if it's too late. Which endpoints do you need? Tick every one. GET /v1/customers List all customers Customers, newest first. Filter by email or created date. POST /v1/customers Create a customer Adds a customer: name, email, metadata. GET /v1/payment_intents List all PaymentIntents Payments, newest first. Filter by customer or created date. GET /v1/payment_intents/:id Retrieve a PaymentIntent One payment, by its ID: amount, currency, status. POST /v1/refunds Create a refund Refunds all or part of a payment. Needs a payment_intent (or charge); amount is optional. Creating another refund can't undo one. Most actions in an API have their own endpoint to reverse them, or none at all. GET /v1/refunds List all refunds Refunds, newest first. Filter by payment_intent, charge or created date. A page at a time. GET /v1/refunds/:id Retrieve a refund One refund, by its ID, with its status. POST /v1/refunds/:id/cancel Cancel a refund Cancels a refund. Only works while its status is requires_action. GET /v1/disputes List all disputes Chargebacks raised through a customer's bank. Filter by payment_intent or created date. EVENT refund.failed Webhook event Stripe sends this to your webhook address when a refund fails, with the refund inside. EVENT charge.dispute.created Webhook event Stripe sends this when a customer disputes a payment, with the dispute inside. Check my answer Try again
Show the answer The calls, in order
1 GET /v1/refunds/re_DEMO123
Check its status first. Cancelling only works while it's requires_action.
Returns "status": "requires_action"
2 POST /v1/refunds/re_DEMO123/cancel
Cancel it.
Returns "status": "canceled"
The outcome: Two calls. Checking the status first lets the tool hide the Cancel button when it's too late, instead of letting the agent press it and see an error.
Next challenge: Spot chargebacks early Challenge 6 of 6
Spot chargebacks early
I want to… When a customer disputes a payment with their bank (a chargeback), there's a deadline to send evidence. The team keeps missing it.
The result I need The team told the moment a dispute is opened, and a weekly count of disputes for the dashboard. Which endpoints do you need? Tick every one. GET /v1/customers List all customers Customers, newest first. Filter by email or created date. POST /v1/customers Create a customer Adds a customer: name, email, metadata. GET /v1/payment_intents List all PaymentIntents Payments, newest first. Filter by customer or created date. GET /v1/payment_intents/:id Retrieve a PaymentIntent One payment, by its ID: amount, currency, status. POST /v1/refunds Create a refund Refunds all or part of a payment. Needs a payment_intent (or charge); amount is optional. GET /v1/refunds List all refunds Refunds, newest first. Filter by payment_intent, charge or created date. A page at a time. A dispute isn't a refund. It's raised by the customer's bank, and Stripe keeps it under Disputes. GET /v1/refunds/:id Retrieve a refund One refund, by its ID, with its status. POST /v1/refunds/:id/cancel Cancel a refund Cancels a refund. Only works while its status is requires_action. GET /v1/disputes List all disputes Chargebacks raised through a customer's bank. Filter by payment_intent or created date. EVENT refund.failed Webhook event Stripe sends this to your webhook address when a refund fails, with the refund inside. EVENT charge.dispute.created Webhook event Stripe sends this when a customer disputes a payment, with the dispute inside. Check my answer Try again
Show the answer The calls, in order
1 Listen for charge.dispute.created
Tells you the moment a dispute is opened, with the dispute inside.
Returns "type": "charge.dispute.created", "data": { "object": { "id": "du_DEMO7", "reason": "fraudulent" } }
2 GET /v1/disputes?created[gte]=…
For the weekly count, list the disputes created in the week, every page.
Returns "data": [ … ], "has_more": false
The outcome: An event for "now" and a list for "how many". Two needs, two tools: that split comes up in almost every integration.
What the challenges have in common Follow the IDs. Most goals are a chain: each call needs an ID from the one before. Ask which IDs your product already stores.Lists have filters. "Every refund for this order" is one list call with a filter, not a call per refund."Later" means an event. When something happens without anyone asking, look for a webhook before you plan to keep checking.Every page, not just the first. Anything that counts or reports has to follow has_more to the end.Check before you change. Reading a status first means the product can hide a button rather than show an error.