Hyperscale docs Sign in Start building

Product runtime

How Hyperscale fits

Hyperscale keeps the books and runs a company's Products. Providers hold the regulated money and make the lending, underwriting, credit, rating and claims decisions. Hyperscale never lends or holds funds.

Layer, customer side first What it does Kubernetes analogue
Tenant apps and their customers Call the operation API Workloads
CLI, portals, MCP and SDK Clients of that one API, with no authority of their own kubectl, dashboards, generated clients
HSX program and UDL contract Declare the Product's rules and record decisions Manifest and stored API object
Hyperscale runtime Admits operations; records accounts, holds, receipts and audit Admission and API server
Product Build The contract an agreement keeps Resource revision
ADL adapters Turn the contract into a provider's API and return evidence Drivers and plugins
Providers Hold money, perform the service, carry the risk Machines

Money moves once: each instruction has a durable identity and consumes provider evidence once. Every move conserves money within its book. A Build binds its providers; no scheduler picks one per move. Sandbox adapters report fixture outcomes, not real origination or custody.

Discover operations

Source Gives Names operations as
@hyperscale0/sdk createClient(...).call(name, input, options) and types Dispatch name, product.objects.create
/openapi.json Method, route, parameters, body, validated examples operationId is the wire name, product_objects_create; x-operation-name is the dispatch name
/.well-known/mcp.json Every MCP tool Wire name, product_objects_create
Runtime discovery The Product's own kinds, fields and actions Public action names from HSX

Operations["product.objects.create"] in the SDK gives input, output, error and errorCode; SharedErrorCode lists the codes every operation shares. The types do not validate network data, so treat an unknown error as an unknown outcome. The SDK retries reads, and retries a write only under an idempotency key, which it makes when you pass none.

The HTTP API page owns the base URL, environments, idempotency, the error envelope, pagination and rate limits.

Who may call what

OpenAPI marks each operation with the credential it takes.

Extension Value Means
x-principal api_key A Product API key can call it once its Product has the capability
x-principal user_session A Product key is refused; a person or an admitted token calls it
x-personal-access-token-scope a scope The scope a personal access token needs; null refuses such tokens
x-customer-session allowed A customer session token acts as that customer
x-environment sandbox, live Which environments admit it
confirmation header SHA-256 hex Required on live requests: the hash of { name, arguments }

An agent calls only what its grant and the agent policy admit, and each call also checks the delegating person's current permissions. Sandbox work inside the grant runs at once; live work waits for browser approval. Sign in covers grants, approval and agent setup; access covers Product scope from the agent's side.

MCP results have their own shape:

Result Fields
Success status, output, operationId, receiptId, traceId (the IDs may be null)
Failure status, error, and the MCP result sets isError

A mutation tool takes idempotencyKey; send one to keep control of retries. rehearse: true runs it with no effect and answers the rehearsal as output, with null operationId and receiptId. A tool that needs confirmation advertises the exact value in its schema. No MCP tool returns a credential except the webhook signing secret: key_create and api_key_sandbox_create answer with hyperscale key create --product <productId> --name <name>, and live keys come only from the founder with hyperscale key create-live.

List shapes

Lists page with cursor and nextCursor as the HTTP API page says. Read each operation's schema for its limit and default. Most lists return items; these do not:

Read Collection field
Team members members
Team roles roles
Sessions sessions
Object action discovery actions and evidence
Account holds items, plus hasMore and observedAt

Product objects and actions

A Build defines object kinds, their metadata, attachments and public actions. Creating an object stores metadata only: it opens no agreement and moves no money.

Operation Route
product.objects.discover GET /v1/products/{productId}/objects
product.objects.create POST /v1/products/{productId}/objects/{kind}
product.objects.list GET /v1/products/{productId}/objects/{kind}
product.objects.retrieve GET /v1/products/{productId}/objects/{kind}/{objectId}
product.objects.actions GET /v1/products/{productId}/objects/{kind}/{objectId}/actions
product.objects.agreements.list GET /v1/products/{productId}/objects/{kind}/{objectId}/agreements
product.objects.agreements.evidence.list GET /v1/products/{productId}/objects/{kind}/{objectId}/agreements/{instanceId}/evidence
product.objects.timeline.list GET /v1/products/{productId}/objects/{kind}/{objectId}/timeline
product.objects.preview POST /v1/products/{productId}/objects/{kind}/{objectId}/actions/{action}/preview
product.objects.execute POST /v1/products/{productId}/objects/{kind}/{objectId}/actions/{action}

The same steps from the CLI, with a Product key in HYPERSCALE_API_KEY:

hyperscale object kinds
hyperscale object create <kind> --fields-file fields.json
hyperscale object actions <kind> <object-id>
hyperscale object run <kind> <object-id> <action> --input-file input.json

