API reference
Everything the portal does goes through this API — and the same API is open to your automations. This page covers authentication, the response envelope, errors, and the endpoint map.
Base URL
https://serront.com/api/v1
(https://serront.forjio.com/api/v1 serves the same API.)
Three ways in
Portal session (browser)
The dashboard authenticates with a session cookie minted by Serront's
sign-in flow (Huudis SSO). If you're building on top of the portal in
the browser, fetches to /api/v1/* ride the cookie — nothing to
configure.
API key (recommended for automation)
Create an sk_live_… key at
/dashboard/api-keys and send it as a Bearer
token:
curl -H "Authorization: Bearer sk_live_xxx" \
"https://serront.com/api/v1/orders?status=requested"
Keys are long-lived (no expiry — revoke to kill), scoped to the workspace they were created in, and shown only once at creation — Serront stores only a hash. The list view shows each key's prefix and when it was last used. This is the simplest credential for scripts, cron jobs, and integrations.
Huudis JWT (Bearer)
Callers that already hold a Huudis access token for the serront
audience can send it directly as a Bearer token. Tokens are
short-lived — refresh and retry on AUTH_REQUIRED rather than
caching one forever. (If that dance is annoying, that's what API keys
are for.)
Response envelope
Every endpoint — success or failure — returns the same envelope:
{
"data": { },
"error": null,
"meta": {
"requestId": "req_01jx2v9k3m8q4r5s6t7u8v9w0x",
"timestamp": "2026-06-11T03:00:00.000Z"
}
}
- Success:
datais the payload,errorisnull. Created resources come back with HTTP201. - Failure:
dataisnull,erroris set, and the HTTP status matches.
Errors
error carries an UPPER_SNAKE_CASE code, a human-readable message,
and sometimes a param naming the offending field. Match on
error.code, not the message — messages may be reworded; codes are
stable.
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR |
400 | Bad or missing field — the message says which |
DISCOUNT_INVALID |
400 | The discount code was rejected |
AUTH_REQUIRED |
401 | Missing/invalid credentials |
INVALID_TOKEN |
401 | Unrecognized API key or unverifiable JWT |
LIMIT_REACHED |
403 | Tier limit hit (e.g. service count) |
UPGRADE_REQUIRED |
403 | Paid-tier feature on a free workspace |
NOT_FOUND |
404 | Resource doesn't exist in your workspace |
CONFLICT |
409 | State conflict (e.g. slug taken, service has orders) |
INVALID_TRANSITION |
409 | Illegal order status move |
PAYMENT_MODULE_DISABLED |
409 | Money endpoint with the Payment module off |
MARKETING_MODULE_DISABLED |
409 | Discount endpoint with the Marketing module off |
RATE_LIMITED |
429 | Slow down (OTP requests, mainly) |
INTERNAL_ERROR |
500 | Our fault — retry, then contact support with the requestId |
Every response carries a unique meta.requestId (req_…) — quote it
when contacting support. Responses also include X-RateLimit-*
headers; treat them as advisory and back off when Remaining
approaches zero.
Pagination
List endpoints that page (orders, ledger entries, payouts) take
limit (1–100, default 50) and an opaque cursor, and return
cursor + hasMore alongside the page. Pass the returned cursor
back to get the next page; a null cursor means you're done.
Endpoint map
The seller surface (API key / JWT / session):
| Area | Endpoints |
|---|---|
| Storefront | GET /storefront · PUT /storefront (full replace; hideBranding: true is Starter+ → 403 UPGRADE_REQUIRED) |
| Services | GET/POST /services · GET/PATCH/DELETE /services/:id |
| Orders | GET /orders · GET/PATCH /orders/:id · POST /orders/:id/messages · POST /orders/:id/confirm-payment · GET /orders/:id/proof |
| Modules | GET/PATCH /modules ({"payment": true} / {"marketing": true}) |
| Money | GET /ledger/balance · GET /ledger/entries · GET/POST /payouts · POST /payouts/:id/cancel · GET/PATCH /payouts/bank-account |
| Discounts | ANY /ripllo/* (proxied to Ripllo, workspace-scoped) |
| Billing | GET /billing · POST /billing/checkout |
| API keys | GET/POST /api-keys · DELETE /api-keys/:id |
| Webhooks | GET/POST /webhook-subscriptions · PATCH/DELETE /webhook-subscriptions/:id |
The public (unauthenticated) surface — what your storefront page and buyer order pages run on:
| Endpoint | Does |
|---|---|
GET /public/storefront/:slug |
Published profile + active services |
POST /public/storefront/:slug/order |
Submit an order request |
POST /public/storefront/:slug/validate-discount |
Dry-run a discount code |
GET /public/orders/:accessToken |
The buyer's order — status, thread, payment instructions |
POST /public/orders/:accessToken/messages |
Buyer reply |
POST /public/orders/:accessToken/claim-payment |
Buyer "I have transferred" |
POST /public/orders/:accessToken/pay |
Mint an online checkout (Payment module) |
PUT /public/orders/:accessToken/proof |
Upload a payment-proof image (≤5 MB) |
Worked example: read a storefront, place an order
# The public storefront (no auth)
curl https://serront.com/api/v1/public/storefront/studio-adi
{
"data": {
"storefront": {
"slug": "studio-adi",
"displayName": "Studio Adi",
"bio": "Design studio in Bandung.",
"whatsappNumber": "+62812xxxx",
"hideBranding": false,
"onlinePayment": true,
"discountCodes": false
},
"services": [
{
"id": "svc_01jx…",
"slug": "logo-design",
"name": "Logo design",
"pricingType": "fixed",
"priceIdr": 1500000,
"packages": null
}
]
},
"error": null,
"meta": { "requestId": "req_01jx…", "timestamp": "2026-06-11T03:00:00.000Z" }
}
# Place an order (what the order form does)
curl -X POST https://serront.com/api/v1/public/storefront/studio-adi/order \
-H "Content-Type: application/json" \
-d '{
"serviceSlug": "logo-design",
"buyerName": "Budi",
"buyerEmail": "budi@example.com",
"notes": "Logo for a coffee shop, warm colors."
}'
{
"data": {
"number": 12,
"accessToken": "Zk3…",
"quotedPriceIdr": 1500000,
"discountAmountIdr": 0
},
"error": null,
"meta": { "requestId": "req_01jx…", "timestamp": "2026-06-11T03:00:01.000Z" }
}
accessToken is the buyer's only credential — their order page is
/o/<accessToken>. For package services, include "packageName";
buyerPhone must be E.164 (+62812…) when present.