REST API
Adama's real-time nature lives on WebSockets, but sometimes you just need plain HTTP. Server-to-server integration, webhooks from Stripe, one-off operations — these don't need a persistent connection. That's what the REST API is for.
Overview
Adama offers two distinct HTTP interfaces:
- Platform API: Administrative operations (document creation, space management)
- Document Web Endpoints: Custom HTTP handlers you define with
@webin your Adama code
Both share common authentication patterns but serve different purposes.
Authentication
Identity Token Header
Most Platform API calls require an Authorization header with your identity token:
Authorization: Bearer your-identity-token-here
Or using the identity query parameter:
GET /api/endpoint?identity=your-identity-token-here
Obtaining an Identity
Identity tokens come from the WebSocket API (see WebSocket API). Once you have one, store it securely and include it in your HTTP requests.
API Keys (Spaces)
For server-to-server integration, you can generate API keys for a space:
# Using the Adama CLI
adama space generate-key --space myspace
This returns a key ID and public key for signing requests.
Platform API Endpoints
Base URL
https://{region}.adama-platform.com
Document Operations
Create a Document
POST /{space}/{key}
Content-Type: application/json
Authorization: Bearer {identity}
{
"arg": {
"title": "My Document",
"owner": "user@example.com"
}
}
Response:
{
"result": "created"
}
Direct Message Send
Send a message to a document channel without maintaining a connection:
POST /{space}/{key}/~channel/{channel-name}
Content-Type: application/json
Authorization: Bearer {identity}
{
"text": "Hello from REST!"
}
Response:
{
"seq": 42
}
Direct Message Send (Idempotent)
Include a X-Dedupe-Key header for at-most-once delivery:
POST /{space}/{key}/~channel/{channel-name}
Content-Type: application/json
Authorization: Bearer {identity}
X-Dedupe-Key: unique-operation-id-12345
{
"action": "process"
}
Space Operations
List Spaces
GET /~api/space/list
Authorization: Bearer {identity}
Response:
{
"spaces": [
{"space": "myspace", "role": "owner", "created": "2024-01-15"},
{"space": "other", "role": "developer", "created": "2024-02-20"}
]
}
Get Space Schema (Reflect)
GET /~api/space/reflect?space={space}&key={key}
Authorization: Bearer {identity}
Response:
{
"reflection": {
"types": { ... },
"channels": { ... },
"fields": { ... }
}
}
Document Listing
GET /~api/document/list?space={space}&limit=100
Authorization: Bearer {identity}
Response (streaming):
{"key": "doc-1", "created": "2024-01-15", "updated": "2024-01-20", "seq": 42}
{"key": "doc-2", "created": "2024-01-16", "updated": "2024-01-18", "seq": 15}
Document Web Endpoints
Every Adama document can expose custom HTTP endpoints via @web handlers. This turns your documents into web servers — great for webhooks, REST APIs, and serving content.
URL Structure
Document web endpoints follow this pattern:
https://{region}.adama-platform.com/{space}/{key}/{path}
Where:
{region}is your deployment region{space}is the Adama space name{key}is the document key{path}is handled by your@webhandlers
Domain Mapping
You can map custom domains to documents, which simplifies the URLs:
https://api.yourdomain.com/{path}
Maps to a specific space/key combination configured via domain settings.
GET Endpoints
Define GET handlers to serve data:
@web get /api/status {
return {
json: { alive: true, version: "1.0" },
cors: true
};
}
HTTP Request:
GET /myspace/doc-1/api/status
Response:
{"alive": true, "version": "1.0"}
PUT/POST Endpoints
Handle incoming data with PUT handlers (POST is normalized to PUT):
message WebhookPayload {
string event;
string data;
}
@web put /webhook (WebhookPayload payload) {
// Process the webhook
return {
json: { received: true }
};
}
HTTP Request:
POST /myspace/doc-1/webhook
Content-Type: application/json
{"event": "payment.success", "data": "..."}
Response:
{"received": true}
DELETE Endpoints
@web delete /session {
// Clear session
return {
json: { cleared: true }
};
}
HTTP Request:
DELETE /myspace/doc-1/session
OPTIONS (CORS Preflight)
Enable browser-based access with OPTIONS handlers:
@web options /api/data {
return { cors: true };
}
@web get /api/data {
return {
json: { items: [1, 2, 3] },
cors: true
};
}
Path Parameters
Capture dynamic URL segments:
@web get /user/$id:int {
return {
json: { user_id: id }
};
}
Request: GET /myspace/doc-1/user/42 Response: {"user_id": 42}
Multiple parameters:
@web get /org/$orgId:int/user/$userId:int {
return {
json: { org: orgId, user: userId }
};
}
Query Parameters
Access query strings via the @parameters constant:
@web get /search {
let query = @parameters.str("q", "");
let page = @parameters.str("page", "1");
return {
json: { query: query, page: page }
};
}
Request: GET /myspace/doc-1/search?q=hello&page=2 Response: {"query": "hello", "page": "2"}
Request Headers
Access HTTP headers via the @headers constant:
@web get /info {
let userAgent = @headers["User-Agent"];
let auth = @headers["Authorization"];
return {
json: { user_agent: userAgent }
};
}
Response Types
JSON
@web get /data {
return {
json: { key: "value", count: 42 }
};
}
Content-Type: application/json
HTML
@web get /page {
return {
html: "<html><body><h1>Hello!</h1></body></html>"
};
}
Content-Type: text/html
XML
Good for webhooks that expect XML responses (Twilio, for instance):
message TwilioSMS {
string From;
string Body;
}
@web put /sms (TwilioSMS sms) {
// Process incoming SMS
return {
xml: "<?xml version=\"1.0\"?><Response><Message>Thanks!</Message></Response>"
};
}
Content-Type: application/xml
CSS and JavaScript
@web get /styles/main {
return {
css: "body { font-family: sans-serif; }"
};
}
@web get /scripts/app {
return {
js: "console.log('Hello from Adama!');"
};
}
Asset Downloads
Serve uploaded files:
private asset user_avatar;
@web get /avatar {
return {
asset: user_avatar
};
}
Redirects
// Temporary redirect (302)
@web get /temp {
return {
forward: "https://example.com/new-location"
};
}
// Permanent redirect (301)
@web get /moved {
return {
redirect: "https://example.com/permanent"
};
}
Signed Identity Response
Issue a signed JWT for the requesting user:
@web get /login/$token:string {
// Validate the token...
return {
sign: "agent" // Signs the given agent name
};
}
Response Modifiers
CORS Headers
Enable cross-origin access:
@web get /api {
return {
json: { data: "value" },
cors: true
};
}
This adds:
Access-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, Authorization
Caching
Enable HTTP caching:
@web get /static_data {
return {
json: { version: "1.0" },
cache_ttl_seconds: 3600 // Cache for 1 hour
};
}
This adds Cache-Control: max-age=3600 header.
Webhook Integration Examples
Stripe Webhooks
message StripeEvent {
string type;
dynamic data;
}
@web put /webhook/stripe (StripeEvent event) {
if (event.type == "payment_intent.succeeded") {
// Handle successful payment
} else if (event.type == "customer.subscription.deleted") {
// Handle subscription cancellation
}
return {
json: { received: true }
};
}
GitHub Webhooks
message GitHubPush {
string ref;
dynamic repository;
dynamic commits;
}
@web put /webhook/github (GitHubPush push) {
// Verify signature via @headers["X-Hub-Signature-256"]
let branch = push.ref;
// Process push event
return {
json: { processed: true }
};
}
Twilio SMS
message TwilioIncoming {
string From;
string Body;
string To;
}
@web put /sms/incoming (TwilioIncoming sms) {
// Store incoming message
// sms.From is the sender's phone number
// sms.Body is the message content
return {
xml: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<Response>" +
" <Message>Thanks for your message!</Message>" +
"</Response>"
};
}
Error Handling
HTTP Status Codes
Adama web handlers return appropriate status codes:
| Scenario | Status Code |
|---|---|
| Success | 200 OK |
| Created | 200 OK |
| Redirect (forward) | 302 Found |
| Redirect (redirect) | 301 Moved Permanently |
| Document not found | 404 Not Found |
| No matching handler | 404 Not Found |
| Authorization failed | 403 Forbidden |
| Handler throws | 500 Internal Server Error |
Error Responses
Return explicit errors:
record User {
public int id;
public string name;
}
table<User> _users;
@web get /user/$id:int {
let found = (iterate _users where id == id);
if (found.size() == 0) {
return {
error: "User not found",
cors: true
};
}
return {
json: { user: found[0] },
cors: true
};
}
Complete REST API Example
Here's a full notes API to show how it all fits together:
record Note {
public int id;
public string title;
public string content;
public datetime created_at;
public datetime updated_at;
}
table<Note> notes;
private int next_id = 1;
// CORS preflight
@web options /api/notes {
return { cors: true };
}
@web options /api/notes/$id:int {
return { cors: true };
}
// List all notes
@web get /api/notes {
return {
json: { notes: iterate notes order by created_at desc },
cors: true,
cache_ttl_seconds: 5
};
}
// Get single note
@web get /api/notes/$id:int {
let found = (iterate notes where id == id);
if (found.size() == 0) {
return {
error: "Note not found",
cors: true
};
}
return {
json: { note: found[0] },
cors: true
};
}
// Create note
message CreateNoteRequest {
string title;
string content;
}
@web put /api/notes (CreateNoteRequest req) {
notes <- {
id: next_id,
title: req.title,
content: req.content,
created_at: Time.datetime(),
updated_at: Time.datetime()
};
let created_id = next_id;
next_id++;
return {
json: { id: created_id, created: true },
cors: true
};
}
// Update note
message UpdateNoteRequest {
string title;
string content;
}
@web put /api/notes/$id:int (UpdateNoteRequest req) {
let found = (iterate notes where id == id);
if (found.size() == 0) {
return {
error: "Note not found",
cors: true
};
}
(iterate notes where id == id).title = req.title;
(iterate notes where id == id).content = req.content;
(iterate notes where id == id).updated_at = Time.datetime();
return {
json: { updated: true },
cors: true
};
}
// Delete note
@web delete /api/notes/$id:int {
let count = (iterate notes where id == id).size();
(iterate notes where id == id).delete();
return {
json: { deleted: count > 0 },
cors: true
};
}
Usage:
# Create a note
curl -X POST https://region.adama-platform.com/myspace/notes/api/notes \
-H "Content-Type: application/json" \
-d '{"title": "My Note", "content": "Note content here"}'
# List notes
curl https://region.adama-platform.com/myspace/notes/api/notes
# Get single note
curl https://region.adama-platform.com/myspace/notes/api/notes/1
# Update note
curl -X PUT https://region.adama-platform.com/myspace/notes/api/notes/1 \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title"}'
# Delete note
curl -X DELETE https://region.adama-platform.com/myspace/notes/api/notes/1
Rate Limiting
Adama applies rate limits:
| Operation | Limit |
|---|---|
| Document creates | 100/minute per space |
| Direct messages | 1000/minute per document |
| Web requests | 1000/minute per document |
When rate limited, you get a 429 Too Many Requests response. Plan accordingly.
Practical Advice
Always enable CORS for browser access. Forgetting cors: true is the most common reason your frontend can't reach your endpoint:
@web get /api/data {
return {
json: { status: "ok" },
cors: true // Don't forget this!
};
}
Pair OPTIONS with other methods. Browsers send CORS preflight requests. If you don't handle OPTIONS, the actual request never arrives:
message Data {
string content;
}
// Always define OPTIONS for CORS preflight
@web options /api/resource {
return { cors: true };
}
@web get /api/resource {
return { json: { status: "ok" }, cors: true };
}
@web put /api/resource (Data d) {
return { json: { received: true }, cors: true };
}
Use appropriate response types. JSON for APIs. XML for webhooks that expect it (Twilio, some payment processors). HTML for server-rendered pages. Assets for file downloads.
Validate webhook signatures. Don't trust incoming webhook data blindly:
message Payload {
dynamic content;
}
procedure validateSignature(maybe<string> s, Payload p) -> bool {
return true;
}
@web put /webhook (Payload p) {
let signature = @headers["X-Signature"];
// Validate signature before processing
if (!validateSignature(signature, p)) {
return { error: "Invalid signature" };
}
// Process webhook
return { json: { ok: true } };
}
Cache read-heavy endpoints. If the data doesn't change every request, tell the browser:
@web get /config {
return {
json: { version: "1.0" },
cache_ttl_seconds: 300 // 5 minutes
};
}
Return structured errors. Give callers something useful to work with:
@web get /resource/$id:int {
if (id == 0) {
return {
json: {
error: true,
code: "NOT_FOUND",
message: "Resource not found"
},
cors: true
};
}
return {
json: { data: id },
cors: true
};
}
For real-time needs, pair REST with the WebSocket API or JavaScript Client. REST is for server integrations, webhooks, and one-off operations where holding a WebSocket open doesn't make sense.