The CLI takes and prints money in major units; REST and MCP take minor-unit strings. HSX money has the table.

Calling an action. kind, objectId and action are enough. Execute and preview fill in expectedRevision, productBuildId, digest and target from the record when you leave them out, and check them when you send them. The CLI's --attachment and --instance pick between agreements that offer the same action. api call reaches every other published route.

Acting for a customer. An object a Product key creates without onBehalfOf belongs to the Product, and money the Product pays itself is never an earning. When a customer pays, create the object and run the payer's steps with onBehalfOf naming that customer. A Product key, a member with desk.products.objects_write, or an agent inside its grant may send onBehalfOf on product.objects.create, actions, execute and preview. The call uses the customer's accounts, keeps the operator's view of records and records both identities.

Money and commits. An action checks input, authority and requirements, then commits its intent; ledger effects follow with durable recovery. A refused admission moves nothing; an uncertain outcome is not a refusal. The acting party pays from its cash account, which for a Product key is the Product pool, so fund the pool before the first money action. A completed action lists its transfers in outcome.moves, each with transferId, amount and accounts. Object schemas and action availability come from runtime discovery, never from the SDK. Admission counts each action's worst case, its own transfers plus every action it invokes, and refuses a Product create, compose or recompose with an action over 253 as execution_budget_exceeded, naming the action and its count. Std lending blocks end a payoff, default or refinance in one transfer per party and money purpose, and walk long lists in batches across clock steps, so a 360-month mortgage stays under the limit.

Refusal When Details
409 insufficient_balance The paying account is short. The action list and preview show the same blocker first. accountId, available, required, shortfall in minor units
409 target_needs_instance A non-create action names an attachment that already has an open agreement. Nothing opens. The open instances and the exact target to send
409 state_conflict, transfer_limit One action would post more than 253 transfers, the ledger's limit per request. Nothing posts. Only a Build admitted before the 253 check can reach it. limit: 253. Split the work.
400 invalid_request, list_position A list position past the end, sent in this request Fix the request
409 state_conflict, list_position The same, on a list saved earlier Correct the stored record first

Customers and sandbox money

A customer belongs to one company and enrols in one Product. customer.create (POST /v1/customers) takes { productId, entity: { kind, displayName, country } }. A new customer's entity and access start pending, and money stays refused until a legal profile clears. In sandbox, test: true fills a fake profile and activates access in the same call; live refuses test. A test customer runs no identity check, so reads show entityStatus not_checked and identity test.

A customer can also customer.signup and customer.login for a session token. Sign-up needs a browser challenge, so a script acts for a customer with its Product key and onBehalfOf.

Your own app signs its customers up and in. It calls customer.signup (POST /v1/customer-identity/signup) or customer.login (POST /v1/customer-identity/login) with the productId, and gets back sessionToken and sessionExpiresAt. Signup leaves the entity and the enrolment pending: no account opens and no money moves until the entity clears and access is active. The session belongs to that Product. Send it as Authorization: Bearer on the customer's own calls, honour its expiry, and end it with customer.logout (POST /v1/customer-identity/logout).

Every sandbox account starts at zero. sandbox.account.fund (POST /v1/sandbox/account-funding) takes { destinationAccountId, amount, currency }, with amount in minor units: "10000" is SAR 100.00. A Product key or a session with transfer:write calls it, never in live. The key names the company, so a key call that also sends tenantId gets invalid_request. The console's Add test money button and the CLI's fund call it in major units.

hyperscale customer create "Noura Alharbi" --test
hyperscale account open <customer-id>
hyperscale fund <account-id> 500

account.deposit (POST /v1/account-deposits) takes { destinationAccountId, adapter, amount, currency } and runs in sandbox and live. The adapter delivers the deposit and returns its credit reference as bankReference, and the account is credited from the company's bank mirror, so the finance report stays complete. The fixture_bank adapter moves fake money. In live it answers only on an estate that sets HYPERSCALE_LIVE_MONEY=fixture; otherwise the call gets capability_not_configured.

Both calls store the bank's credit as a provider fact on the transfer: provider fixture_bank, reference credit:<transferId>, with the amount and value date. A bank statement that lists the top-up then matches it, by that reference or by amount and value date, so a sandbox company rehearses reconciliation against its own funding.

Sandbox clock

Each sandbox Product has its own clock. sandbox.clock.advance (POST /v1/sandbox/clock/advance, CLI hyperscale clock advance 3d) moves it forward to at; release: true (hyperscale clock reset) returns it to wall time from any point. Records stamped while the clock ran ahead keep their stamps.

