Merchant integration guide
Sandbox account to live embedded credit flow: auth, identifiers, the loan lifecycle, KYC responsibilities, webhooks, error handling, and a go-live checklist. For full request/response schemas see Swagger UI.
On this page
1. Sandbox quick start
Every merchant starts in sandbox. Sandbox loans settle against a simulated chain (no real testnet funds required) so you can build and test your whole integration before certifying for go-live. Create a sandbox account to get your first API key.
Configure a merchant
- Open Credit → Configuration and select
TON mainnet / USDT,Polygon mainnet / USDCorBase mainnet / USDC. - Save limits, tenures and pricing. STAO creates or synchronizes the active loan product automatically.
- Complete branding and allowed origins, then copy the merchant-specific URL from Credit → Integration. Its embed key is generated automatically.
- Complete KYC policy, liquidity, webhooks and operational-wallet setup before enabling real funding.
One merchant configuration has one active rail. Changing it updates that merchant's active embedded product and widget. Use a separate merchant configuration when two rails must remain live simultaneously.
Request your first loan
curl -X POST https://api.stao.io/v1/loans \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"loanProductId": "...",
"borrowerRef": "user-123",
"borrowerWalletAddress": "0x...",
"chain": "POLYGON_AMOY",
"principalAmount": "250.00",
"idempotencyKey": "order-9F21"
}'The response includes the loan's immutable id and its underwriting decision. Poll GET /v1/loans/:id or, better, register a webhook (section 9) to be notified the moment the decision and funding events happen.
Supported chains. Sandbox tenants always run on a testnet. Once a tenant completes go-live certification, requests may target a live network instead — pass the matching value in chain:
| Environment | chain value | Network |
|---|---|---|
| Sandbox | POLYGON_AMOY | Polygon Amoy testnet |
| Sandbox | BASE_SEPOLIA | Base Sepolia testnet |
| Sandbox | SOLANA_DEVNET | Solana devnet |
| Live | POLYGON | Polygon mainnet — real USDC, real settlement |
| Live | BASE | Base mainnet — real native USDC, real settlement |
| Live | TON | TON mainnet — real USDT jetton, real settlement |
On TON the settlement asset is USDT (a TEP-74 jetton), not USDC, and collateral is not supported. Borrower addresses are accepted in either user-friendly (EQ… / UQ…) or raw (0:…) form. Live-chain access is enabled per tenant after go-live certification (section 12). Requests with a live chain value move real funds — there is no faucet and transactions are irreversible once confirmed on-chain.
2. Authentication
Two credential types, two audiences. Use the one that matches who is calling.
| Credential | Header | Used by |
|---|---|---|
x-api-key API key | x-api-key: sk_... | Your backend, server-to-server. Create/revoke under API keys in the merchant dashboard. |
| Dashboard session (JWT) | Authorization: Bearer ... | The merchant dashboard itself (POST /v1/auth/login). Not for backend integrations. |
| Embed key | path segment, no header | Public, browser-safe calls from your checkout widget (section 7). Identifies your tenant only, carries no privileges. |
API keys are shown once on creation and stored hashed, copy it immediately. Revoking a key takes effect immediately on the next request.
3. Identifiers & idempotency
Every loan gets an immutable id (UUID) the moment POST /v1/loans or the embed /accept call succeeds. It never changes and is the join key across the API, dashboards, webhooks, and notifications, store it against your own order/application record.
Pass idempotencyKey on every loan-creation call (max 255 chars, your own order id works well). If a request is retried with the same key, you get back the original loan instead of a duplicate. Without it, a network retry on your side can create two loans for one purchase.
4. Loan lifecycle
A loan's status field moves through one path:
CREATED -> ACTIVE -> REPAID
-> DEFAULTED (past due date + grace period, unpaid)Underwriting runs synchronously on creation and records a decision, APPROVED, DECLINED, or MANUAL_REVIEW. A declined loan never leaves CREATED; the dashboard and GET /v1/loans filters surface it as a synthetic DECLINED state derived from that decision, alongside the real LoanStatus values, so declined applications stay visible instead of disappearing.MANUAL_REVIEW loans wait for an ops reviewer; watch for loan.approved or loan.rejected.
5. Funding & repayment
Stao is non-custodial: we never hold your borrower's funds. On APPROVED, principal is drawn from your tenant's liquidity pool (your own LP, or the shared pool) and sent on-chain directly to borrowerWalletAddress. The loan moves to ACTIVE once that transaction confirms and loan.funded fires. If funding can't be completed (for example no LP wallet has enough headroom) the loan stays CREATED and loan.funding_failed or liquidity.unavailable fires instead.
To collect a repayment, call POST /v1/loans/:id/repay, it returns an unsigned transaction for the borrower's own wallet to sign, never a charge you initiate on their behalf. Once that transaction confirms on-chain, our settlement listener records it, the loan moves to REPAID, and loan.repaid fires. See GET /v1/settlements/:loanId for the full settlement history of a loan.
Repayment on TON (mainnet)
On TON there is no contract call to sign. The borrower repays by sending the total due in USDT from their own wallet to the platform settlement wallet with the transfer comment stao:repay:<loanId>. The comment is how settlement matches the transfer to the loan — it must be included exactly. Warn borrowers never to repay from an exchange account: exchange withdrawals drop the comment.
You do not need to build this UX. Link the borrower to the hosted repayment page — it shows a pre-filled wallet deep link, a QR code, and copyable manual-transfer fields:
https://merchant.stao.io/embed/YOUR_EMBED_KEY/repay/LOAN_ID
Or fetch the raw instructions from the public embed API and render them yourself:
GET https://api.stao.io/v1/embed/YOUR_EMBED_KEY/loans/LOAN_ID/repayment-instructions
{
"chain": "TON",
"supported": true,
"to": "UQ...", // platform settlement wallet
"asset": "USDT",
"amount": "5.099986", // total due today (6dp)
"amountUnits": "5099986", // jetton base units
"comment": "stao:repay:LOAN_ID", // REQUIRED transfer comment
"jettonMaster": "EQ...",
"links": {
"ton": "ton://transfer/...",
"tonkeeper": "https://app.tonkeeper.com/transfer/..."
}
}Settlement polls the platform wallet and applies the transfer automatically once it confirms with the matching comment; the loan moves to REPAID and loan.repaid fires as usual. Amounts refresh as late fees accrue, so fetch instructions at the moment the borrower is about to pay.
Loans unpaid past dueAt fire loan.overdue; if still unpaid after the grace period they're auto-marked DEFAULTED (loan.defaulted) and any pledged collateral is seized.
6. KYC responsibilities
Stao does not collect, store, or verify identity documents. You are the responsible party for knowing your borrower under your own AML/KYC program. Once you've verified a borrower, tell us the outcome:
curl -X PATCH https://api.stao.io/v1/borrowers/:id/kyc \
-H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{ "status": "VERIFIED", "merchantUserId": "agent_482" }'status is one of PENDING, VERIFIED, FAILED. merchantUserId is your own internal reference for who/what performed the check, we stamp verifiedAt and write an immutable audit record on every change. Your underwriting policy can require VERIFIED before a loan is approved (see GET/PUT /v1/policies).
Loan-level KYC and agreement confirmation
The borrower-level endpoint above records the reusable KYC status. If your tenant has requiresKycBeforeFunding enabled, an approved API-originated loan also remains unfunded until your backend confirms KYC for that specific loan and records the agreement the borrower accepted. Use the loan id returned by POST /v1/loans.
curl -X POST https://api.stao.io/v1/loans/LOAN_ID/kyc-confirmation \
-H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"verified": true,
"merchantUserId": "BION_INTERNAL_REFERENCE",
"reference": "KYC_CASE_2026_00482"
}'verified is required. merchantUserId and reference are optional merchant-controlled audit references. STAO stores the outcome and references only, never identity documents.
curl -X POST https://api.stao.io/v1/loans/LOAN_ID/agreement \
-H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"walletSignature": "0x8f2c...BORROWER_WALLET_SIGNATURE",
"agreementChallengeId": "SERVER_ISSUED_SINGLE_USE_CHALLENGE_ID",
"agreementText": "THE EXACT AGREEMENT TEXT SHOWN TO THE BORROWER"
}'Sign the exact agreementChallenge.message returned by the quote endpoint and send both walletSignature and agreementChallengeId. The challenge is bound to the wallet, merchant, amount, tenure, chain and stablecoin, expires after five minutes, and is single-use. agreementText is optional; supply it when you render the agreement yourself so STAO preserves the exact immutable text the borrower signed. Reposting does not overwrite an agreement already on file.
7. Widget / iframe embed
The fastest integration path: no backend loan-creation code at all. Create a widget program on the Credit page of the merchant dashboard. Each program has its own embed key, network, asset, loan product and allowed origins, so it can coexist with native integrations such as a Telegram Mini App. Polygon/USDC and Base/USDC use Reown AppKit for MetaMask, Trust, Coinbase, injected wallets and WalletConnect QR/deep-link connections. The same wallet session signs the revolving agreement, confirms Drawdowns and submits network-matched USDC repayments.
<iframe src="https://merchant.stao.io/embed/YOUR_EMBED_KEY" style="width:100%;height:640px;border:0" ></iframe>
Or use the responsive launcher for websites and mobile-app WebViews:
<script src="https://merchant.stao.io/widget.js" data-embed-key="YOUR_EMBED_KEY" data-channel="WEB" ></script>
Set data-channel to WEB, IOS, ANDROID orPARTNER_WEBVIEW. STAO records the program and channel on the loan and includes both in webhook envelopes. For MVP these programs keep exposure separate from native channels.
The embed page walks the borrower through wallet eligibility, reusable KYC, the revolving credit-line agreement, Drawdown selection and repayment. When KYC is required it posts astao:kyc-required message to the parent page; the merchant completes its own KYC and calls the borrower credit-line agreement endpoint from section 6. The widget then resumes without repeating wallet analysis, KYC or agreement acceptance. It is branded with your logo and primary color and always shows "Powered by Stao".
| Method & path | Purpose |
|---|---|
GET /v1/embed/:embedKey/config | Branding, network, stablecoin and tenure/amount limits for the widget to render. |
POST /v1/embed/:embedKey/eligibility | Check a wallet before quoting. Body: walletAddress. |
POST /v1/embed/:embedKey/credit-line/bootstrap | Resolve a verified wallet to its durable revolving agreement; returns requiresKyc until the merchant confirms KYC. |
POST /v1/embed/:embedKey/quote | Price a specific amount + tenure. Body: walletAddress, amount, tenureDays. |
POST /v1/embed/:embedKey/accept | Create the loan after verifying the exact, unexpired, single-use agreement challenge. Requires walletSignature and agreementChallengeId. |
GET /v1/embed/:embedKey/loans/:loanId | Status for the widget's confirmation screen. |
GET /v1/embed/:embedKey/loans/:loanId/repayment-instructions | Chain-specific repayment details (TON deep links/QR/comment; Polygon or Base USDC approval and the matching pool-contract call). See section 5. |
8. Branding & credit config
Configured on the Credit page of the merchant dashboard, changes apply immediately to both the widget and any loans you create via the API.
| Setting | Notes |
|---|---|
| Tenures | Any subset of 3, 7, 15 days (product v1). |
| Min / max ticket | Your own floor and ceiling; hard product cap is $1,000 regardless. |
| Network / stablecoin | Supported pairs are TON / USDT, POLYGON / USDC and BASE / USDC. Each widget program has one fixed pair; mixed pairs are rejected. |
| Loan product | Created automatically when absent and synchronized to the selected stablecoin and ticket limits when configuration is saved. |
| Embed key | Generated per widget program. A merchant may operate multiple independent programs without replacing a native integration. |
| Grace period | 0 to 30 days after dueAt before auto-default. |
| Processing / late fees | Fixed or percentage, set independently. |
| Branding | Company name, logo URL, primary color (hex), support email, terms URL, rendered on the widget. |
9. Webhooks
Register an endpoint from the Webhooks page (or POST /v1/webhooks) with a URL and the event types you want. Every delivery is a signed POST with a JSON body; verify it before trusting it:
x-stao-event: loan.funded
x-stao-signature: <hex hmac-sha256 of the raw body>
// verify (Node):
const expected = crypto.createHmac('sha256', YOUR_WEBHOOK_SECRET)
.update(rawRequestBody).digest('hex');
if (expected !== req.headers['x-stao-signature']) return res.status(401).end();Failed deliveries retry up to 3 attempts with backoff. After the final attempt fails, a webhook.delivery_failed notification is raised so it doesn't fail silently, check delivery history any time under GET /v1/webhooks/deliveries or the Webhooks page.
| Event type | Fires when |
|---|---|
loan.application_submitted | A loan is created, before underwriting. |
loan.decision | Underwriting records APPROVED / DECLINED / MANUAL_REVIEW. |
loan.approved / loan.rejected | A manual-review loan is actioned by an ops reviewer. |
loan.funded / loan.funding_failed | Principal transfer to the borrower succeeds or fails. |
loan.repaid / loan.repayment_failed | A repayment transaction settles or fails. |
loan.overdue | Past dueAt, still unpaid. |
loan.defaulted | Auto-defaulted after the grace period. |
liquidity.low / liquidity.unavailable | Your funding pool is running low or a draw couldn't be filled. |
webhook.delivery_failed | An endpoint failed all 3 delivery attempts. |
Every event above also appears in the merchant dashboard's Notifications page and is emailed to the account owner, webhooks are for your systems, notifications are for your team.
10. Error handling
Errors are plain JSON: { "statusCode": 400, "message": "...", "error": "Bad Request" }.message is either a string or an array of field-level validation messages.
| Status | Typical cause |
|---|---|
400 | Failed validation (missing/malformed field), or a business rule such as no LP wallet with enough headroom. |
401 | Missing/invalid x-api-key or expired dashboard session. |
403 | Authenticated but not permitted (for example a MEMBER calling an Owner/Admin-only route). |
404 | Loan, borrower, or embed key not found for your tenant. |
429 | Rate limited, back off and retry. |
5xx | Our fault, safe to retry with the same idempotencyKey. |
11. Test scenarios
Sandbox tenants run on a simulated chain, so funding and repayment settle instantly without real testnet tokens. Before requesting go-live, exercise at least these paths end to end:
| Scenario | What to check |
|---|---|
| Happy path | Create, approved, funded, repaid. Loan id stable throughout; all webhooks received and signatures verify. |
| Declined application | Loan appears in your DECLINED filter; no funds move; loan.decision received. |
| Manual review | Loan sits pending until actioned; loan.approved/loan.rejected received after. |
| Duplicate submit | Same idempotencyKey twice returns the same loan, not a second one. |
| Overdue & default | Unpaid past dueAt triggers loan.overdue, then loan.defaulted after the grace period. |
| Webhook outage | Point a webhook at a dead URL; confirm 3 retries then webhook.delivery_failed. |
| KYC gate | With a policy requiring VERIFIED KYC, confirm an unverified borrower is blocked/declined. |
12. Go-live checklist
Sandbox certification runs through a fixed 10-step checklist inside your merchant dashboard (Partner onboarding) before we flip you to production credentials, it covers, in order:
- KYB documents approved
- Commercial terms agreed
- Active underwriting policy
- Active loan product
- Funding liquidity available (your own LP or the shared pool)
- Webhook endpoint configured
- API key issued
- Owner user account exists
- Branding configured (company name set)
- At least one loan has completed a full sandbox cycle (funded and repaid)
Live wallet requirements
| Role | Requirement |
|---|---|
| Treasury / operator | Public address plus secure server-side signing access. Never place a private key in browser code, email or support chat. |
| Disbursement | Holds the selected stablecoin and sends approved principal to borrowers. |
| Repayment / collections | Receives principal, interest and fees and is recorded against settled loans. |
| Gas | POL for Polygon, ETH for Base, or TON for TON jetton transfers, monitored above the configured threshold. |
One controlled address may temporarily cover treasury, disbursement and repayment during a limited team test. Its signing key must match the configured public address and have a secure, recoverable backup.
Track progress and submit for review from the Partner onboarding page in your dashboard.
Questions we have not covered? Reach out from the merchant dashboard, or open Swagger UI for exact schemas.