v1https://btelo.com

API Documentation

btelo.com HTTP API. This document is the complete, authoritative contract — any third party (human or AI) should be able to integrate using ONLY this JSON, without additional communication.

Quickstart

  1. 1. Ask a btelo admin to create an Integration for you. You will receive (integration_id, secret).
  2. 2. Store the secret server-side. Never ship it to browsers. The secret itself is never sent over the wire — each request carries only a short-lived HMAC signature derived from it.
  3. 3. For API calls (server-to-server): sign each request with the HMAC scheme documented in `auth.hmac` (secret stays local; only `Btelo-Sign` is transmitted, valid for ±5 min).
  4. 4. For end-user login (OAuth2 SSO): send users to /api/oauth/authorize, exchange the code at /api/oauth/token using your integration_id + secret as client_id + client_secret.
  5. 5. If you are an automation (e.g. AI agent) that needs to REGISTER OTHER INTEGRATIONS programmatically, ask the admin to set is_admin=true on your integration. You can then call /admin/integrations/* via the same HMAC auth.

Authentication

Each third-party product ("integration") is registered in the btelo admin console and gets ONE pair of credentials: (integration_id, secret). The same pair is used for BOTH server-to-server API calls (HMAC below) AND OAuth login (client_id=integration_id, client_secret=secret on the /api/oauth/token endpoint). The secret can be viewed or rotated by the admin at any time. A disabled integration is rejected by both flows.

HMAC-SHA256

Server-to-server API endpoints (including /admin/integrations/* for admin integrations) require 3 headers. The secret is never transmitted — only a time-limited HMAC signature.

Headers

Btelo-Key-IdThe integration_id (public identifier).
Btelo-SignFirst 16 hex chars of HMAC-SHA256("<integration_id>:<timestamp>", <secret>)
Btelo-TsCurrent Unix timestamp (seconds). Must be within ±5 minutes of server time.

Signature formula

sign = hex(HMAC_SHA256(secret, integration_id + ":" + unix_ts))[:16]

Clock skew tolerance: ±300 seconds

Examples

curl
KEY_ID=vibe-remote; SECRET=bti_your_secret; TS=$(date +%s)
SIGN=$(python3 -c "import hmac,hashlib,os; print(hmac.new(os.environ['SECRET'].encode(), f\"$KEY_ID:$TS\".encode(), hashlib.sha256).hexdigest()[:16])")
curl -H "Btelo-Key-Id: $KEY_ID" -H "Btelo-Ts: $TS" -H "Btelo-Sign: $SIGN" https://btelo.com/api/v1/tiers
go
keyID := "vibe-remote"
secret := "bti_your_secret"
ts := fmt.Sprintf("%d", time.Now().Unix())
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(keyID + ":" + ts))
sign := hex.EncodeToString(mac.Sum(nil))[:16]
req.Header.Set("Btelo-Key-Id", keyID)
req.Header.Set("Btelo-Ts", ts)
req.Header.Set("Btelo-Sign", sign)
node
const crypto = require('crypto');
const keyId = 'vibe-remote', secret = 'bti_your_secret';
const ts = Math.floor(Date.now()/1000).toString();
const sign = crypto.createHmac('sha256', secret).update(`${keyId}:${ts}`).digest('hex').slice(0,16);
headers['Btelo-Key-Id']=keyId; headers['Btelo-Ts']=ts; headers['Btelo-Sign']=sign;
python
import hmac, hashlib, time
key_id, secret = 'vibe-remote', 'bti_your_secret'
ts = str(int(time.time()))
sign = hmac.new(secret.encode(), f'{key_id}:{ts}'.encode(), hashlib.sha256).hexdigest()[:16]
headers = {'Btelo-Key-Id': key_id, 'Btelo-Ts': ts, 'Btelo-Sign': sign}

OAuth 2.0 SSO

access_token_formatRS256 JWT (validate with JWKS above). TTL ~1 hour.
authorize_urlhttps://btelo.com/api/oauth/authorize
jwks_urlhttps://btelo.com/api/oauth/jwks.json
redirect_uri_policyMust be in the integration's registered redirect_uris allow-list, OR any https://*.btelo.com URI (auto-allowed).
supported_grantsauthorization_code
token_urlhttps://btelo.com/api/oauth/token
userinfo_urlhttps://btelo.com/api/oauth/userinfo

Error responses

401Missing / invalid auth — HMAC signature mismatch, stale timestamp, unknown key_id, or disabled integration.
403Authenticated but not authorized (e.g. calling /admin/* without is_admin).
404Resource not found (user, integration, subscription, etc.).
409Conflict (e.g. creating an integration whose id already exists).
429Rate limit (currently not enforced).
5xxServer error. Retry with exponential backoff.
formatErrors are JSON {"error":"<message>"} for JSON endpoints. Some older endpoints return plain text.

Conventions

content_typeapplication/json; charset=utf-8 for JSON endpoints.
idsUser IDs are 32-char hex. Integration IDs are human slugs (e.g. "vibe-remote").
paginationNot used — list endpoints return full arrays (bounded collections).
timestampsRFC3339 for body/response fields. Unix seconds for HMAC headers.

Brand assets

Official btelo logo. Use the SVG where possible; use the white PNG on dark backgrounds.

SVG (animated)SVG (animated)https://btelo.com/public/logo-spin.svg
PNG — blackPNG — blackhttps://btelo.com/public/logo-spin.png
PNG — whitePNG — whitehttps://btelo.com/public/logo-spin-white.png

Endpoints

Blog

GET/api/blog/posts

Returns the N most recently published blog posts.

NameInTypeRequiredDescription
nqueryintegeroptionalNumber of posts to return (1–20).

Response

content_typeapplication/json
schema
countinteger — number of posts returned
post_fields
author_nameAuthor display name (omitted when none)
cover_image_urlAbsolute cover image URL (omitted when none)
published_atPublication date (YYYY-MM-DD)
slugURL slug of the post
summaryShort summary / excerpt
titlePost title
urlCanonical post URL
view_countTotal view count
postsarray of post objects

Example

GET https://btelo.com/api/blog/posts?n=5

OAuth2 SSO

GET/api/oauth/authorize

Start an OAuth 2.0 authorization code flow. Redirects the user to the btelo.com login page, then back to your redirect_uri with ?code=... on success. Desktop/mobile clients must use PKCE (RFC 7636) by passing code_challenge — this allows the subsequent /api/oauth/token call to omit client_secret.

NameInTypeRequiredDescription
client_idquerystringrequiredYour integration_id.
redirect_uriquerystringrequiredURI to redirect to after authorization. Must match a registered URI for the client.
response_typequerystringrequiredMust be "code".
statequerystringoptionalOpaque value echoed back in the redirect to prevent CSRF.
scopequerystringoptionalSpace-separated scopes (e.g. "openid profile email").
code_challengequerystringoptionalPKCE code challenge: base64url(SHA256(code_verifier)). When present, /api/oauth/token requires the matching code_verifier and does not require client_secret.
code_challenge_methodquerystringoptionalOnly "S256" is supported (default when code_challenge is set). "plain" is rejected.

Response

error302 redirect to redirect_uri?error=...
success302 redirect to redirect_uri?code=AUTH_CODE&state=...

Example

GET https://btelo.com/api/oauth/authorize?client_id=CLIENT_ID&redirect_uri=https://yourapp.com/callback&response_type=code&state=xyz&scope=openid+profile+email&code_challenge=CHALLENGE&code_challenge_method=S256
POST/api/oauth/token

Exchange an authorization code for a JWT access token, or refresh an existing token. Two flows are supported: (1) confidential-client flow requires client_secret; (2) PKCE public-client flow (for desktop/mobile apps) requires code_verifier instead — client_secret is optional and may be omitted entirely.

NameInTypeRequiredDescription
grant_typebody (form)stringrequired"authorization_code" or "refresh_token".
codebody (form)stringoptionalThe authorization code from /api/oauth/authorize (required when grant_type=authorization_code).
redirect_uribody (form)stringoptionalMust match the redirect_uri used in /api/oauth/authorize.
refresh_tokenbody (form)stringoptionalRequired when grant_type=refresh_token.
client_idbody (form)stringrequiredYour integration_id.
client_secretbody (form)stringoptionalIntegration secret. Required for confidential-client flow (no code_challenge at authorize time). Optional for PKCE flow — omit it on desktop/mobile clients that cannot safely hold a secret.
code_verifierbody (form)stringoptionalPKCE verifier (RFC 7636). Required when grant_type=authorization_code and the original /api/oauth/authorize call used code_challenge. Server verifies base64url(SHA256(code_verifier)) == stored code_challenge.

Response

content_typeapplication/json
schema
access_tokenSigned JWT (RS256) — use as Bearer token
expires_inSeconds until the access token expires
refresh_tokenOpaque token to obtain a new access token
scopeGranted scopes
token_type"Bearer"

Example

POST https://btelo.com/api/oauth/token  body (PKCE): grant_type=authorization_code&code=CODE&redirect_uri=...&client_id=...&code_verifier=VERIFIER
GET/api/oauth/userinfo

Returns the authenticated user's profile. Requires a valid Bearer access token obtained from /api/oauth/token.

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <access_token>

Response

content_typeapplication/json
schema
emailUser email address
nameDisplay name
pictureAvatar URL
subUnique user ID

Example

GET https://btelo.com/api/oauth/userinfo  Authorization: Bearer <access_token>
GET/api/oauth/jwks.json

Returns the RSA public key set (JWKS) used to verify JWT access tokens issued by /api/oauth/token.

Response

content_typeapplication/json
schemaRFC 7517 JSON Web Key Set

Example

GET https://btelo.com/api/oauth/jwks.json

Auth

GET/api/auth/me

Returns the currently logged-in user's profile (session cookie required). Returns 401 if not authenticated.

Response

content_typeapplication/json
schema
avatar_urlAvatar URL
bioUser bio / description
emailEmail address
idUser ID
nameDisplay name
owner_appsArray of app slugs this user owns, e.g. ["vibe-remote"].
providerAuth provider (google, email)

Example

GET https://btelo.com/api/auth/me

Profile

GET/api/profile

Returns the authenticated user's editable profile. Auth: session cookie (browser) or Btelo-Api-Key + user_id (server-to-server).

Response

content_typeapplication/json
schema
avatar_urlAvatar URL
bioUser bio / description
emailEmail address
idUser ID
nameDisplay name
owner_appsArray of app slugs this user owns, e.g. ["vibe-remote"].
providerAuth provider (google, email)

Example

GET https://btelo.com/api/profile  (Cookie: btelo_session=... or Authorization: Bearer ...)
PUT/api/profile

Update the authenticated user's profile. Send only the fields you want to change. Auth: session cookie (browser) or Btelo-Api-Key + user_id (server-to-server).

NameInTypeRequiredDescription
namebody (JSON)stringoptionalDisplay name.
avatar_urlbody (JSON)stringoptionalAvatar image URL.
biobody (JSON)stringoptionalShort bio / description.

Response

content_typeapplication/json
schema
avatar_urlAvatar URL (updated)
bioBio (updated)
emailEmail address
idUser ID
nameDisplay name (updated)
owner_appsArray of app slugs this user owns.
providerAuth provider

Example

PUT https://btelo.com/api/profile  body: {"name":"New Name","bio":"Hello world"}
POST/api/account/delete

Delete the currently logged-in account. Requires a browser session cookie; HMAC/API-key auth is intentionally not accepted. The user must manually type their full user id and send it as user_id_confirm. The backend validates an exact match before deleting. Account/login/app state is removed; historical order rows are retained for legal/accounting audit. This endpoint does not cancel Apple or Stripe subscriptions; users must cancel those in Apple subscriptions or Stripe billing.

NameInTypeRequiredDescription
Cookieheaderstringrequiredbtelo_session=<session token>.
user_id_confirmbody (JSON or form)stringrequiredThe exact current user's 32-char btelo user id, typed manually by the user.

Response

content_typeapplication/json for JSON requests; otherwise 303 redirect
errors401 when not logged in; 400 when user_id_confirm is missing or does not exactly match the session user id.
success
{
  "ok": true,
  "deleted": true
}

Example

POST https://btelo.com/api/account/delete  Cookie: btelo_session=...  body: {"user_id_confirm":"32hexuserid"}
POST/api/profile/inviter

Bind the authenticated user to an inviter by invite code. This is allowed after registration only if the user does not already have an inviter. Once set, the inviter cannot be changed. Future successful subscription payments use this inviter relationship to apply regular invite rewards or share-partner commission ledger entries.

NameInTypeRequiredDescription
invite_codebody (JSON or form)stringrequiredInvite code to bind. `code` is also accepted as an alias.

Response

content_typeapplication/json
errors
400Invalid code, unknown code, or self-invite.
409The user already has an inviter and it cannot be changed.
schema
invite_codeNormalized invite code snapshot
inviter_idUser ID of the inviter
oktrue on success
source"regular" or "affiliate", frozen at bind time

Example

POST https://btelo.com/api/profile/inviter  body: {"invite_code":"streamer-jenny"}

Email

POST/api/email/send

Send an email to a user immediately, or schedule it for a future time. Requires Btelo-Api-Key header.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringrequiredAPI key ID.
Btelo-TsheaderstringrequiredUnix timestamp (±5 min).
Btelo-SignheaderstringrequiredFirst 16 hex chars of HMAC-SHA256(key_id:timestamp, secret).
user_idbody (JSON)stringrequiredTarget user ID.
subjectbody (JSON)stringrequiredEmail subject line.
htmlbody (JSON)stringrequiredEmail HTML body.
send_atbody (JSON)stringoptionalRFC3339 timestamp for scheduled send (e.g. "2026-04-20T10:00:00Z"). Omit for immediate send.

Response

content_typeapplication/json
schema
emailTarget email address
idScheduled email ID
send_atScheduled send time (RFC3339)
status"sending" (immediate) or "scheduled" (future)
subjectEmail subject
user_idTarget user ID

Example

POST https://btelo.com/api/email/send  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...  body: {"user_id":"abc","subject":"Hello","html":"<h1>Hi</h1>"}
GET/api/email/scheduled

List scheduled emails. Optionally filter by user_id. Requires Btelo-Api-Key header.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringrequiredAPI key ID.
Btelo-TsheaderstringrequiredUnix timestamp (±5 min).
Btelo-SignheaderstringrequiredFirst 16 hex chars of HMAC-SHA256(key_id:timestamp, secret).
user_idquerystringoptionalFilter by target user ID.

Response

content_typeapplication/json
schema
emailsArray of {id, user_id, subject, send_at, sent_at, canceled}

Example

GET https://btelo.com/api/email/scheduled?user_id=abc  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...
DELETE/api/email/scheduled/{id}

Cancel a pending scheduled email before it is sent. Requires Btelo-Api-Key header.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringrequiredAPI key ID.
Btelo-TsheaderstringrequiredUnix timestamp (±5 min).
Btelo-SignheaderstringrequiredFirst 16 hex chars of HMAC-SHA256(key_id:timestamp, secret).
idpathstringrequiredScheduled email ID.

Response

status204 No Content on success, 404 if not found or already sent

Example

DELETE https://btelo.com/api/email/scheduled/abc123  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...

Utility

GET/api/health

Health check. Returns 200 OK with body "ok".

Response

bodyok
content_typetext/plain

Example

GET https://btelo.com/api/health
GET/api/doc

Returns this API documentation as JSON.

Response

content_typeapplication/json

Example

GET https://btelo.com/api/doc

Product API

GET/api/v1/tiers

List all available subscription tiers with pricing. Auth: Bearer user JWT OR HMAC.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended for client apps).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative to Bearer).
Btelo-TsheaderstringoptionalUnix timestamp (±5 min). Required with HMAC.
Btelo-SignheaderstringoptionalHMAC signature. Required with HMAC.

Response

content_typeapplication/json
schema
{
  tiers: [
    {
      slug,
      name,
      level,
      monthly_cents,
      yearly_cents,
      trial_days,
      monthly_credit_cap
    }
  ]
}—monthly_credit_capistheAI-creditallowancegrantedeachcalendarmonthatthistier(0fortierswithnoallowance).

Example

GET https://btelo.com/api/v1/tiers  Authorization: Bearer <user_jwt>
GET/api/v1/subscription

Get a user's current subscription status. Auth: Bearer user JWT (user_id inferred from token) OR HMAC + user_id. With Bearer, query user_id is ignored.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalBtelo user ID. Required for HMAC; ignored for Bearer.
tierquerystringoptionalCheck if user has access to this tier level (returns has_access field).

Response

content_typeapplication/json
schema
{
  active,
  had_subscription,
  tier?,
  status?,
  plan?,
  source?,
  platform?,
  app_id?,
  cancel_at_period_end?,
  current_period_end?,
  canceled_at?,
  has_access?
}—active=truewhentheeffectiveunifiedbillingsourceisactive/trialing/grace.source/platformmaybestripe,
apple,
ormanual.Whenactive=false,
fieldsdescribetheMOSTRECENTsubscriptionsourcesotheclientcandistinguish'neversubscribed'from'expired'.

Example

GET https://btelo.com/api/v1/subscription?tier=pro  Authorization: Bearer <user_jwt>
POST/api/v1/subscription/checkout-url

Create a Stripe Checkout URL for a subscription. Auth: Bearer user JWT OR HMAC + user_id.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalRequired for HMAC; ignored for Bearer.
tierbody (JSON)stringrequiredTier slug (starter, pro, pro-plus).
planbody (JSON)stringrequiredmonthly or yearly.
success_urlbody (JSON)stringrequiredURL to redirect after successful checkout.
cancel_urlbody (JSON)stringrequiredURL to redirect if user cancels.

Response

content_typeapplication/json
schema
{
  url: "https: //checkout.stripe.com/..."
}

Example

POST https://btelo.com/api/v1/subscription/checkout-url  Authorization: Bearer <user_jwt>  body: {"tier":"pro","plan":"monthly","success_url":"https://yourapp.com/ok","cancel_url":"https://yourapp.com/cancel"}
GET/api/v1/credits

Get a user's credit balance. Auth: Bearer user JWT OR HMAC + user_id.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalRequired for HMAC; ignored for Bearer.

Response

content_typeapplication/json
schema
{
  user_id,
  credits
}

Example

GET https://btelo.com/api/v1/credits  Authorization: Bearer <user_jwt>
POST/api/v1/credits/deduct

Deduct credits from a user's balance. Returns 402 if insufficient. Auth: Bearer user JWT (self-deduct) OR HMAC + user_id (any user).

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (self-deduct only).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalRequired for HMAC; ignored for Bearer.
amountbody (JSON)integerrequiredNumber of credits to deduct.
reasonbody (JSON)stringoptionalReason for deduction (logged).

Response

content_typeapplication/json
schema
{
  user_id,
  deducted,
  remaining
}

Example

POST https://btelo.com/api/v1/credits/deduct  Authorization: Bearer <user_jwt>  body: {"amount":100,"reason":"image generation"}
POST/api/v1/credits/checkout-url

Create a Stripe Checkout URL for a one-time credit purchase. Auth: Bearer user JWT OR HMAC + user_id.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalRequired for HMAC; ignored for Bearer.
slugbody (JSON)stringrequiredCredit product slug (credits-500, credits-1000, credits-5000).
success_urlbody (JSON)stringrequiredURL to redirect after successful checkout.
cancel_urlbody (JSON)stringrequiredURL to redirect if user cancels.

Response

content_typeapplication/json
schema
{
  url: "https: //checkout.stripe.com/..."
}

Example

POST https://btelo.com/api/v1/credits/checkout-url  Authorization: Bearer <user_jwt>  body: {"slug":"credits-500","success_url":"https://yourapp.com/ok","cancel_url":"https://yourapp.com/cancel"}
POST/api/v1/entitlements/quote

Calculate the wallet credit cost for a product entitlement price. This does not require btelo-side SKU configuration: product servers choose their own SKU names and dollar prices, and btelo converts price_cents to credits using the same credit price curve used by credit purchases. Auth: Bearer user JWT OR HMAC.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt>.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
price_centsbody (JSON)integerrequiredDollar price in cents, e.g. 5000 for $50. btelo returns the matching cost_credits.

Response

content_typeapplication/json
schema
{
  price_cents,
  cost_credits,
  basis
}—basisis"btelo_credit_price_curve".

Example

POST https://btelo.com/api/v1/entitlements/quote  Authorization: Bearer <user_jwt>  body: {"price_cents":5000}
GET/api/v1/entitlements

List active product entitlements for a user, or check one product/SKU pair. Auth: Bearer user JWT (user_id inferred from token) OR HMAC + user_id. With Bearer, query user_id is ignored.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt>.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalRequired for HMAC; ignored for Bearer.
productquerystringoptionalProduct integration slug, e.g. vibe-remote.
skuquerystringoptionalProduct-defined SKU, e.g. lifetime or pro-lifetime. When product and sku are both provided, response includes active.

Response

content_typeapplication/json
schema
{
  user_id,
  entitlements: [
    {
      id,
      product,
      sku,
      name,
      kind,
      price_cents,
      cost_credits,
      granted_at,
      expires_at?
    }
  ],
  active?,
  product?,
  sku?
}

Example

GET https://btelo.com/api/v1/entitlements?product=vibe-remote&sku=lifetime  Authorization: Bearer <user_jwt>
POST/api/v1/entitlements/redeem

Redeem wallet credits for a product entitlement. This is the recommended buyout/lifetime access flow. No btelo-side product/SKU configuration is required: the authenticated product backend sends its own SKU and price_cents. This endpoint is HMAC-only so users cannot forge dynamic prices from the browser. The product defaults to Btelo-Key-Id and, if provided, must match Btelo-Key-Id. Currently only lifetime entitlements are supported. Repeated redemption of the same active user/product/sku returns the existing entitlement without deducting credits again.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringrequiredIntegration ID. This becomes the product slug unless body.product is provided with the same value.
Btelo-TsheaderstringrequiredUnix timestamp (±5 min).
Btelo-SignheaderstringrequiredFirst 16 hex chars of HMAC-SHA256(key_id:timestamp, secret).
user_idquery or X-User-ID headerstringrequiredBtelo user ID receiving the entitlement.
productbody (JSON)stringoptionalProduct slug. Omit to use Btelo-Key-Id. If provided, it must match Btelo-Key-Id.
skubody (JSON)stringoptionalProduct-defined SKU. Defaults to lifetime.
namebody (JSON)stringoptionalDisplay name for audit/support views.
kindbody (JSON)stringoptionalEntitlement kind. Defaults to lifetime; only lifetime is currently supported.
price_centsbody (JSON)integerrequiredProduct price in cents. btelo computes cost_credits from this value using the credit price curve.
idempotency_keybody (JSON)stringoptionalOptional product-side idempotency key. When omitted, btelo uses user_id:product:sku:kind.

Response

content_typeapplication/json
errors
401Missing or invalid HMAC headers. Bearer JWT is not accepted for redeem.
402Insufficient credits.
403body.product does not match Btelo-Key-Id.
schema
{
  user_id,
  created,
  remaining,
  cost_credits,
  entitlement: {
    id,
    product,
    sku,
    name,
    kind,
    price_cents,
    cost_credits,
    granted_at
  }
}

Example

POST https://btelo.com/api/v1/entitlements/redeem?user_id=usr_123  Btelo-Key-Id: vibe-remote  Btelo-Ts: ...  Btelo-Sign: ...  body: {"sku":"lifetime","name":"Vibe Remote Lifetime","price_cents":5000}
GET/api/v1/ownership

Check whether a user is marked as an owner of an app. Btelo admins set owner apps on the user detail page. Auth: Bearer user JWT (user_id inferred from token) OR HMAC + user_id. With Bearer, query user_id is ignored.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
user_idquerystringoptionalRequired for HMAC; ignored for Bearer.
appquerystringoptionalApp slug to check, e.g. vibe-remote. When omitted, the response only lists owner_apps.

Response

content_typeapplication/json
schema
{
  user_id,
  owner_apps,
  app?,
  is_owner?
}—owner_appsisthecompletearrayofappslugs.appandis_ownerarepresentwhenqueryappisprovided.

Example

GET https://btelo.com/api/v1/ownership?app=vibe-remote  Authorization: Bearer <user_jwt>

AI Proxy

GET/api/ai/readme

Returns the AI proxy API documentation as JSON. Lists all AI endpoints, auth requirements, and request/response formats.

Response

content_typeapplication/json

Example

GET https://btelo.com/api/ai/readme
GET/api/ai/quota

Get the caller's current monthly AI-credit allowance and wallet balance. Use this to render a usage gauge before making AI calls. Auth: Bearer user JWT (required — HMAC not supported on this endpoint).

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <user_jwt>.

Response

content_typeapplication/json
schema
{
  tier,
  monthly_cap,
  allowance_used,
  allowance_remaining,
  wallet_credits,
  total_available,
  month
}—monthly_cap=AI-creditallowancefortheuser'stierthiscalendarmonth(0ifnoactivesubscription);allowance_used=consumedthismonth;allowance_remaining=max(0,
cap-used);wallet_credits=purchasedone-timecredits;total_available=allowance_remaining+wallet_credits;month='2006-01'UTC.

Example

GET https://btelo.com/api/ai/quota  Authorization: Bearer <user_jwt>
POST/v1/images/generations

OpenAI image generation with auto-resize. Auth: Bearer user JWT OR HMAC. Bearer calls are metered per-user.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug. Required on Bearer path.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.

Example

POST https://btelo.com/v1/images/generations  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up
POST/v1/audio/speech

OpenAI text-to-speech proxy. Auth: Bearer user JWT OR HMAC. Bearer calls are metered per-user.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug. Required on Bearer path.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.

Example

POST https://btelo.com/v1/audio/speech  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up
GET/api/ai/pricing

Public price list for every enabled model, normalized to credits. Chat rates are credits per 1k input/output/cache-read/cache-write tokens; image rates are credits per single image for each (model,size,quality) combo; TTS rates are credits per 1k characters. Use this to label model pickers with authoritative, always-fresh cost info — re-fetch on startup and whenever model admin state may have changed.

Response

content_typeapplication/json
schema
{
  chat: [
    {
      provider,
      model,
      max_output_tokens,
      input_credits_per_1k,
      output_credits_per_1k,
      cache_read_credits_per_1k,
      cache_write_credits_per_1k
    }
  ],
  image: [
    {
      provider,
      model,
      size,
      quality,
      credits_per_image
    }
  ],
  tts: [
    {
      provider,
      model,
      credits_per_1k_chars
    }
  ],
  video: [
    {
      provider,
      model,
      resolution,
      duration_seconds,
      credits_per_video
    }
  ],
  music: [
    {
      provider,
      model,
      credits_per_song
    }
  ],
  unit,
  update
}

Example

GET https://btelo.com/api/ai/pricing
POST/v1/images/icon-pack

Convert one user-uploaded image into a fully-spec-compliant macOS + iOS app-icon bundle. Returns a ZIP containing: macos/icon.icns (drop-in binary), macos/icon.iconset/* (Apple squircle template, 16-1024 px with @2x), and ios/AppIcon.appiconset/* (iPhone + iPad full-bleed PNGs at every standard size, plus 1024 marketing image and a preconfigured Contents.json). Pure CPU work — no upstream AI is called. Flat 50 credits per request; free when called with Btelo-Product: btelo-up.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug. Required on Bearer path. Setting it to "btelo-up" waives the credit charge.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
imagebody (multipart/form-data)filerequiredSource image (PNG or JPEG). Any aspect/size — will be center-fit and resized to a 1024×1024 working canvas.

Response

content_typeapplication/zip
schemaZIP archive: macos/icon.icns, macos/icon.iconset/icon_NxN[@2x].png, ios/AppIcon.appiconset/Contents.json + Icon-N.png set, README.txt.

Example

POST https://btelo.com/v1/images/icon-pack  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up  -F [email protected]
POST/v1/minimax/video/generation

MiniMax video generation (synchronous wrapper — the server submits the async task, polls every 5s up to 5 min, then returns the final download URL). Billed once at submission per (model, resolution, duration); full refund on upstream failure or timeout. The (model, resolution, duration) triple must match an enabled row in the video price table (see GET /api/ai/pricing → video). Auth: Bearer user JWT OR HMAC.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug. Required on Bearer path.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
modelbody (JSON)stringrequiredMiniMax video model slug (e.g. MiniMax-Hailuo-02, MiniMax-Hailuo-2.3, MiniMax-Hailuo-2.3-Fast).
promptbody (JSON)stringrequiredText description of the desired video.
resolutionbody (JSON)stringrequired512P | 768P | 1080P (must match enabled price row for the model).
durationbody (JSON)integerrequiredVideo duration in seconds: 6 or 10 (must match enabled price row).
prompt_optimizerbody (JSON)booleanoptionalEnable MiniMax prompt optimization.

Response

content_typeapplication/json
schema
{
  status: "success",
  task_id,
  file_id,
  download_url
}

Example

POST https://btelo.com/v1/minimax/video/generation  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up  body: {"model":"MiniMax-Hailuo-02","prompt":"a cat surfing","resolution":"768P","duration":6}
POST/v1/minimax/music/generation

MiniMax music generation (synchronous). Flat per-song charge for tracks up to 5 minutes. model must be an enabled music-* slug (music-2.6, music-2.5+, music-2.5, music-2.0). Auth: Bearer user JWT OR HMAC.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug. Required on Bearer path.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
modelbody (JSON)stringrequiredEnabled music-* model slug (see GET /api/ai/pricing → music).
lyricsbody (JSON)stringoptionalLyric text to sing (MiniMax schema).
promptbody (JSON)stringoptionalStyle / mood prompt.
audio_settingbody (JSON)objectoptionalMiniMax audio config (see MiniMax docs).

Response

content_typeapplication/json
schemaMiniMax music_generation response (passthrough).

Example

POST https://btelo.com/v1/minimax/music/generation  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up  body: {"model":"music-2.6","lyrics":"...","prompt":"upbeat pop"}
POST/v1/minimax/music/lyrics

MiniMax lyrics generation. Flat per-song charge billed against the virtual 'lyrics-gen' model (see GET /api/ai/pricing → music). Auth: Bearer user JWT OR HMAC.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug. Required on Bearer path.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
promptbody (JSON)stringrequiredTheme / style prompt for the lyrics.

Response

content_typeapplication/json
schemaMiniMax lyrics response (passthrough).

Example

POST https://btelo.com/v1/minimax/music/lyrics  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up  body: {"prompt":"a wistful autumn ballad"}

AI Proxy · Protocol A · Anthropic Messages

POST/v1/messages

Anthropic Messages API wire format. Integrate with @anthropic-ai/sdk or any Anthropic-compatible client — callers see pure Anthropic semantics (messages, content blocks, tool_use, stop_reason). Upstream happens to be MiniMax's anthropic-compat endpoint, but that is an implementation detail. Distinct from /v1/responses (Codex) and /v1/chat/completions (OpenAI). Auth: Bearer user JWT OR HMAC. Bearer calls are metered against the user's subscription tier + wallet.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (recommended).
Btelo-ProductheaderstringoptionalRegistered product slug (e.g. btelo-up). Required on Bearer path.
Btelo-Key-IdheaderstringoptionalHMAC key ID (alternative).
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.

Example

POST https://btelo.com/v1/messages  Authorization: Bearer <user_jwt>  Btelo-Product: btelo-up

AI Proxy · Protocol B · OpenAI Chat Completions

POST/v1/chat/completions

OpenAI Chat Completions wire format. Use with OpenAI SDKs and legacy OpenAI-style clients. NOTE: codex 0.95+ does NOT use this path — codex speaks Responses API only (see /v1/responses). Distinct from /v1/messages (Anthropic) and /v1/responses (Codex). Supports streaming. Requires AI API key (HMAC headers or user JWT).

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringoptionalAPI key ID (HMAC path).
Btelo-TsheaderstringoptionalUnix timestamp (±5 min). Required with HMAC.
Btelo-SignheaderstringoptionalFirst 16 hex chars of HMAC-SHA256(key_id:timestamp, secret). Required with HMAC.
AuthorizationheaderstringoptionalBearer <user_jwt> as alternative to HMAC.
modelbody (JSON)stringoptionalMiniMax model id (e.g. MiniMax-M2.7). Defaults to the active MiniMax config model.
messagesbody (JSON)arrayrequiredOpenAI-format chat messages.
streambody (JSON)booleanoptionalIf true, server streams SSE deltas.

Example

POST https://btelo.com/v1/chat/completions  body: {"model":"MiniMax-M2.7","messages":[{"role":"user","content":"hi"}]}

Admin API

POST/admin/integrations

Register a new integration. Auth: admin session cookie OR HMAC from an integration with is_admin=true. Returns the created integration including the generated secret.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringoptionalAdmin integration ID (HMAC auth path). Omit when using session cookie.
Btelo-TsheaderstringoptionalUnix timestamp (±5 min). Required with HMAC auth.
Btelo-SignheaderstringoptionalHMAC signature. Required with HMAC auth.
idbody (JSON)stringrequiredURL-safe slug chosen by caller (e.g. "auto-social"). Must be unique.
namebody (JSON)stringrequiredHuman-readable product name.
redirect_urisbody (JSON)array<string>optionalAllowed OAuth redirect URIs. Omit/empty if the integration only uses HMAC API. URIs under *.btelo.com are auto-allowed.
is_adminbody (JSON)booleanoptionalIf true, the new integration can call /admin/* endpoints. Default false.

Response

content_typeapplication/json
errors400 invalid body · 409 id already exists
schema
created_atYYYY-MM-DD HH:MM:SS
disabledboolean
idintegration_id
is_adminboolean
namedisplay name
redirect_urisarray of registered redirect URIs
secretplaintext secret (bti_<hex>). Store securely. Can be viewed again via GET.
status201 Created

Example

POST https://btelo.com/admin/integrations  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...  body: {"id":"auto-social","name":"Auto Social","redirect_uris":["https://auto-social.com/auth/cb"]}
GET/admin/integrations

List all registered integrations. Secrets are included (plaintext) because they must be retrievable for re-display.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringoptionalAdmin integration ID (HMAC auth path).
Btelo-TsheaderstringoptionalUnix timestamp.
Btelo-SignheaderstringoptionalHMAC signature.

Response

content_typeapplication/json
schemaarray of integration objects (same shape as POST /admin/integrations response)
status200 OK

Example

GET https://btelo.com/admin/integrations  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...
PATCH/admin/integrations/{id}

Update an existing integration's name, redirect URIs, and admin flag. The secret is NOT changed (use /rotate for that). Pass the full desired redirect_uris list — the stored list is replaced, not merged.

NameInTypeRequiredDescription
idpathstringrequiredintegration_id to update.
Btelo-Key-IdheaderstringoptionalAdmin integration ID (HMAC auth path). Omit when using session cookie.
Btelo-TsheaderstringoptionalUnix timestamp (±5 min). Required with HMAC auth.
Btelo-SignheaderstringoptionalHMAC signature. Required with HMAC auth.
namebody (JSON)stringrequiredHuman-readable product name.
redirect_urisbody (JSON)array<string>optionalFull replacement list of allowed OAuth redirect URIs. Send [] to clear. URIs under *.btelo.com are auto-allowed regardless.
is_adminbody (JSON)booleanoptionalWhether this integration can call /admin/* endpoints.

Response

content_typeapplication/json
errors400 invalid body or empty name · 404 integration not found
schemaintegration object (same shape as POST /admin/integrations response)
status200 OK

Example

PATCH https://btelo.com/admin/integrations/vibe-remote  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...  body: {"name":"Vibe Remote","redirect_uris":["https://vibe-remote.com/auth/cb","https://app.vibe-remote.com/auth/cb"],"is_admin":false}
DELETE/admin/integrations/{id}

Delete an integration permanently. Existing OAuth tokens already issued keep working until expiry; new logins and HMAC calls are rejected immediately.

NameInTypeRequiredDescription
idpathstringrequiredintegration_id to delete.
Btelo-Key-IdheaderstringoptionalAdmin integration ID (HMAC auth path).
Btelo-TsheaderstringoptionalUnix timestamp.
Btelo-SignheaderstringoptionalHMAC signature.

Response

status204 No Content

Example

DELETE https://btelo.com/admin/integrations/vibe-remote  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...
POST/admin/integrations/{id}/rotate

Rotate the secret of an existing integration. The previous secret is invalidated immediately. Returns the new plaintext secret.

NameInTypeRequiredDescription
idpathstringrequiredintegration_id.
Btelo-Key-IdheaderstringoptionalAdmin integration ID (HMAC auth path).
Btelo-TsheaderstringoptionalUnix timestamp.
Btelo-SignheaderstringoptionalHMAC signature.

Response

content_typeapplication/json
schema
idintegration_id
secretnew plaintext secret (bti_<hex>)
status200 OK

Example

POST https://btelo.com/admin/integrations/vibe-remote/rotate  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...
POST/admin/integrations/{id}/disable

Enable or disable an integration without deleting it. Disabled integrations cannot authenticate (OAuth and HMAC both return 401).

NameInTypeRequiredDescription
idpathstringrequiredintegration_id.
disabledbody (JSON)booleanrequiredtrue to disable, false to re-enable.
Btelo-Key-IdheaderstringoptionalAdmin integration ID (HMAC auth path).
Btelo-TsheaderstringoptionalUnix timestamp.
Btelo-SignheaderstringoptionalHMAC signature.

Response

status204 No Content

Example

POST https://btelo.com/admin/integrations/vibe-remote/disable  Btelo-Key-Id: ... Btelo-Ts: ... Btelo-Sign: ...  body: {"disabled":true}

AI Proxy · Protocol C · OpenAI Responses (Codex)

POST/v1/responses

OpenAI Responses API wire format — the protocol codex 0.95+ speaks to custom providers. Input items are message / function_call / function_call_output; tools are declared as flat {type,name,parameters}. Always SSE; events follow the response.* schema (response.created, response.output_item.added, response.output_text.delta, response.output_item.done, response.completed). Internally translated to Chat Completions before forwarding upstream and streamed back as Responses events. Distinct from /v1/messages (Anthropic protocol) and /v1/chat/completions (OpenAI Chat protocol) — they are NOT interchangeable. Codex clients SHOULD set model_provider.proxy.wire_api="responses" in ~/.codex/config.toml. Same auth, billing, and quota pipeline as the other AI proxy paths.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (preferred), or use HMAC headers.
inputbody (JSON)arrayrequiredResponses input items: message / function_call / function_call_output.
instructionsbody (JSON)stringoptionalSystem-level instructions (prepended as a system message upstream).
toolsbody (JSON)arrayoptionalResponses tool declarations (flat {type,name,parameters} shape).
streambody (JSON)booleanoptionalAlways set true for codex; the endpoint always emits SSE.

Example

POST https://btelo.com/v1/responses  body: {"model":"MiniMax-M2.7","instructions":"be helpful","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}],"stream":true}

AI Proxy · Protocol D · MiniMax Coding Plan MCP

POST/v1/coding_plan/search

Web-search tool exposed by the minimax-coding-plan-mcp server (the `web_search` MCP tool). Returns a structured JSON of organic results with title / link / snippet. Preferred over shell `curl` inside agent sandboxes — avoids dumping raw HTML into function_call_output. Same auth + billing pipeline as the other /v1 AI proxy routes.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (preferred), or use HMAC headers.
querybody (JSON)stringrequiredSearch query. 3–5 keywords works best; include a date for time-sensitive topics.

Example

POST https://btelo.com/v1/coding_plan/search  body: {"query":"codex CLI mcp config"}
POST/v1/coding_plan/vlm

Image-understanding tool exposed by the minimax-coding-plan-mcp server (the `understand_image` MCP tool). Analyzes images with AI based on text prompts. Supports JPEG/PNG/GIF/WebP up to 20MB. Same auth + billing pipeline as the other /v1 AI proxy routes.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt> (preferred), or use HMAC headers.
promptbody (JSON)stringrequiredQuestion or instruction about the image.
imagebody (JSON)stringrequiredImage as URL, absolute path, or base64 data URI.

Example

POST https://btelo.com/v1/coding_plan/vlm  body: {"prompt":"what is in this screenshot?","image":"https://.../shot.png"}

GET/api/ai/quota

Caller's current monthly allowance + wallet (requires user JWT).

GET/api/ai/usage

Caller's recent AI usage rows (requires user JWT). ?month=YYYY-MM optional.

User Meta

GET/api/meta

List every meta blob owned by the caller. Each entry has `kind`, `data` (verbatim JSON you stored), and `updated_at`.

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <user_jwt>.

Response

content_typeapplication/json
schema
{
  metas: [
    {
      kind: string,
      data: any,
      updated_at: string
    }
  ]
}

Example

GET https://btelo.com/api/meta  Authorization: Bearer <jwt>
GET/api/meta/{kind}

Fetch a single meta blob by kind. 404 if absent. `kind` must match [a-z0-9_.-]{1,64}.

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <user_jwt>.
kindpathstringrequiredNamespace slug for the blob (e.g. "settings", "editor.prefs").

Response

content_typeapplication/json
schema
{
  kind: string,
  data: any,
  updated_at: string
}

Example

GET https://btelo.com/api/meta/settings  Authorization: Bearer <jwt>
PUT/api/meta/{kind}

Create-or-update the blob under kind. Body must be valid JSON (object, array, number, string, boolean, or null) and ≤ 256 KiB. (user_id, kind) is the unique key — there is no separate POST/create.

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <user_jwt>.
kindpathstringrequiredNamespace slug for the blob.
bodybody (JSON)anyrequiredArbitrary JSON value stored verbatim.

Response

content_typeapplication/json
schema
{
  kind: string,
  data: any,
  updated_at: string
}

Example

PUT https://btelo.com/api/meta/settings  Authorization: Bearer <jwt>  body: {"theme":"dark","lang":"en"}
DELETE/api/meta/{kind}

Remove the blob under kind. Idempotent — returns 204 even if the kind was already absent.

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <user_jwt>.
kindpathstringrequiredNamespace slug for the blob.

Response

content_type(empty)
schema204 No Content

Example

DELETE https://btelo.com/api/meta/settings  Authorization: Bearer <jwt>

Billing API

GET/api/v1/billing/config

Return store product IDs for one app/platform. Client apps use this to discover StoreKit product IDs without hardcoding them. Auth: Bearer user JWT OR HMAC.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt>.
Btelo-Key-IdheaderstringoptionalHMAC key ID.
app_idquerystringrequiredRegistered billing app id, e.g. btelo-english.
platformquerystringrequiredStore platform, currently apple or stripe.

Response

content_typeapplication/json
schema
{
  app_id,
  name,
  platform,
  ios_bundle_id?,
  products: [
    {
      kind,
      store_product_id,
      tier?,
      plan?,
      credit_slug?,
      credits?,
      cents?
    }
  ]
}

Example

GET https://btelo.com/api/v1/billing/config?app_id=btelo-english&platform=apple  Authorization: Bearer <user_jwt>
POST/api/v1/iap/apple/transactions

Sync an Apple StoreKit purchase to btelo. The app sends the transaction_id after purchase; btelo verifies it with Apple's App Store Server API, checks the configured bundle/product mapping, then records either a unified billing subscription or a one-time wallet credit transaction. The app should pass StoreKit appAccountToken during purchase when possible so Apple server notifications can be routed even if they arrive before this client sync. Auth: Bearer user JWT.

NameInTypeRequiredDescription
AuthorizationheaderstringrequiredBearer <user_jwt> for the purchasing user.
app_idbody (JSON)stringrequiredRegistered billing app id.
transaction_idbody (JSON)stringrequiredStoreKit Transaction.id.

Response

content_typeapplication/json
schemaSubscription response: { kind:'subscription', subscription:{ active, tier, status, plan, source, platform, app_id, product_id, current_period_end } }. One-time credit response: { kind:'wallet_credits', credits, transaction:{ app_id, platform, transaction_id, original_transaction_id, product_id, kind, credit_slug, credits, cents, environment } }.

Example

POST https://btelo.com/api/v1/iap/apple/transactions  Authorization: Bearer <user_jwt>  body: {"app_id":"btelo-english","transaction_id":"2000000123456789"}
POST/api/iap/apple/notifications

Apple App Store Server Notifications V2 receiver. Configure this exact URL as both Production and Sandbox Server URL for each app that uses btelo billing. This endpoint is called by Apple, not by client apps. A single shared URL is safe for multiple apps because btelo routes by bundleId + originalTransactionId/appAccountToken and validates product mappings before writing subscription state or wallet-credit transactions.

NameInTypeRequiredDescription
signedPayloadbody (JSON)stringrequiredApple V2 signed notification payload.

Response

content_typetext/plain
schemaHTTP 200 when accepted or intentionally ignored; HTTP 500 for retryable update failures.

Example

App Store Connect -> My Apps -> <app> -> App Information -> App Store Server Notifications -> Version 2 -> Production/Sandbox URL = https://btelo.com/api/iap/apple/notifications

Billing Admin API

POST/api/v1/billing/apps

Create or update a billing app config. Auth: HMAC from an Integration with is_admin=true.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringrequiredAdmin HMAC key ID.
idbody (JSON)stringrequiredApp id, e.g. btelo-english.
namebody (JSON)stringrequiredHuman app name.
ios_bundle_idbody (JSON)stringoptionalApple bundle id for IAP verification.
disabledbody (JSON)booleanoptionalDisable this billing app.

Example

POST https://btelo.com/api/v1/billing/apps  <admin HMAC headers>  body: {"id":"btelo-english","name":"Btelo English","ios_bundle_id":"com.btelo.english"}
POST/api/v1/billing/products

Create or update a store product mapping. Auth: HMAC from an Integration with is_admin=true.

NameInTypeRequiredDescription
Btelo-Key-IdheaderstringrequiredAdmin HMAC key ID.
app_idbody (JSON)stringrequiredRegistered app id.
platformbody (JSON)stringrequiredapple or stripe.
store_product_idbody (JSON)stringrequiredApple product id or Stripe price id.
kindbody (JSON)stringoptionalsubscription (default) or wallet_credits.
tierbody (JSON)stringoptionalBtelo tier slug. Required when kind=subscription.
planbody (JSON)stringoptionalmonth or year. Required when kind=subscription.
credit_slugbody (JSON)stringoptionalBtelo credit product slug, e.g. credits-5000. Used when kind=wallet_credits.
creditsbody (JSON)integeroptionalWallet credits to add for this one-time product. Required when kind=wallet_credits unless credit_slug maps to a known Btelo credit product.
centsbody (JSON)integeroptionalStore price in cents for audit/display.

Announcements

GET/api/v1/announcements

Return active announcements for one product app. Disabled announcements, expired announcements, and announcements for other apps are filtered out by btelo.com. Audience targeting is returned for product apps to evaluate locally.

NameInTypeRequiredDescription
AuthorizationheaderstringoptionalBearer <user_jwt>. Recommended when the app has the user's btelo JWT.
Btelo-Key-IdheaderstringoptionalHMAC key ID. Alternative server-to-server auth.
Btelo-TsheaderstringoptionalRequired with HMAC.
Btelo-SignheaderstringoptionalRequired with HMAC.
appquerystringrequiredProduct app slug, e.g. btelo-english.

Response

content_typeapplication/json
example_body
announcements
field_values
audience
audiences
date_semantics
expires_atInclusive admin date in YYYY-MM-DD. Example: expires_at=2026-02-15 may be shown on 2026-02-15; it is omitted starting 2026-02-16 UTC.
enabled
schema
announcement
appsstring[]. Lowercase product app slugs selected in btelo.com admin. The requested app is guaranteed to be present in this array.
audiencestring. Legacy single audience field mirroring audiences[0]. Current values: "all" or "paid". Prefer audiences for new integrations.
audiencesstring[]. Audience enum values. Current values: "all" = show to every authenticated user; "paid" = show only when the product app locally knows the user has paid access. Current responses contain exactly one value, but callers should treat this as an array.
bodiesobject<string,string>. Locale-keyed body map. "en" is required and is the fallback. Values are plain announcement body text authored by admin.
enabledboolean. Always true in this public endpoint because disabled announcements are filtered out by btelo.com before response.
expires_atstring, optional. YYYY-MM-DD maximum display date authored by admin. The date is inclusive; after that date passes in UTC, btelo.com omits the announcement.
idstring. Stable announcement id. Product apps must persist this id locally per user after display/read.
titlesobject<string,string>. Locale-keyed title map. "en" is required and is the fallback. Keys are lowercase locale tags stored by admin, e.g. "en", "zh", "zh-hans", "ja".
announcementsarray

Example

GET https://btelo.com/api/v1/announcements?app=btelo-english  <Btelo HMAC headers>

Feedback

GUIDEapp-integration-prompt

Prompt for product app agents implementing Btelo feedback. Use this as the canonical integration behavior instead of inventing app-specific feedback protocols.

POST/api/v1/feedback

Create a product-scoped feedback thread. Product apps may use their own user ids; the user_id does not need to exist in btelo.com. If Authorization: Bearer is present and user_id is omitted, btelo uses the JWT subject. Clients should provide system_info whenever possible so admins can diagnose issues without asking the user for hidden technical details.

NameInTypeRequiredDescription
Btelo-ProductheaderstringoptionalProduct name or slug. Used when product_name is omitted from the body.
product_namebodystringoptionalProduct name or slug. Required when Btelo-Product is not provided.
user_idbody/header/querystringoptionalProduct-side user id. Required unless a Bearer JWT is provided.
user_namebodystringoptionalOptional display name for admin context.
contentbodystringrequiredInitial feedback text.
system_infobodyobjectoptionalAdmin-only diagnostic context collected automatically by the client. It is accepted on create, stored on the ticket, and not returned by user-facing feedback APIs. Provide as much as is reasonably available: current user name/email/id, subscription tier/status/source/current_period_end, account created_at, app version/build/platform, device model/OS, browser user agent/language/timezone/screen size, network mode, active route, locale, feature flags, recent error codes, and any safe product state useful for support. Do not include secrets, auth tokens, payment card data, passwords, or sensitive document/content bodies.
metadatabodyobject|stringoptionalAlias for system_info, accepted for clients that already use a metadata field.
contextbodyobject|stringoptionalAlias for system_info.
device_infobodyobject|stringoptionalAlias for system_info.

Response

feedback
{
  id,
  product_name,
  user_id,
  user_name?,
  status,
  messages[
    
  ]
}.Officialresponsesarereturnedasmessageswithauthor="official"andauthor_label="Officialreply".

Example

POST https://btelo.com/api/v1/feedback  Btelo-Product: btelo-english  body: {"user_id":"app-user-123","user_name":"Alice","content":"The transcript screen is blank.","system_info":{"user":{"email":"[email protected]","created_at":"2026-05-01T12:00:00Z"},"subscription":{"tier":"pro","status":"active","source":"apple"},"app":{"version":"1.4.2","build":"88","route":"#/video"},"device":{"platform":"ios","model":"iPhone","os":"17.5"},"browser":{"user_agent":"...","language":"en-US","timezone":"America/Chicago","screen":"390x844"}}}
GET/api/v1/feedback

List feedback threads for one product/user pair, including official replies and user follow-ups. If Authorization: Bearer is present and user_id is omitted, btelo uses the JWT subject.

NameInTypeRequiredDescription
product_namequery/headerstringoptionalProduct name or slug. Required unless Btelo-Product is provided.
user_idquery/headerstringoptionalProduct-side user id. Required unless a Bearer JWT is provided.

Response

feedbackArray of feedback threads with messages[] in chronological order.

Example

GET https://btelo.com/api/v1/feedback?product_name=vibe-remote&user_id=app-user-123
POST/api/v1/feedback/{id}/messages

Append a user follow-up to an existing feedback thread. The supplied product/user identity must match the original thread.

NameInTypeRequiredDescription
idpathstringrequiredFeedback thread id.
product_namebody/header/querystringoptionalOptional product name check.
user_idbody/header/querystringoptionalProduct-side user id. Required unless a Bearer JWT is provided.
user_namebodystringoptionalOptional display name.
contentbodystringrequiredFollow-up text.

Response

feedbackUpdated feedback thread with messages[] in chronological order.

Example

POST https://btelo.com/api/v1/feedback/{id}/messages  body: {"product_name":"vibe-remote","user_id":"app-user-123","content":"More details..."}
Machine-readable source: /api/doc (JSON)