# PayProof AGENTS.md
Agent integration guide for the PayProof verification & developer billing platform.
## Product rules
1. PayProof **verifies** Ethiopian bank transfer receipts. It does **not** collect settlement for developer products.
2. End-customers pay **the developer's bank accounts**. PayProof confirms the receipt and maintains subscription/payment status.
3. Platform usage (app + API verification) is **credit-based**. One billable verification = 1 credit (after free allowance).
## Canonical merchant flow
```
Create app → Add receiving accounts → Set product price → Create checkout
→ Customer pays with reference_code → Verify receipt → Checkout auto-completes
→ Webhook subscription.activated | payment.verified
```
Dashboard path: **Apps → [app] → Pricing → Checkouts**.
Public customer pay page: `/pay/{checkout_public_id}`
Public API: `GET /api/public/gateway/checkouts/:public_id/`
Public verify (multipart): `POST /api/public/gateway/checkouts/:public_id/verify/`
Live updates: `wss://{host}/ws/gateway/checkouts/:public_id`
## Base URL
```
https://verifyitet.duckdns.org
```
Local: `NEXT_PUBLIC_API_URL` (synced by `./start_dev`).
## Auth
### Dashboard (Telegram OIDC)
Same as Android. Obtain `id_token` via Telegram OIDC SDK, then:
```
POST /api/auth/telegram-login/
{ "id_token": "<OIDC JWT>" }
```
JWT on `Authorization: Bearer` for `/api/platform/*` and `/api/subscription/*`.
### Phone OTP
```
POST /api/auth/send-otp/ { "phone": "09xxxxxxxx" }
→ channel: "telegram" (6-digit DM) | "sms" (4-digit)
POST /api/auth/verify-otp/ { "phone", "code" }
```
### API key (server)
```
Authorization: Bearer pk_live_...
```
Only on `/api/v1/*`. Create on each app (Dashboard → Apps → [app] → API keys).
## Credits
```
GET /api/v1/credits/
GET /api/subscription/credits/
POST /api/subscription/credit-topups/ { "amount_etb": "150" }
```
Credits minted = `floor(paid_etb / etb_per_verification)`. Pay **PayProof platform** accounts from the top-up intent.
## Apps & pricing
```
POST /api/platform/apps/ { "name", "description?", "logo_url?", "brand_color?", "checkout_style?"}
POST /api/platform/products/ {
"app_id", "name",
"product_type": "subscription" | "one_time",
"pricing_model": "fixed" | "dynamic", # dynamic only for one_time
"interval": "week" | "month" | "year" | "day" | "custom",
"interval_count?": 1,
"trial_days?": 0,
"amount_etb": "499" # null/omit for dynamic
}
GET /api/platform/products/?app_id=
GET/PUT /api/platform/apps/:id/settings/ # window, expiry, partial, overpayment, auto_manual_review, default_success_url, default_cancel_url
POST /api/platform/api-keys/ { "name", "app_id" } # app-scoped preferred
```
Product must belong to the app. Inactive app/product cannot create checkouts.
## Gateway checkouts
```
POST /api/v1/checkouts/ (or /api/platform/checkouts/)
{
"app_id", "product_id",
"amount_etb?": "999.50", # required for pricing_model=dynamic
"customer_ref?": "user_42",
"idempotency_key?": "order_42",
"expires_in_hours?": 48,
"success_url?": "https://yoursite.com/orders/42/thanks",
"cancel_url?": "https://yoursite.com/cart"
}
→ {
"checkout": {
"id" | "public_id",
"reference_code": "PP-{company}-{hex}",
"expected_amount_etb",
"amount_paid_etb",
"status": "pending",
"expires_at",
"success_url?",
"cancel_url?"
},
"payment_accounts": [...],
"pay_url": "/pay/cs_..."
}
```
**Redirects:** Hosted `/pay/:id` auto-redirects to `success_url` after verification (brief success UI, then navigate). Query params appended: `checkout_id`, `reference_code`, `status=verified`, and `customer_ref` when set. App defaults: `PUT /api/platform/apps/:id/settings/` with `default_success_url` / `default_cancel_url`. Per-checkout URLs override app defaults.
**Customer instructions:** transfer exact `expected_amount_etb` to a listed account; put `reference_code` in the transfer remark/reason.
Hosted page upload (no auth, rate-limited by IP+checkout):
```
POST /api/public/gateway/checkouts/:public_id/verify/
Content-Type: multipart/form-data
fields: images[] | images | qr_payload?
→ { "verification_id", "status": "PENDING", "websocket": "/ws/gateway/checkouts/:public_id" }
```
Also available authenticated: `POST /api/v1/checkouts/:public_id/verify/` and `POST /api/platform/checkouts/:public_id/verify/`.
Statuses: `pending | partially_paid | verified | failed | expired | canceled | manual_review`.
Polling: `GET /api/public/gateway/checkouts/:public_id/status/`
WS: `wss://{host}/ws/gateway/checkouts/:public_id` — `{type:"checkout.updated", data:{...}}`.
Manual review:
```
POST /api/public/gateway/checkouts/:public_id/manual-review/
POST /api/platform/checkouts/:public_id/manual-review/approve/ { "note?", "amount_etb?" }
POST /api/platform/checkouts/:public_id/manual-review/reject/ { "reason?" }
```
Subscription status:
```
GET /api/v1/subscriptions/:id/status/
→ { "id", "status", "ok", "interval", "current_period_end", "customer_ref" }
```
## Verification API
```
POST /api/v1/verifications/
Authorization: Bearer pk_live_...
Content-Type: multipart/form-data
fields: images[] , qr_payload?
→ { "id", "status": "PENDING", "websocket": "/ws/verify/{schema}/" }
GET /api/v1/verifications/:id/
```
WebSocket: `wss://{host}/ws/verify/{schema_name}/` — type `verification_update`.
Gateway checkout WS: `wss://{host}/ws/gateway/checkouts/:public_id`.
Billable terminal statuses: `VERIFIED`, `FAILED`, `REJECTED` (1 credit each, including gateway uploads).
## Webhooks
```
POST /api/platform/webhooks/
{
"app_id",
"url",
"events": [
"checkout.created", "checkout.verified", "checkout.partially_paid",
"checkout.failed", "checkout.expired",
"checkout.manual_review_requested", "checkout.manual_review_approved",
"checkout.manual_review_rejected",
"subscription.activated", "subscription.ended", "subscription.renewed",
"payment.verified", "payment.failed",
"verification.verified", "verification.failed"
],
"enabled": true
}
```
Deliveries: `GET /api/platform/webhooks/:id/deliveries/` · retry `POST …/deliveries/:delivery_id/retry/`.
Header: `PayProof-Signature: t=<unix>,v1=<hex>`
Verify: `HMAC-SHA256(secret, "#{t}.#{raw_body}")`.
## Accounts
```
GET /api/platform/accounts/
POST /api/platform/accounts/ { bank_name, account_number, account_name }
POST /api/platform/accounts/import/ { "ids": "all" } # list existing verification accounts
```
## Do not
- Expect PayProof to settle developer product funds.
- Complete checkouts without a real verified payment in production workflows — rely on reference-matched verification.
- Put API keys in client-side apps.