Build Your First API
Build GET /api/v1/tasks: an endpoint that takes a status, searches the tasks storage and replies in JSON. On the way you validate the request, secure it with a scope and test it with curl.
This page picks up where the Quickstart left off. The quickstart shows the smallest possible endpoint; this page goes deeper into URLs, storage queries, validation and security. If you followed Build Your First CRUD App, you already have the tasks storage this endpoint reads from.
Create the API blueprint
An API endpoint lives in its own blueprint. Open the blueprints overview in RUAL Studio, create a new blueprint, and choose the type Build an API endpoint. This seeds the canvas with an on_startup_register_uri_get block: the trigger that starts your flow on every incoming request.
Name the blueprint after what it serves, for example Tasks API. One blueprint per endpoint (or per small group of related endpoints) keeps flows readable, similar to assigning a single class to a file in traditional coding.
Define the endpoint on the trigger block
The trigger block carries the endpoint definition. Click its no url given button and type tasks. The block exposes URLs under /api/ with a version, v1 by default, so your endpoint becomes GET /api/v1/tasks. Its version pin lets you introduce v2 later without breaking existing consumers.
- Other methods have their own trigger blocks, for example
on_startup_register_uri_postfor POST. A single blueprint can hold several of them when the endpoints belong together. - URLs can be multiple segments deep and may include named parameters such as
tasks/:guid. Read those withhttpconnection_get_params. - Keep the URL lowercase and plural:
tasks, notgetTasks. It is the public contract of your endpoint.
Hand each request to a function
The trigger block does not contain logic itself. It hands each request to a function. Drag from the block's On Request out-pin into an empty area of the canvas and create a trigger_custom_function, naming it list_tasks. From then on, the function's flow out-pin runs on every request to GET /api/v1/tasks.
Functions are private to their blueprint by default and can be reused from other flows with function_custom_execute: handy when a web page and an API endpoint need the same query logic.
Search the storage and reply in JSON
Inside the function, search the tasks storage and send the results back with httpconnection_set_json. Wire the blocks like this:
| Block | Purpose | Connects to |
|---|---|---|
storage (tasks) |
Selects the storage to search | function_search (storage pin) |
query_bool_term_fields (status = open) |
Match only open tasks | query_bool_filter |
query_bool_filter |
Wrap the term query in an efficient filter context | query_and |
query_sort_field (_meta.created) |
Newest tasks first | query_and |
query_and |
Combine filter and sort into one query | function_search (query pin) |
number_default (50) |
Limit the result set | function_search (limit pin) |
function_search |
Runs the search; outputs an array of documents |
httpconnection_set_json (data pin) |
httpconnection_current_request |
Reference to the incoming request | httpconnection_set_json (connection pin) |
number_default (200) |
The HTTP status code | httpconnection_set_json (code pin) |
Connect the function's flow out-pin to the flow in-pin of function_search, and from there to httpconnection_set_json. The flow pins control execution order, the data pins carry the results. Query composition is covered in depth under Building Search Queries.
Validate the request before touching storage
Real endpoints check their input first. Say the caller has to name a status: GET /api/v1/tasks?status=open. Read it with httpconnection_get_query: connect httpconnection_current_request to its connection in-pin, and it outputs a query object containing status.
Check the value
Add a
condition_not_empty_valueblock and connect thestatusvalue to it.Branch on it
Add a
branchblock and connect the function's flow and the condition. It splits execution intotrueandfalsepaths.Refuse on false
Reply with a second
httpconnection_set_json: anumber_defaultof400on the code pin and anobject_new_fieldswith anerrorfield such asmissing required parameter: statuson the data pin.Search on true
Run the search from the previous section, now using the value in the term query instead of a fixed one.
Blocks like function_search also expose success and error out-pins. Use them to return a 500 with a useful message instead of failing silently. When a flow misbehaves, Debugging shows how to trace execution inside the blueprint.
The whole endpoint in the code view, including the scope from the next section. You can paste it instead of wiring the blocks:
blueprint "Tasks API"
route GET "tasks" -> list-tasks with {scopes: ["*loggedin"]}
fn list-tasks(connection, params, query, body) {
guard condition_not_empty_value(query.status) else {
reply 400 {error: "missing required parameter: status"}
}
tasks = search@tasks(
query: query_and(query_bool_filter(term{status: query.status}), query_sort_field[_meta.created]()),
limit: 50,
)
guard tasks.success else {
reply 500 {error: tasks.error}
}
reply 200 array: tasks.array
}Name a scope, or the route stays locked
A route without any scope answers 403 with ENDPOINT_NOT_PUBLIC to everyone. Click the lock icon on the trigger block to open the Scopes Management Modal and choose:
| Scope | Who may call |
|---|---|
*public | Anyone on the internet. |
*loggedin | Users with a valid access_token. Tokens are issued by the login APIs, are valid for 14 days, and are extended automatically while actively used. |
| Custom scopes | Create your own (for example tasks-read) and assign it to only the users or groups that may call this endpoint. |
Callers can pass a token five ways: an Authorization: Bearer header, an x-authtoken or x-token header, an access_token cookie, or ?access_token= in the query string.
The block also holds two per-endpoint protection switches: a rate limit (key type, max requests, timeframe in seconds: extra requests are blocked) and a throttle (key type plus a timeout: requests with the same key run one at a time). Both are explained in detail under Remote Access Control.
Activate it and test with curl
APIs are inactive by default. Save the blueprint (Cmd/Ctrl + S), then click the switch that reads Inactive in the top bar. Saving is what makes a change live, so the endpoint now answers everyone its scope allows:
curl -H "Authorization: Bearer YOUR_TOKEN" "https://<your-cluster>/api/v1/tasks?status=open"[
{
"_meta": {
"cms": 1752768000362,
"created": 1752768000,
"entity": 1,
"expiry": -1,
"guid": "b34686f50e1b04964250f13391274d85308c129a793c6ec97bc41870fc67081e",
"removed": 0,
"ums": 1752768000803,
"update_hash": "60e27209fa93063b5605e41605c2722ed428cae0",
"updated": 1752768000
},
"owner_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
"owner_name": "joe doe",
"status": "open",
"title": "write the release notes"
}
]A 401 means the token is missing or invalid; a 403 means the scope check failed. Verify the lock icon shows the intended scope and that your token is passed correctly. To try a change before saving it, simulate the function: a simulation runs your unsaved edits and writes nothing.
Next steps
You registered a versioned GET endpoint, wired a storage query into a JSON reply, validated input with a branch, and secured the endpoint.
API Guide Authentication, request conventions, error shapes and rate limits for every RUAL API. Cluster APIs Your cluster ships more than 240 built-in REST operations for managing users, storage and blueprints. Remote Access Control Scopes, the Scopes Management Modal, rate limiting and throttling in full detail. Quickstart The 15-minute version: a Hello World page plus a minimal JSON endpoint.Frequently asked
How do I build a REST API endpoint in RUAL?
Register the route with an on_startup_register_uri_get block in its own blueprint, query storage in the flow, and reply with a JSON body. Then save and press Activate. Saving publishes it, and there is no separate deploy step on a current core.
How do I secure a custom RUAL API endpoint?
Assign a scope to the endpoint in the Scopes Management Modal. Callers then need a token carrying that scope. You can add a rate limit or a throttle per endpoint in the same modal.
Do custom RUAL endpoints have a rate limit by default?
Yes. An endpoint without a limit of its own falls under the cluster baseline: by default 1200 requests a minute per client address when anonymous, and 600 a minute per session when signed in. Add a rate limit or throttle per endpoint when you need something tighter.


