Testing#
Test mode is a key mode, not a separate environment. A povito_ck_test_… key against the normal API host creates objects with livemode: false that never touch money and never share a row with live objects. Everything in the Checkout guides works identically in both modes. Test keys are accepted on the Live host as well as the Sandbox host — see Keys and environments.
Test keys#
| Key | Where | Unlocks |
|---|---|---|
povito_ck_test_<key_id>_<secret> |
your server | the merchant API, in test mode |
povito_pk_test_<key_id>_<secret> |
browser or app | /v1/public/* for your test sessions — only needed for the Phase 2 drop-in |
Keys are shown once when issued and stored hashed. A test key cannot read a live object (404, never a hint that it exists) and cannot register a live webhook endpoint (403 mode_mismatch). Scopes are the same set as live: sessions:write, sessions:read, refunds:write, refunds:read, webhooks:manage, events:read, customers:read.
The test gateway#
In test mode your merchant can enable a method called test: a simulated vendor with a page of buttons instead of a bank. Check GET /v1/payment_methods for "code": "test", "enabled": true; if it is not there, ask Povito to switch it on for your test merchant. It presents IQD and USD, supports partial refunds, and is refused outright in live mode.
{ "…": "…", "payment_method_types": ["test"] }Omitting payment_method_types on a test merchant does not include test — it must be asked for explicitly, so a test key with your real methods enabled behaves like production.
The buttons#
After phone verification the page redirects to checkout.povito.com/t/{attempt_id} — the simulated vendor — showing the amount and four buttons:
| Button | What the "vendor" answers | Result |
|---|---|---|
| Succeed | captured, exactly the presented amount | attempt succeeded, session completed, payment_status: paid, checkout.session.completed fires |
| Fail | declined (simulated_decline) |
attempt failed, session back to open, payment.attempt.failed fires; the shopper can pick another method |
| Cancel | cancelled by the shopper | attempt canceled, session back to open |
| Take 90 s | no answer yet | the page returns and polls; the attempt resolves by the amount rule below — pick a …003 amount to actually watch it stay pending |
Each button sends the shopper back to checkout.povito.com/r/{attempt_id} — the same return path a real vendor uses — where the page asks the API to verify, exactly as in live mode. Automation can press the buttons through the public API with a publishable key:
curl -X POST https://api.checkout.povito.com/v1/public/test-gateway/pa_test_3M8XQ2K7A9BC4DEFGH1J/decide \
-H "Authorization: Bearer $POVITO_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "succeeded" }'{ "attempt_id": "pa_test_3M8XQ2K7A9BC4DEFGH1J", "status": "succeeded", "return_url": "https://checkout.povito.com/r/pa_test_3M8XQ2K7A9BC4DEFGH1J" }decision is succeeded, failed, canceled or slow. The call is 403 for any attempt that is not a test-gateway attempt.
Amount-driven outcomes#
For unattended tests the last three digits of the presented amount decide, unless a button was pressed:
| Amount ends in | Outcome |
|---|---|
…001 (e.g. 45001) |
declined immediately — payment.attempt.failed, reason simulated_decline |
…002 (e.g. 45002) |
stays pending; the reconcile job keeps re-checking on a backoff and the attempt expires after 24 hours with reason timeout |
…003 (e.g. 45003) |
pending for 30 seconds, then succeeds — exercises the "shopper left the vendor page, webhook arrives later" path |
| anything else | succeeds |
The line_items must still reconcile, so bump a charge line by the same amount.
The test phone and OTP#
In test mode the phone +9647500000000 always receives the code 000000, and no SMS is sent. Any other number goes through the real OTP provider (with the same 5-per-10-minutes limit), so use the test phone in automation. The page shows a hint with the code when a session is in test mode.
A verified phone yields a session-scoped customer_token; the hosted page keeps it in an HTTP-only cookie, a script passes it as X-Povito-Customer-Token on the public API.
Vendor sandboxes#
Beyond the simulated gateway, Povito ops can attach real vendor sandboxes — ZainCash UAT, FIB staging, Stripe test — to your test merchant so zaincash, fib or card sessions in test mode go to the vendor's own test environment. Ask for it when you are ready for integration verification; each vendor's sandbox has its own test accounts and cards, which Povito support will share.
Webhooks in test mode#
Register an endpoint with "mode": "test" using your test key; test events go only to test endpoints. The test tool sends a synthetic event of any type:
curl -X POST https://api.checkout.povito.com/v1/webhook_endpoints/we_2K7A9BC4DEFGH1J3M8XQ/test \
-H "Authorization: Bearer $POVITO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "refund.succeeded" }'For local development your endpoint must be reachable from Povito over HTTPS — use a tunnel. Private-network and localhost targets are refused at registration.
A full run, end to end#
This is the sequence Povito's own end-to-end test performs against the API — every call on this site, in order. $SK is a test secret key, $PK a test publishable key.
API=https://api.checkout.povito.com/v1
# 1. Register a webhook endpoint (secret shown once)
curl -s -X POST $API/webhook_endpoints -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
-d '{"url":"https://your-store.example/webhooks/povito","events":["*"],"mode":"test"}'
# 2. Create the 45,000 IQD session offering the test gateway and cash on delivery
curl -s -X POST $API/checkout/sessions -H "Authorization: Bearer $SK" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{"reference_id":"POV-1041","amount":45000,"currency":"IQD",
"line_items":[{"label":"Basket","amount":42000,"type":"charge"},{"label":"Delivery","amount":5000,"type":"shipping"},{"label":"Coupon","amount":2000,"type":"discount"}],
"payment_method_types":["test","cashondelivery"],
"customer":{"phone":"+9647500000000","name":"Ali M. Ismail"},
"success_url":"https://your-store.example/thanks","cancel_url":"https://your-store.example/cart","locale":"ar"}'
# → id cs_test_…, code 7KQ4…, status open, payment_status unpaid, payment null
# 3. What the page sees (publishable key + code). Note: no metadata, and every method with availability.
curl -s $API/public/sessions/$CODE -H "Authorization: Bearer $PK"
# 4. Phone verification with the test OTP → customer_token
curl -s -X POST $API/public/sessions/$CODE/otp -H "Authorization: Bearer $PK" -H "Content-Type: application/json" -d '{"phone":"+9647500000000"}'
# → 202 { "sent": true, "retry_after": 30 }
curl -s -X POST $API/public/sessions/$CODE/otp/verify -H "Authorization: Bearer $PK" -H "Content-Type: application/json" -d '{"phone":"+9647500000000","code":"000000"}'
# → { "customer_token": "…", "expires_at": "…", "profile": { "name": null, "addresses": [], "instruments": [] } }
# 5. Start an attempt on the test gateway → kind redirect, redirect_url https://checkout.povito.com/t/pa_test_…
curl -s -X POST $API/public/sessions/$CODE/attempts -H "Authorization: Bearer $PK" -H "X-Povito-Customer-Token: $TOKEN" -H "Content-Type: application/json" -d '{"method":"test"}'
# a second attempt while this one is open → 409 attempt_in_progress
# 6. Press "Fail": the session returns to open, redirect_to is null
curl -s -X POST $API/public/test-gateway/$ATTEMPT/decide -H "Authorization: Bearer $PK" -H "Content-Type: application/json" -d '{"decision":"failed"}'
curl -s $API/public/sessions/$CODE/attempts/$ATTEMPT -H "Authorization: Bearer $PK"
# → { "attempt_id": "pa_test_…", "code": "7KQ4…", "method": "test", "status": "failed", "session_status": "open", "payment_status": "unpaid",
# "redirect_to": null, "failure_reason": "simulated_decline", "present": null, "cancel_url": "https://your-store.example/cart" }
# 7. Try again and press "Succeed": the poll now returns the redirect with session_id and reference_id appended
curl -s -X POST $API/public/sessions/$CODE/attempts -H "Authorization: Bearer $PK" -H "X-Povito-Customer-Token: $TOKEN" -H "Content-Type: application/json" -d '{"method":"test"}'
curl -s -X POST $API/public/test-gateway/$ATTEMPT2/decide -H "Authorization: Bearer $PK" -H "Content-Type: application/json" -d '{"decision":"succeeded"}'
curl -s $API/public/sessions/$CODE/attempts/$ATTEMPT2 -H "Authorization: Bearer $PK"
# → { "attempt_id": "pa_test_…", "code": "7KQ4…", "method": "test", "status": "succeeded", "session_status": "completed", "payment_status": "paid",
# "redirect_to": "https://your-store.example/thanks?session_id=cs_test_…&reference_id=POV-1041", "failure_reason": null, "present": null, "cancel_url": "https://your-store.example/cart" }
# The same poll without the code, for a return page opened in another browser (adds merchant, locale, livemode):
curl -s $API/public/attempts/$ATTEMPT2 -H "Authorization: Bearer $PK"
# 8. Verify from the server: payment.method test, captured 45000 IQD, customer.phone +9647500000000
curl -s $API/checkout/sessions/$SESSION -H "Authorization: Bearer $SK"
# …and the checkout.session.completed webhook arrives at your endpoint, signed; GET /events/{id} shows deliveries[0].status_code 200
# 9. Refund 5,000 → partially_refunded; 41,000 → 409 refund_exceeds_captured; 40,000 → refunded
curl -s -X POST $API/refunds -H "Authorization: Bearer $SK" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{"session_id":"'$SESSION'","amount":5000,"reason":"Customer returned the delivery item, ticket #1"}'
# 10. Cash on delivery completes offline with no money moved
curl -s -X POST $API/checkout/sessions -H "Authorization: Bearer $SK" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
-d '{"reference_id":"POV-1042","amount":45000,"currency":"IQD",
"line_items":[{"label":"Basket","amount":42000,"type":"charge"},{"label":"Delivery","amount":5000,"type":"shipping"},{"label":"Coupon","amount":2000,"type":"discount"}],
"payment_method_types":["cashondelivery"],"collect":{"phone":"none","name":"optional","shipping_address":"none"},
"success_url":"https://your-store.example/thanks"}'
curl -s -X POST $API/public/sessions/$CODE2/attempts -H "Authorization: Bearer $PK" -H "Content-Type: application/json" -d '{"method":"cashondelivery"}'
# → { "kind": "offline", … }; GET the session: status completed, payment_status requires_offline_collection, payment.captured_amount nullThings worth testing on your side#
- Idempotency: replay a create with the same key and body (
201again,Idempotent-Replayed: true), then the same key with a different body (409 idempotency_key_conflict). - Reconciliation: create with the same
reference_idand body twice while the first session is open — you get the first one back with200. Create again with a differentamount— the first is nowexpiredand you get a new session with201. - Validation:
amount: 46000on the sample basket (400 amount_mismatch), asuccess_urlon an unregistered host (422 return_host_not_allowed), a method you have not enabled (422 method_not_enabled). - Isolation: read a test session with a live key —
404. - Webhook signature: verify a real delivery, then verify it again with a wrong secret and with
tshifted by ten minutes; both must fail. - Late return: press Take 90 s with a
…003amount, close the tab, and confirm your webhook handler alone completes the order.