What an advance does. It runs every clock action due by at before it answers, and lists them in ran: instrument, action, objectId, instanceId, dueAt, amount for money, status, and reason when refused. Each piece's transfer carries its own dueAt as its value date, and one transaction packs one calendar month (UTC), so a two-month jump files each month's dues in that month, in the books, activity and statements. A piece that waited on an acceptance or another write to its own agreement posts at that write; a write to another agreement never moves it. A retried charge posts at the Product clock.

Limit Value
Clock actions per transaction 366
Transactions per advance 1,000, then the maintenance sweep takes over
Declared transfers per transaction 253, so a plan far behind catches up 253 at a time
Maintenance sweep Every 60 seconds

In live, and on a sandbox Product whose clock reads wall time, a piece runs within a minute of its dueAt with no request.

Refused clock work. A refusal is either retryable or lasting.

Refusal Retries How it clears
insufficient_balance Yes. ran carries retries: true, accountId, required, shortfall Fund the account. The sweep tries about once a minute and the next advance tries first. In sandbox, any action on the record tries it first; in live only the sweep does.
Ledger rejection after a competing spend, a concurrent write, clock_work_pending from other clocks Yes Same as above
Anything the action itself decides, such as a broken reference or terms that no longer hold No. work list shows it under needs_human; schedule list shows refused, retries: false Fix the cause, then run any action or write on the record. It retries the refused clock first.

Before a caller's action runs, the Product's due clock work runs. If that work cannot finish, the action returns 409 state_conflict with details.reason clock_work_pending and changes nothing: either 8 scheduled updates were refused in that request and each is now set aside with its reason, or the Product is further behind than one request catches up. Retry: each attempt runs more of the due work.

Piece states in schedule list.

State Means
waiting Due, but its record is in another state, such as a draft schedule. Accepting the schedule collects every piece due, once each.
refused A charge that failed. Only charges are ever refused.
completed Posted, with its amount, shown beside the next piece
cancelled The schedule or agreement was cancelled or stopped. It never runs.
waived The payee forgave it
ended The record finished another way, such as completed or expired

A start date written as today's date, such as startsAt: "2026-10-10", means now on the Product clock, so accepting a schedule that starts today collects its first piece in the same request. work list holds only what is due or refused and waits for a person; an action with no due date, such as ending a membership, stays an option on the record. A due date on any other action only makes it runnable, and a passed deadline refuses it. work list --staff-role credit_underwriter keeps only the decisions authored for that staff role, with the customer, amount and request time, and needs a role that grants credit:underwrite.

Which time a read shows. Sandbox reads date money on the Product clock: money summary, account balance, statements, the finance report, margin, treasury and block reads, activity, the request log and object timelines. asOf, occurredAt and a statement period are Product time; a host time is observedAt (recordedAt on a log row). In live the two are the same.

Refunds and returns

refund.create (POST /v1/refunds, CLI hyperscale transfer refund) gives a posted payment into a Product's pool back to the account that paid it, over the same two accounts. It takes { tenantId, paymentTransferId }, an optional amount in minor units for part of it, and an optional reason: duplicate, fraudulent or requested_by_customer. A founder session calls it; a Product key and a personal access token cannot. A full refund first gives back the earn-rate fees the Product took; a partial one keeps them.

hyperscale transfer refund --payment-transfer-id <transfer-id> --amount 25
Refusal When
409 transfer_lineage_conflict The payment already has a refund, or was reversed or returned. details.existingTransferId names it.
409 transfer_return_exists A return is in progress
400 transfer_adjustment_amount_invalid amount is above the payment

transfer_return.create opens a return and moves no money until it completes. waitsFor says what completes it: sandbox_clock_advance in sandbox, where the next advance or the minute sweep completes every open return, and provider_settlement in live. A transfer with a refund or reversal takes no return.

Team invitations and API keys

These are company operations for people signed in to the portals, so /openapi.json, which lists the Product API and the routes a CLI login or agent token calls, does not list them. Reach them with the CLI (hyperscale key list), MCP or a browser session.

  • team.member.list returns { teamId, members, nextCursor? }.
  • team.invitation.list (GET /v1/team/invitations) takes { tenantId, cursor?, limit? } and returns { teamId, items, nextCursor? }. Items have invitationId, email, role, invitedByUserId, expiresAt and createdAt; only pending, unexpired invitations appear, oldest first, then by ID descending. limit is 1 to 100, default 50. The cursor binds environment, tenant and team, and a foreign one returns invalid_request. It needs the same team read permission as the member list. Neither read returns invitation tokens.
  • api_key.list and api_key.retrieve items carry expiresAt and lastUsedAt, an ISO UTC time or null. A use is recorded at most once every five minutes, so a key used a minute ago can show an older time.