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.

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: data is the payload, error is null. Created resources come back with HTTP 201.
  • Failure: data is null, error is 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.

See also

  • Webhooks — push instead of poll.
  • CLI — the same API from your terminal.