PovitoDevelopers

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.

Create a session that offers itjson
{ "…": "…", "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:

Press "Succeed" from a scriptbash
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" }'
Response 200json
{ "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:

bash
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.

bash
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 null

Things worth testing on your side#

  • Idempotency: replay a create with the same key and body (201 again, Idempotent-Replayed: true), then the same key with a different body (409 idempotency_key_conflict).
  • Reconciliation: create with the same reference_id and body twice while the first session is open — you get the first one back with 200. Create again with a different amount — the first is now expired and you get a new session with 201.
  • Validation: amount: 46000 on the sample basket (400 amount_mismatch), a success_url on 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 t shifted by ten minutes; both must fail.
  • Late return: press Take 90 s with a …003 amount, close the tab, and confirm your webhook handler alone completes the order.