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.

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

  1. Open Credit → Configuration and select TON mainnet / USDT, Polygon mainnet / USDC or Base mainnet / USDC.
  2. Save limits, tenures and pricing. STAO creates or synchronizes the active loan product automatically.
  3. Complete branding and allowed origins, then copy the merchant-specific URL from Credit → Integration. Its embed key is generated automatically.
  4. 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:

Environmentchain valueNetwork
SandboxPOLYGON_AMOYPolygon Amoy testnet
SandboxBASE_SEPOLIABase Sepolia testnet
SandboxSOLANA_DEVNETSolana devnet
LivePOLYGONPolygon mainnet — real USDC, real settlement
LiveBASEBase mainnet — real native USDC, real settlement
LiveTONTON 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.

CredentialHeaderUsed by
x-api-key API keyx-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 keypath segment, no headerPublic, 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 & pathPurpose
GET /v1/embed/:embedKey/configBranding, network, stablecoin and tenure/amount limits for the widget to render.
POST /v1/embed/:embedKey/eligibilityCheck a wallet before quoting. Body: walletAddress.
POST /v1/embed/:embedKey/credit-line/bootstrapResolve a verified wallet to its durable revolving agreement; returns requiresKyc until the merchant confirms KYC.
POST /v1/embed/:embedKey/quotePrice a specific amount + tenure. Body: walletAddress, amount, tenureDays.
POST /v1/embed/:embedKey/acceptCreate the loan after verifying the exact, unexpired, single-use agreement challenge. Requires walletSignature and agreementChallengeId.
GET /v1/embed/:embedKey/loans/:loanIdStatus for the widget's confirmation screen.
GET /v1/embed/:embedKey/loans/:loanId/repayment-instructionsChain-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.

SettingNotes
TenuresAny subset of 3, 7, 15 days (product v1).
Min / max ticketYour own floor and ceiling; hard product cap is $1,000 regardless.
Network / stablecoinSupported pairs are TON / USDT, POLYGON / USDC and BASE / USDC. Each widget program has one fixed pair; mixed pairs are rejected.
Loan productCreated automatically when absent and synchronized to the selected stablecoin and ticket limits when configuration is saved.
Embed keyGenerated per widget program. A merchant may operate multiple independent programs without replacing a native integration.
Grace period0 to 30 days after dueAt before auto-default.
Processing / late feesFixed or percentage, set independently.
BrandingCompany 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 typeFires when
loan.application_submittedA loan is created, before underwriting.
loan.decisionUnderwriting records APPROVED / DECLINED / MANUAL_REVIEW.
loan.approved / loan.rejectedA manual-review loan is actioned by an ops reviewer.
loan.funded / loan.funding_failedPrincipal transfer to the borrower succeeds or fails.
loan.repaid / loan.repayment_failedA repayment transaction settles or fails.
loan.overduePast dueAt, still unpaid.
loan.defaultedAuto-defaulted after the grace period.
liquidity.low / liquidity.unavailableYour funding pool is running low or a draw couldn't be filled.
webhook.delivery_failedAn 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.

StatusTypical cause
400Failed validation (missing/malformed field), or a business rule such as no LP wallet with enough headroom.
401Missing/invalid x-api-key or expired dashboard session.
403Authenticated but not permitted (for example a MEMBER calling an Owner/Admin-only route).
404Loan, borrower, or embed key not found for your tenant.
429Rate limited, back off and retry.
5xxOur 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:

ScenarioWhat to check
Happy pathCreate, approved, funded, repaid. Loan id stable throughout; all webhooks received and signatures verify.
Declined applicationLoan appears in your DECLINED filter; no funds move; loan.decision received.
Manual reviewLoan sits pending until actioned; loan.approved/loan.rejected received after.
Duplicate submitSame idempotencyKey twice returns the same loan, not a second one.
Overdue & defaultUnpaid past dueAt triggers loan.overdue, then loan.defaulted after the grace period.
Webhook outagePoint a webhook at a dead URL; confirm 3 retries then webhook.delivery_failed.
KYC gateWith 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:

  1. KYB documents approved
  2. Commercial terms agreed
  3. Active underwriting policy
  4. Active loan product
  5. Funding liquidity available (your own LP or the shared pool)
  6. Webhook endpoint configured
  7. API key issued
  8. Owner user account exists
  9. Branding configured (company name set)
  10. At least one loan has completed a full sandbox cycle (funded and repaid)

Live wallet requirements

RoleRequirement
Treasury / operatorPublic address plus secure server-side signing access. Never place a private key in browser code, email or support chat.
DisbursementHolds the selected stablecoin and sends approved principal to borrowers.
Repayment / collectionsReceives principal, interest and fees and is recorded against settled loans.
GasPOL 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.