This takes you from a test API key to a payment on our hosted checkout page, has you pay it yourself in test mode, and shows how you learn that it succeeded. Every command uses curl and test mode, so no money moves.
You need a test API key: a client ID (ci_test_...) and its secret (sk_test_...), made in the Merchant Dashboard under Developers → API keys. The secret is shown once.
1. Get an Access Token
Section titled “1. Get an Access Token”Exchange the key for an Access Token. It lasts 15 minutes, and it is the only credential you send on /v1 calls (see Authentication).
curl https://api.example.com/oauth/token \ -u "ci_test_...:sk_test_..." \ -d grant_type=client_credentials{"access_token": "at_test_...", "token_type": "Bearer", "expires_in": 900}Keep the token for the next commands:
export ACCESS_TOKEN=at_test_...2. Create a payment intent
Section titled “2. Create a payment intent”A payment intent is one payment to take. Create it without a payment method, and your customer chooses one on our checkout page. The Idempotency-Key header is required on a create: use a value unique to the operation, such as your order number and attempt.
curl https://api.example.com/v1/payment_intents \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Idempotency-Key: order-1042-1" \ -H "Content-Type: application/json" \ -d '{ "amount": 10000, "currency": "php", "description": "Order 1042", "return_url": "https://shop.example.com/orders/1042/paid", "cancel_url": "https://shop.example.com/cart", "customer_email": "maria@example.com" }'amount is in the currency's minor unit, so 10000 in php is ₱100.00. The answer is 201 with the intent, waiting for your customer:
{ "id": "pi_01j9x2m8k4q7r5t3v6w8y0z2a4", "object": "payment_intent", "amount": 10000, "amount_received": 0, "currency": "php", "payment_method_types": ["card", "qrph"], "channel": "ecommerce", "status": "requires_action", "next_action": { "type": "redirect_to_url", "redirect_to_url": { "url": "https://checkout.example.com/pay/q3Jx8m1Vw0b_Zc4N-7pLkTfYs2HdRa9uEoGiW6nBvXy", "return_url": "https://shop.example.com/orders/1042/paid" } }, "last_payment_error": null, "cancellation_reason": null, "canceled_at": null, "description": "Order 1042", "metadata": {}, "livemode": false, "created": 1790411400}livemode is false: this is a test payment. payment_method_types lists the methods your account can take this payment with now, so what you see depends on how your account is set up.
3. Send your customer to the checkout page
Section titled “3. Send your customer to the checkout page”Redirect your customer to next_action.redirect_to_url.url. That is our hosted checkout page: it shows your business's name, the amount and how long the page has left, and offers the methods in payment_method_types. Open it in a browser to see what your customer sees.
When they are done, the page sends them back to your return_url, with payment_intent and checkout_session added. That proves nothing about the payment: read the outcome from the API, as below.
4. Pay it yourself, in test mode
Section titled “4. Pay it yourself, in test mode”Nobody pays a test payment for you: in test mode, you play the customer. Open the link in a browser, choose Card, and select Continue to card payment. In place of card fields, the page shows the simulated card step, marked test mode. It asks for no card number, and offers four choices:
| Choose | What happens |
|---|---|
| Pay | The bank approves the payment at once. |
| Decline | The bank declines it, with a reason you choose or none, and the customer can try again. |
| Challenge, then pay | The whole page goes to a simulated check by the customer's bank, which passes. |
| Challenge, then fail | The same check, which fails: the customer could not confirm it was them. |
Choose Challenge, then pay and select Continue to the bank’s check. On the bank's check, select Finish the check. The page says the payment was successful and sends you back to your return_url, as it would send a customer after a real card payment.
To pay from your terminal instead, choose QR Ph or Cash on the page, then give the payment its outcome with the test helper. paid pays the code or the cash reference:
curl https://api.example.com/v1/test_helpers/payment_intents/pi_01j9x2m8k4q7r5t3v6w8y0z2a4/simulate \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Idempotency-Key: order-1042-1-paid" \ -H "Content-Type: application/json" \ -d '{"outcome": "paid"}'The helper takes a test key only. Testing your integration has every outcome it gives, and how to make a payment fail or expire.
5. Read the outcome
Section titled “5. Read the outcome”Retrieve the intent with the id you were given:
curl https://api.example.com/v1/payment_intents/pi_01j9x2m8k4q7r5t3v6w8y0z2a4 \ -H "Authorization: Bearer $ACCESS_TOKEN"The payment you just made is succeeded, with amount_received equal to amount, and payment_method_types now names the method it was paid with:
{ "id": "pi_01j9x2m8k4q7r5t3v6w8y0z2a4", "object": "payment_intent", "amount": 10000, "amount_received": 10000, "currency": "php", "payment_method_types": ["card"], "channel": "ecommerce", "status": "succeeded", "next_action": null, "last_payment_error": null, "cancellation_reason": null, "canceled_at": null, "description": "Order 1042", "metadata": {}, "livemode": false, "created": 1790411400}The status says where a payment stands. Until your customer pays it is requires_action; it becomes processing once they have paid and the outcome is not known yet, then succeeded, with amount_received equal to amount. A failed attempt does not end the intent: it goes back to requires_payment_method, with last_payment_error saying why. Every status is explained in Payment intents.
6. Or be told by webhook
Section titled “6. Or be told by webhook”Rather than reading the intent, give us an HTTPS address and we send you an event as each payment moves. The signing secret is in this answer only, so store it:
curl https://api.example.com/v1/webhook_endpoints \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://shop.example.com/webhooks", "enabled_events": ["payment_intent.succeeded", "payment_intent.payment_failed"] }'{ "id": "we_3f9a1c2b7d4e5f60718293a4", "object": "webhook_endpoint", "url": "https://shop.example.com/webhooks", "enabled_events": ["payment_intent.succeeded", "payment_intent.payment_failed"], "status": "enabled", "secret": "whsec_...", "livemode": false, "created": 1790411400}The address must be https:// and resolve only to public addresses, so a server on your own machine needs a tunnel to be reached. Every delivery is signed: Webhooks shows how to verify one, and how retries and rotating the secret work.