RIDM Apps deployment guide
RIDM Apps is hosting that builds and deploys a connected GitHub repository and publishes it at <name>.ridm.ai. Sign up for it under My Apps in the console, and check prices on the pricing page. Terms of use are set by Chapter 3 of the Terms of Service (Korean). For a quick look at what's included, see the app hosting overview.
Deployment flow
- Sign up — In My Apps, pick a plan and an address (subdomain). The address can't be changed later.
- Connect GitHub — On the app screen, install the GitHub App
ridm-apps-builderand choose a repository and branch. It only gets read access to the repository. - Push — Every push to the connected branch starts a build. Build history and failure logs show on the app screen.
- First deploy — When the build succeeds, it passes automatic checks and goes live right away (below). Later pushes are deployed automatically too.
Dockerfile requirements
- You need a
Dockerfileat the repository root. Without one the build fails. - The app must answer HTTP on port 3000.
PORT=3000andHOSTNAME=0.0.0.0are always set. Once the container starts, it must answer a request to/within 30 seconds (any status code). If it doesn't, the start counts as failed, and for a new version the previous version keeps serving. - If you set a health check path (the "Health check path" field on the app screen, e.g.
/api/health), that path is checked instead of/, and the start only succeeds if it answers with a 2xx, 3xx or 404 (5xx or any other 4xx fails). Use a path that also checks the database, and a version that opens the port but can't really run won't go live. Leave it empty and the rule above applies. - If a new version restarts 3 or more times within 5 minutes after going live (the process keeps dying), we roll back once, automatically, to the last version that ran fine and email you (with the last logs the app left). If the rolled-back version also keeps dying, we leave it as is, so fix your code and push again.
- The container runs as user 1000:1000. That user must be able to read your app files (the
nodeuser innode:*-alpineimages is 1000). - The file system is read-only. The only writable places are
/tmpand/app/.next/cache(both in memory, cleared on restart). Store lasting data in the database. - Builds are for x86_64 (amd64). The build time limit is 20 minutes.
- The build stops if secrets show up in the repository: AWS access keys, GitHub tokens, Stripe live keys, private key blocks, PostgreSQL URLs that contain a password. Put secrets in environment variables.
Next.js starter
Add output: 'standalone' to next.config.js and put the Dockerfile below at the repository root. If you don't have a public folder, delete that line.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=1000:1000 /app/public ./public
COPY --from=build --chown=1000:1000 /app/.next/standalone ./
COPY --from=build --chown=1000:1000 /app/.next/static ./.next/static
USER 1000
EXPOSE 3000
CMD ["node", "server.js"]
It doesn't have to be Next.js. Any stack works as long as the container answers on port 3000.
Runtime
- On the free plan, the app starts when someone visits and stops after 15 minutes without requests. The first visit after a stop can take a few seconds longer. Search crawlers, monitoring requests and HEAD requests don't wake the app.
- Paid plans run all the time. If the app crashes, it restarts automatically.
- If the app goes over its memory, it is killed. The Node.js heap limit is set to 75% of the plan's memory.
- WebSocket is available on paid plans (on the free plan, upgrade requests get
403). Your app simply acceptsUpgrade: websocketon port 3000 as usual (the path, subprotocol,Origin,X-Forwarded-ForandCF-Connecting-IPare passed through as is, and responses your app sends instead of upgrading, such as 403 or 429, also reach the visitor unchanged). Limits:- 120 concurrent connections per app. Beyond that, new connections are refused with
503(Retry-After: 10). - 64KB per message (all fragments combined). Beyond that, the connection is closed with
1009. The compression extension (permessage-deflate) isn't used. - Up to 2,000 messages per second per app in each direction, and 60 per second sent by a single visitor connection. Above that, messages are delayed, not dropped.
- After 100 seconds with no frames, the edge (Cloudflare) drops the connection. Have your app or client send pings more often than that.
- Same-app Origin only: a browser connection opens only when
Originis that app's address (appname.ridm.aior a domain connected to that app). Connections from other apps or other sites get403, even if the apps belong to the same partner. Different apps can't connect to each other directly over WebSocket. - Non-browser clients (servers, apps, CLIs) can connect without an
Origin, but then noCookieis passed to your app. Authenticate with a token in a header (Authorization) or the query string instead of cookies. - While a connection is open, the app doesn't go to sleep. When a new version is deployed or the platform restarts, connections are closed with
1001, so make your clients reconnect.
- 120 concurrent connections per app. Beyond that, new connections are refused with
- Outbound HTTPS requests are allowed. Mail ports (SMTP) and internal networks are blocked. To send emails, call the app mail API with
HRUM_API_KEY, or use a mail service with an HTTPS API. - For security, we record where each app connects out to: the address and port it connected to, the domain names it looked up, when each was first and last seen, and how many connections were made. We don't record request or response contents or full URLs. These records are kept for 3 months and then deleted. They are only viewed to investigate a security incident or a prohibited activity (Terms, Article 26 (Korean)), or when the law requires it.
- App logs (stdout and stderr) can be viewed by operators when handling an incident. Before they leave the host, environment variable values, database passwords, tokens, email addresses and the like are masked (Terms, Article 20).
Cookies
Many partner apps live under ridm.ai, so the gateway ties cookies to each app's own address. Your app code can use cookies as usual. But:
- Cookies are always
SecureandPath=/, and theDomainattribute is ignored. - Cookie names must be 57 characters or fewer. Cookies with longer names are dropped.
- Cookies created by browser JavaScript (
document.cookie) are not sent to the server. Create any cookie the server needs to read withSet-Cookiein a server response.
Database
Each app gets its own PostgreSQL database, and the connection details arrive as environment variables: DATABASE_URL (SSL required) plus PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD and PGSSLMODE. The storage limit depends on the plan, and usage is shown on the app screen.
Database connections use TLS with full certificate verification. The certificate authority (Amazon RDS) is preinstalled in the container (NODE_EXTRA_CA_CERTS, PGSSLROOTCERT), so you don't need any SSL options in your code — new Pool({ connectionString: process.env.DATABASE_URL, max: 2 }) is enough. Each app gets up to 3 connections, so keep your pool size at 2 or less.
- Up to 3 concurrent connections per app; a single query can run for 10 seconds; a connection idle for more than 60 seconds inside a transaction is closed.
- We email you when usage passes 80% or goes over the limit.
- If you stay over the limit for 24 hours (or immediately if you pass 150%), the database becomes read-only. To delete data, first run
SET default_transaction_read_only = offon the connection. - If you pass 110% while read-only, database connections are cut. No data is deleted, and you can still create backups.
- Get below 90% of the limit or move to a bigger plan, and the restriction lifts within 5 minutes.
Environment variables
- Add and delete them under "Environment variables" on the app screen. Saved values are stored encrypted and can't be viewed again (only names are shown). To change one, overwrite it with a new value.
- Saving restarts the app with the new values.
- Names may only use uppercase letters, digits and underscores (can't start with a digit, 64 characters max); values must be a single line of 2KB or less; up to 50 per app.
- Names the platform uses can't be set:
PORT,HOSTNAME,NODE_OPTIONS,DATABASE_URL, and names starting withRIDM_orPG. - Values the platform provides:
RIDM_COMMIT(full SHA of the deployed commit),RIDM_COMMIT_SHORT(first 7 characters),RIDM_DEPLOYED_AT(when that version was deployed, ISO UTC) — useful for showing the running version at/api/health, for example. If you roll back to an earlier version, that version's values are used. HRUM_API_KEY: put in a key issued under API keys in the console. For apps with AI Connect turned on, the platform sets it automatically and you can't set it yourself; if you set a value yourself, delete it before turning on AI Connect. Keys from other accounts and app-only keys can't be used.- Environment variables of a cancelled app can't be changed.
- Values can never be read back through any screen or API. Lists and API responses show names only. If you lose a value, get a new one from where it was issued (your payment provider, an external service's console, etc.) and save it again under the same name.
Add secrets in the console (people)
A person can add any name in the console. Names that AI tools can't set (below) are added this way too.
- Sign in at console.hrum.ai and open My Apps in the left menu.
- Scroll down the app card to the Environment variables section. Names already saved show
••••••••instead of a value. - Type the name in the first field, Name (e.g. STRIPE_SECRET_KEY). Lowercase letters and other characters turn into uppercase and underscores as you type.
- Paste the value into the second field, Value. The field is masked like a password field.
- Click Save and you'll see a confirmation that the variable was saved and the app will restart shortly. Saving again under the same name changes the value.
- To delete one, click Delete on that row, then Delete in the confirmation dialog. Deleting restarts the app, and the value can't be recovered.
While moving a value:
- Copy the value from your password manager or macOS Keychain Access and paste it right away. Don't write it down in a notes app, chat, email or document first.
- Clear the clipboard after pasting (macOS Terminal:
pbcopy < /dev/null, or just copy any other text once). - If the browser offers to save the value as a password, decline.
- Don't enter it while screen sharing or recording.
Add secrets with AI (AI operations token)
For an AI coding tool to set environment variables for you, it needs an AI operations token. That's a console management token (hrum_cm_…, see Manage the console with AI) for which a person checked the hosting_ops permission when it was issued. With this permission, the tool doesn't ask for consent again for each task. Setting environment variables isn't part of the HRUM MCP, so values never pass through the AI model; use the HTTP paths below.
Names you can set through the AI path are only those ending in _KEY, _SECRET, _TOKEN, _PASSWORD, _PASSWD, _PASS, _SALT or _CLIENT_ID (e.g. STRIPE_SECRET_KEY, MASTER_KEY). Even among those, names that change the runtime or a connection address are refused: names ending in _URL, _URI, _ENDPOINT, _HOST or _PROXY; names starting with NODE_, LD_, DYLD_, PYTHON, JAVA_, _JAVA_, JDK_, RUBY, PERL, GIT_, NPM_CONFIG_, GLOBAL_AGENT_ or AWS_ENDPOINT_URL; and names such as HTTP_PROXY, HTTPS_PROXY, NODE_TLS_REJECT_UNAUTHORIZED, SSL_CERT_FILE, PATH and OPENAI_API_BASE. Other names (e.g. ADMIN_PATH, SMTP_HOST) are added by a person in the console. Deleting works through the AI path too (except reserved names).
Getting a token — valid for 30 days.
- Without authentication, the AI sends
{"action":"token_issue","payload":{"scopes":["hosting_ops"],"label":"My app ops"}}toPOST https://api.hrum.ai/api/v1/partners/v1/console-ai/consents. The responsedatacontainsconsentUrl,userCode(8 characters),codeandpollSecret. - The AI shows the user only the
consentUrland the 8-character code. It doesn't showpollSecret. - Within 10 minutes, the person opens the link in a browser signed in to the console, enters the 8-character code from the AI tool's screen in the Confirmation code field, checks
hosting_opsand clicks Agree. Read the warning under the checkbox first (if the token leaks, someone could overwrite environment variables, deploy and restart). - When the AI calls
GET …/console-ai/consents/{code}with theX-Consent-Poll-Secretheader, the first response after approval carries the token indata.token.value, exactly once. Don't print it; put it straight into a secret store (Keychain, a file with permissions 600, etc.). - When you no longer need it, click Revoke under "AI console management tokens" on the console's API keys screen. It's blocked immediately. Each account can have up to 10 active tokens.
Calls — base URL https://api.hrum.ai, header Authorization: Bearer hrum_cm_…. The app id is data.apps[].id (a UUID) in the response of GET /api/v1/partners/v1/console-ai/hosting.
| Task | Request | Response |
|---|---|---|
| List names | GET /api/v1/partners/v1/console-ai/hosting/apps/{appId}/env | {"success":true,"data":{"env":{"names":[…]}}} — no values |
| Add or change | PUT …/hosting/apps/{appId}/env/{NAME} with body {"value":"…"} | The updated list of names. The app restarts |
| Delete | DELETE …/hosting/apps/{appId}/env/{NAME} | The updated list of names. The app restarts |
Example (bash or zsh). The value and the token are never passed as command-line arguments; the value goes through standard input. Replace $APP_ID and the Keychain item names with your own.
# 1) Read secrets into shell variables (nothing is printed)
HRUM_CM_TOKEN=$(security find-generic-password -s hrum-console-ops -w)
VALUE=$(security find-generic-password -s myapp-STRIPE_SECRET_KEY -w)
# 2) Value goes through stdin, token through a header file: neither shows up in ps or shell history
printf '%s' "$VALUE" | jq -Rs '{value: .}' | curl -s -X PUT \
-H @<(printf 'Authorization: Bearer %s' "$HRUM_CM_TOKEN") \
-H 'Content-Type: application/json' --data-binary @- \
"https://api.hrum.ai/api/v1/partners/v1/console-ai/hosting/apps/$APP_ID/env/STRIPE_SECRET_KEY" \
| jq '.data.env.names // .'
# 3) Forget the value
unset VALUE
Once it's saved, the response shows only the list of names. Open the app's address to check that it restarted with the new value.
Deploy and read logs with AI (AI operations token)
With the same AI operations token (hosting_ops), you can start a deploy and check deploy status and the logs your app writes — without opening the site.
From HRUM MCP 0.3.3, MCP tools do the same: hrum_hosting_deploy (start a deploy), hrum_hosting_deploys (deploy status) and hrum_hosting_logs (runtime logs). Get a token with the hosting_ops permission through hrum_ai_connect first, and find the app id with hrum_hosting_status. Tools that don't use MCP use the HTTP paths below.
| Task | Request | Response |
|---|---|---|
| Start a deploy | POST …/hosting/apps/{appId}/deploy with body {} or {"commit":"40-char sha"} | {"deployId","commitSha","retried","status":"queued"}. Just like "Deploy now" in the console, it builds the latest commit on the connected branch (same first-deploy review and plan rules). commit is accepted only when it is that latest commit — older commits aren't supported yet (400 COMMIT_NOT_BRANCH_HEAD). Up to 2 per minute |
| Deploy status | GET …/hosting/apps/{appId}/deploys?limit=20 | Newest first: id, commitSha, status, queuedAt, startedAt, finishedAt, liveAt, failReason, scanFindings (only the file, line and rule flagged by the secret scan), logTail (end of the build log), startFailReason, startFailLog |
| Request logs | POST …/hosting/apps/{appId}/logs with body {"lines":100} (1–200) | 202 {"requestId"} |
| Fetch logs | GET …/hosting/apps/{appId}/logs/{requestId} | If status is pending, try again in 2–3 seconds; if delivered, read lines. After 3 minutes it becomes expired |
Logs and failure reasons are masked before they're returned: environment variable values stored for that app, and key-shaped strings such as sk-…, AKIA…, Bearer …, JWTs and …_live_…, become [redacted]. Still, the first step is making sure your app doesn't log secrets. Every call (including refused ones) is recorded with the token, the app and the result.
The same paths exist for a console sign-in session or an HRUM MCP sign-in session: POST …/console/hosting/apps/{appId}/deploy-now (for MCP, only sessions created by signing up), GET …/console/hosting/apps/{appId}/deploys, POST …/console/hosting/apps/{appId}/logs with body {"lines":100} (1–200; for MCP, only sessions created by signing up) → GET …/logs/{requestId}. They only work for your own apps, and log lines are masked by the same rules. Fetched log contents are deleted 24 hours after they're delivered (or requested) (7 days from 2026-12-01; Terms, Article 20, Paragraph 5).
HRUM_CM_TOKEN=$(security find-generic-password -s hrum-console-ops -w)
API=https://api.hrum.ai/api/v1/partners/v1/console-ai/hosting/apps/$APP_ID
git push origin main
curl -s -X POST -H @<(printf 'Authorization: Bearer %s' "$HRUM_CM_TOKEN") \
-H 'Content-Type: application/json' -d '{}' "$API/deploy" | jq '.data // .'
curl -s -H @<(printf 'Authorization: Bearer %s' "$HRUM_CM_TOKEN") \
"$API/deploys?limit=3" | jq '.data.deploys[] | {id, commitSha, status, liveAt, failReason}'
Secret safety rules (for AI tools)
- Never ask the user to paste a value into the chat. Ask them to put it in Keychain or a password manager, and ask only for the item name.
- If the user pastes a value into the chat anyway, don't print it back. It's already in the conversation history, so recommend that they get a new one from the issuer and replace it.
- Read values into shell variables and pass them through standard input. Don't use anything that leaves the value on the command line or in logs, such as
curl -d "value",echo valueorset -x. - Don't write values into code, a committed
.env, PROGRESS.md, logs or screen output. Report only "name + whether it was saved". - Don't work around names the AI path refuses (above); hand them to a person to add in the console.
- Treat the operations token (
hrum_cm_…) just like a value. When you're done, remind the user to revoke it in the console.
A real case: an AI tool building a sales management app tried to put the app's master key into an environment variable, but back then there was no operations path for AI, so it stopped there. Today, names ending in _KEY like MASTER_KEY can be set with an AI operations token, and other names are added by a person in the console. Either way, values never travel through chat.
Environment variable errors
Error bodies look like {"success":false,"statusCode":…,"message":"…"}. Environment variable errors have no code, so tell them apart by the status code and the start of the message below. The API returns these messages in Korean, so the start of each message is shown as returned, followed by its meaning. The full messages are in llms.txt.
| Status | Start of message | Cause → fix |
|---|---|---|
| 400 | 이름은 대문자·숫자·밑줄만 (names: uppercase, digits, underscores only) | Name breaks the rules → uppercase, digits and underscores only, not starting with a digit, 64 characters max |
| 400 | 플랫폼이 쓰는 이름이라 (name used by the platform) | PORT, HOSTNAME, NODE_OPTIONS, DATABASE_URL, or a RIDM_ / PG prefix → use another name |
| 400 | AI 연결이 자동으로 넣는 이름이라 (set automatically by AI Connect) | HRUM_API_KEY → this app has AI Connect turned on, so the platform sets it. To set it yourself, turn AI Connect off |
| 400 | 이 계정에서 발급한 API 키만 넣을 수 있어요 (only keys issued by this account) | Use a key from API keys in your own console |
| 400 | AI 연결이 앱마다 만든 키는 넣을 수 없어요 (per-app AI Connect keys not allowed) | Use a key issued under API keys in the console |
| 400 | 값을 입력해 주세요 (enter a value) | The value is empty |
| 400 | 값에 줄바꿈을 넣을 수 없 (no line breaks in values) | Multi-line value (a PEM private key, etc.) → convert it to a single base64 line and decode it in your app |
| 400 | 값은 2048바이트까지 (values up to 2048 bytes) | The value is over 2KB → keep large values in the database or an external secret store |
| 400 | 실행 환경을 바꾸는 이름이라 AI 경로 (changes the runtime; not via the AI path) | A name the AI path refuses → a person adds it in the console |
| 400 | AI 경로(AI 운영 토큰·MCP)로는 _KEY (AI path only accepts _KEY…) | Not a secret-name suffix → rename it, or have a person add it in the console |
| 400 | Validation failed (uuid is expected) | The app id isn't a UUID → use apps[].id from GET /console-ai/hosting |
| 401 | MGMT_TOKEN_INVALID | Token missing, expired or revoked → issue a new one |
| 403 | MGMT_SCOPE_DENIED (scope: "hosting_ops") | The token doesn't have hosting_ops → issue a new one with it checked |
| 404 | 앱을 찾을 수 없 (app not found) | Another account's app or a deleted app → check the app id |
| 404 | 없는 환경변수 (no such environment variable) | The name you're deleting doesn't exist → check the list of names |
| 409 | 해지된 앱은 환경변수를 바꿀 수 없 (cancelled app) | The app is being cancelled → it can't be changed |
| 409 | 환경변수는 앱당 50개까지 (up to 50 per app) | Delete names you don't use and try again |
| 429 | — | Adding and deleting are limited to 30 per minute → try again shortly |
| 503 | 환경변수 저장소가 준비되지 않 (variable store not ready) | A server-side problem → try again shortly; contact us if it continues |
AI Connect
AI Connect lets your app call the Saju and AI APIs: the platform adds an API key that only this app uses. It's available on every account.
- Turn it on — Open the app's details under Apps in the console and turn on "Use AI Connect". An app-only key is created and added as the environment variable
HRUM_API_KEY, and the app restarts. - Call — In your app's server code, read
process.env.HRUM_API_KEYand put it in theX-Partner-Api-Keyheader. The key value isn't shown even in the console. Don't put it in code that's sent to the browser.
While it's on, you can't set HRUM_API_KEY yourself. If you set a value yourself, delete it before turning AI Connect on (otherwise you'll get an error asking you to delete the existing HRUM_API_KEY environment variable first). Charges from this key come out of your credits, and once it reaches the daily usage limit set in the app's details, it stops for that day. Turning it off cuts the key immediately; turning it back on creates a new key. This key can't call the payment APIs — accepting payments in your app uses a key issued in the console.
Accept payments in your app
You can take payments from your app's users through the HRUM Payments checkout page. Selling opens once you register as a seller (individual, sole proprietor or corporation) and are approved. Register as a seller under Payouts in the console. The conditions are set by Articles 32–39 of the Terms of Service (Korean). Apps on the free plan can sell too. However, while an app is not being served because it hit its monthly transfer limit or was suspended, opening new checkouts is refused with 409 APP_NOT_SERVING. Checkouts already opened, order lookups and refunds still work.
- Add an API key — Put a key issued under API keys in the console into an environment variable (e.g.
HRUM_PARTNER_KEY). TheHRUM_API_KEYthat AI Connect adds can't call the payment APIs. Use the key only in server code and never send it to the browser. - Register a product —
POST /api/v1/partners/v1/storefront/skuswith body{"appId","name","priceWon"}. Payments are only taken at the price registered here. - Open a checkout — From your server, send
POST /api/v1/partners/v1/storefront/checkoutswith body{"appId","skuId","qty","returnUrl"}, then send the user tocheckoutUrlfrom the response.returnUrlmust be an https address on this app (<name>.ridm.aior a connected domain). - Check the result — After paying, the user comes back to
returnUrl?orderId=…. On your server, callGET /api/v1/partners/v1/storefront/orders/{orderId}and confirmstatusispaidbefore delivering the product. Don't trust the value in the URL alone.
Every request needs the X-Partner-Api-Key header. For refunds, email the order number to [email protected]. Once refunded, the order status changes to canceled or partial_canceled.
const res = await fetch("https://api.hrum.ai/api/v1/partners/v1/storefront/checkouts", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Partner-Api-Key": process.env.HRUM_PARTNER_KEY },
body: JSON.stringify({ appId, skuId, qty: 1, returnUrl: "https://myapp.ridm.ai/paid" }),
});
const { checkoutUrl } = (await res.json()).data;
What each key permission can call
You can choose permissions when you create a key. Calling a path outside the chosen permissions returns 403 KEY_SCOPE_MISSING. Existing keys with no permissions chosen can still call every path. All paths are in the OpenAPI document.
payments— register productsPOST /storefront/skus, open checkoutsPOST /storefront/checkoutsorders.read— look up ordersGET /storefront/orders/{orderId}refunds— refund results arrive through the orderstatusand theorder.refundedwebhooksettlement.read— settlement dataGET /settlement/sales·/settlement/payouts·/settlement/summarywebhooks— register a seller webhook address and view delivery records
Settlement data API
Get the same numbers as the Console → Payouts screen from your server. Your key only sees your own data. The period from / to uses Korean dates (YYYY-MM-DD, both ends included); leave it empty for the current month. Up to 366 days at a time.
GET /api/v1/partners/v1/settlement/sales?from=&to=&skuId=&limit=&cursor=— for each sale: amount paid (paidWon), HRUM sales commission (salesCommissionWon, incl. VAT), per-order fee (perOrderFeeWon), card processing fee (pgFeeWon), amount to be paid out (sellerWon) and refunds (refundedWon). All values are net of refunds.totalsis the total for the whole period, not just this page.GET /api/v1/partners/v1/settlement/payouts?from=&to=&status=scheduled|completed|unpaid— for each payout,grossWon= money sent (amountWon) + payout fee (feeWon) + its VAT (feeVatWon) + 3.3% withholding tax for individual sellers (withholdingWon). The period is by payout date.GET /api/v1/partners/v1/settlement/summary?from=&to=— total sales, scheduled and completed payout totals, and the current payout fee in one call.
const res = await fetch("https://api.hrum.ai/api/v1/partners/v1/settlement/summary?from=2026-11-01&to=2026-11-30", {
headers: { "X-Partner-Api-Key": process.env.HRUM_PARTNER_KEY },
});
const { sales, payouts, payoutFee } = (await res.json()).data;
Seller webhooks
We notify your server when a payment completes, a refund happens or a payout is made. Only https:// addresses are accepted (ports 443 and 8443), and private or internal addresses can't be registered.
- Register an address —
PUT /api/v1/partners/v1/webhooks/endpointwith body{"url":"https://myapp.ridm.ai/hrum/webhook"}. Thesecret(whsec_…) in the response is shown only this once. Put it in the server environment variableHRUM_WEBHOOK_SECRETright away. To get a new one, send{"url": …, "rotateSecret": true}. - Events —
order.paid(payment completed),order.refunded(one refund),payout.scheduled(payout scheduled),payout.completed(payout completed),webhook.test(POST /webhooks/test). The body is{"id","type","created","data"}and contains no buyer information. Each occurrence is created only once with the sameid, so useidto filter out duplicates. - Verify the signature — Header
Hrum-Signature: t=<unix seconds>,v1=<hex>.v1is HMAC-SHA256 with your secret over (t + "." + the raw body as received). Reject anytolder than 5 minutes. - Retries — If you don't answer with a 2xx within 10 seconds, we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours (up to 6 attempts). View delivery records with
GET /webhooks/deliveriesand redeliver withPOST /webhooks/deliveries/{eventId}/redeliver.
const crypto = require("node:crypto");
function verifyHrumWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(String(header || "").split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const got = Buffer.from(parts.v1 || "", "hex");
return got.length === want.length && crypto.timingSafeEqual(got, want);
}
// Express: verify the raw body before parsing it as JSON
app.post("/hrum/webhook", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyHrumWebhook(raw, req.get("Hrum-Signature"), process.env.HRUM_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(raw); // if event.id was already handled, just return 200
res.sendStatus(200);
});
First deploy and automatic checks
When the first build succeeds, it passes automatic checks, is approved automatically and the address goes live right away. Paid plan charges aren't deducted until the first deploy is live. If the repository contains secrets (keys, tokens, passwords), the build fails. Deploys still go out automatically when a new outbound destination or a new risky pattern is found, or when the repository owner changes; those changes are recorded. Every automatic approval and check result is recorded and reviewed by operators afterwards, and publication can be stopped if there's a problem. If publication is stopped, we tell you why by email and on the app screen. For prohibited activities, see Article 26 of the Terms (Korean).
Pricing and limits
Price, memory, database and monthly transfer for each plan are on the pricing page. Paid plan fees are deducted from your topped-up credits every month. If your balance is short on the billing date, the app stops, and it resumes automatically once you top up. Monthly transfer is the sum of responses sent to visitors and traffic your app sends out (including external API calls, uploads and database writes). For WebSocket, frames in both directions count. When you hit the limit, open WebSockets are also closed with 1008. At the limit, both publication and the app stop (resuming on the 1st of the next month); on paid plans, buying a transfer block on the app screen brings it back within a few minutes. We email you at 80% and when you hit the limit.
Backup and cancellation
- Create a database dump with "Database backup" on the app screen. You get a download link valid for 1 hour, and you can create up to 3 a day.
- When you cancel, a paid plan stops at the end of the period you've already paid for, and for 30 days after that you can download your data or restore the app. After 30 days the app and database are deleted and can't be recovered. Charges already deducted are not refunded.
Contact: contact@float.do