Everything on this page uses a sk_test_... key. Test events are real events: they are signed, retried and
listed by GET /v1/events exactly like live ones, with "livemode": false.
Receive events on your computer#
Your laptop has no public address, and Fluveo delivers only to one. A test endpoint takes http as well as
https, so the scheme is not the problem: being reachable is. The usual bridge is a tunnel: a
small program on your machine that opens a public address and passes every request it gets to a port
on your laptop. Pick a tunnel your team already trusts. Shut it down when you finish: while it runs, anyone who
learns the address can send requests to your machine.
Start your handler#
Run one of the signature handlers. Each listens on port 4242 at
/webhooks/fluveo.bashexport FLUVEO_WEBHOOK_SECRET=whsec_placeholder # replaced in step 4 node server.mjsStart a tunnel to port 4242#
Start your tunnel tool and point it at
http://localhost:4242. It prints a public address such ashttps://random-name.example-tunnel.dev. Your webhook URL is that address plus/webhooks/fluveo.Create a test endpoint with that URL#
Ask for
*while you explore, so every event reaches you.Create a test endpointcurl https://api.fluveo.com/v1/webhook_endpoints \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: local-tunnel-endpoint-1" \ -d url="https://random-name.example-tunnel.dev/webhooks/fluveo" \ -d "enabled_events[]=*" \ -d description="Local testing"const response = await fetch("https://api.fluveo.com/v1/webhook_endpoints", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "local-tunnel-endpoint-1" }, body: new URLSearchParams([ ["url", "https://random-name.example-tunnel.dev/webhooks/fluveo"], ["enabled_events[]", "*"], ["description", "Local testing"], ]), }); const { id, secret } = await response.json();import requests response = requests.post( "https://api.fluveo.com/v1/webhook_endpoints", headers={"Authorization": "Bearer sk_test_...", "Idempotency-Key": "local-tunnel-endpoint-1"}, data=[ ("url", "https://random-name.example-tunnel.dev/webhooks/fluveo"), ("enabled_events[]", "*"), ("description", "Local testing"), ], ) endpoint = response.json()<?php $ch = curl_init('https://api.fluveo.com/v1/webhook_endpoints'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_test_...', 'Idempotency-Key: local-tunnel-endpoint-1'], CURLOPT_POSTFIELDS => 'url=' . urlencode('https://random-name.example-tunnel.dev/webhooks/fluveo') . '&enabled_events[]=*&description=' . urlencode('Local testing'), ]); $endpoint = json_decode(curl_exec($ch), true);Give the handler the secret#
Copy
secret(whsec_...) from the answer, setFLUVEO_WEBHOOK_SECRETto it and restart the handler.Make an event#
Run any call from the table below. Your handler prints
received payment_intent.succeeded evt_...within seconds.
When the tunnel address changes, move the endpoint instead of making a new one:
POST /v1/webhook_endpoints/{id} with the new url. When you are finished, delete it with
DELETE /v1/webhook_endpoints/{id}.
Send a test event from the Dashboard#
On an endpoint's page under Webhooks in the Dashboard, Send test event posts one signed sample event of a type you pick to the endpoint's URL and shows the HTTP status and the first part of your answer. Use it to check that your URL is reachable and that your signature check passes.
A Dashboard test event is not a real event: it is not listed by GET /v1/events and it is never retried. It goes
where a real test delivery goes: an http or https URL on any port, never localhost, a private address or an
internal name. To test your business logic, make real test events with the API calls below.
You can send one only in test mode, only of a type the endpoint subscribes to, and at most 5 per endpoint
and 20 per store each minute; more are refused until the minute passes. Fluveo waits up to 10 seconds for
your answer. The test event is signed with the endpoint's own secret and carries the same headers as a real
delivery. Tell it apart by its body: the top-level fluveo_test_event is true, livemode is false, the
event id starts with evt_test_. Object ids, when present, carry _test_, such as pi_test_....
Make each event with test calls#
Each row is an API call made with sk_test_... and the events it causes, in the usual order. Remember that
your endpoint may receive them in another order. Amounts are in the smallest currency unit: 2000 is $20.00.
| What to do | Call | Events |
|---|---|---|
| A card payment that succeeds | POST /v1/payment_intents with amount=2000, currency=usd, payment_method_types[]=card, payment_method=pm_card_visa, confirm=true | payment_intent.created, charge.succeeded, payment_intent.succeeded, then charge.updated a few seconds later, when the payment's balance line is ready |
| A card that is declined | The same call with payment_method=pm_card_visa_chargeDeclined (or another decline card) | payment_intent.created, charge.failed, payment_intent.payment_failed |
| Authorize now, capture later | The success call plus capture_method=manual | payment_intent.created, charge.succeeded, payment_intent.amount_capturable_updated |
| Capture it | POST /v1/payment_intents/{id}/capture | charge.captured, payment_intent.succeeded |
| Cancel an authorized payment | POST /v1/payment_intents/{id}/cancel | payment_intent.canceled |
| Refund | POST /v1/refunds with payment_intent=pi_... | refund.created, charge.refunded |
| A refund that fails later | Pay with payment_method=pm_card_refundFail, then refund it | refund.created, charge.refunded, then refund.failed and charge.refund.updated |
| A dispute | Pay with payment_method=pm_card_createDispute | the payment events and charge.dispute.created: the dispute is already open when the payment answers |
| Customers | POST /v1/customers, POST /v1/customers/{id}, DELETE /v1/customers/{id} | customer.created, customer.updated, customer.deleted |
| A Checkout Session that expires | POST /v1/checkout/sessions/{id}/expire on an open session | checkout.session.expired |
The decline cards are pm_card_visa_chargeDeclined, pm_card_visa_chargeDeclinedInsufficientFunds,
pm_card_chargeDeclinedExpiredCard, pm_card_chargeDeclinedIncorrectCvc and
pm_card_chargeDeclinedProcessingError. The dispute cards are pm_card_createDispute,
pm_card_createDisputeProductNotReceived and pm_card_createDisputeInquiry. For what each event means, see
Event types.
curl https://api.fluveo.com/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: webhook-test-order-1" \
-d amount=2000 \
-d currency=usd \
-d "payment_method_types[]=card" \
-d payment_method=pm_card_visa \
-d confirm=trueconst response = await fetch("https://api.fluveo.com/v1/payment_intents", {
method: "POST",
headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "webhook-test-order-1" },
body: new URLSearchParams({
amount: "2000",
currency: "usd",
"payment_method_types[]": "card",
payment_method: "pm_card_visa",
confirm: "true",
}),
});
const paymentIntent = await response.json();import requests
payment_intent = requests.post(
"https://api.fluveo.com/v1/payment_intents",
headers={"Authorization": "Bearer sk_test_...", "Idempotency-Key": "webhook-test-order-1"},
data={"amount": 2000, "currency": "usd", "payment_method_types[]": "card", "payment_method": "pm_card_visa", "confirm": "true"},
).json()<?php
$ch = curl_init('https://api.fluveo.com/v1/payment_intents');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_test_...', 'Idempotency-Key: webhook-test-order-1'],
CURLOPT_POSTFIELDS => http_build_query([
'amount' => 2000,
'currency' => 'usd',
'payment_method_types' => ['card'],
'payment_method' => 'pm_card_visa',
'confirm' => 'true',
]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);Then confirm that Fluveo recorded the event, without waiting for the webhook:
curl -G https://api.fluveo.com/v1/events \
-H "Authorization: Bearer sk_test_..." \
-d type=payment_intent.succeeded \
-d limit=1The event's data.object.id is your PaymentIntent, and its request.idempotency_key is
webhook-test-order-1.
Pix and Boleto#
Creating and confirming a test Pix or Boleto sends payment_intent.created, then
payment_intent.requires_action. Fluveo's test bank then pays it, and the payer email in
payment_method_data[billing_details][email] sets the moment:
succeed_immediately@example.comis paid within seconds. Your endpoint getscharge.succeededandpayment_intent.succeeded, thencharge.updated.- Any other address, such as
ada@example.com, sends the same events about 3 minutes after the confirm. expire_immediately@example.comis never paid. Give a Pixpayment_method_options[pix][expires_after_seconds]=60and, a few minutes later, your endpoint getscharge.failedandpayment_intent.payment_failed.
To pay one that is still waiting, open its test page from hosted_instructions_url (Pix) or
hosted_voucher_url (Boleto). See Pix and Boleto.
Before you go live#
- Your handler checks
Fluveo-Signatureand answers400on a bad signature. Send it a request with a wrong signature and check that it is refused. - It answers
2xxwithin a few seconds, and does slow work later. - Sending the same event twice changes nothing the second time.
- It handles events in any order.
- The endpoint's
enabled_eventsholds the types your code acts on and nothing more, instead of*.