HTTP & WebSocket
This page is for developers building SemiLayer clients in languages or environments
where @semilayer/client is not available — Go, Python, Ruby, Rust, browser vanilla JS,
edge runtimes, and anything else that can speak HTTP and WebSocket.
The protocol is simple. If you can make an HTTP POST and a WebSocket connection,
you can implement the full SemiLayer API surface.
Copy
https://api.semilayer.com
Pass your API key as a Bearer token:
Copy
Authorization: Bearer sk_live_...
For per-user access control, also pass the user JWT:
Copy
X-User-Token: eyJhbGci...
Copy
POST /v1/search/:lens
Content-Type: application/json
Authorization: Bearer {apiKey}
Copy
{
"query" : "lightweight running shoes" ,
"limit" : 10 ,
"minScore" : 0.5 ,
"mode" : "semantic"
}
Response:
Copy
{
"results" : [
{
"id" : "emb_abc" ,
"sourceRowId" : "42" ,
"content" : "..." ,
"metadata" : { "name" : "Air Glide 3" , "price" : 89.99 } ,
"score" : 0.94
}
] ,
"meta" : { "lens" : "products" , "query" : "..." , "count" : 10 , "durationMs" : 18 }
}
Copy
POST /v1/similar/:lens
Content-Type: application/json
Authorization: Bearer {apiKey}
Copy
{ "id" : "42" , "limit" : 5 , "minScore" : 0.7 }
Response: same shape as search.
Copy
POST /v1/query/:lens
Content-Type: application/json
Authorization: Bearer {apiKey}
Copy
{
"where" : { "category" : "footwear" } ,
"orderBy" : { "field" : "price" , "dir" : "asc" } ,
"limit" : 20 ,
"cursor" : "eyJpZCI6NTB9"
}
Response:
Copy
{
"rows" : [ { "id" : 42 , "name" : "Air Glide 3" , "price" : 89.99 } ] ,
"meta" : { "lens" : "products" , "total" : 142 , "nextCursor" : "..." , "count" : 20 , "durationMs" : 12 }
}
Copy
{ "error" : "Access denied by lens rules" }
Status Meaning 400Bad request body 401Invalid API key 403Operation not permitted by access rules 404Lens not found 429Rate limit — check Retry-After header 502Upstream (embedding) error
The streaming endpoint uses a single multiplexed WebSocket connection. You can run
multiple operations (search, subscribe, observe) over one socket using the id field
to correlate frames.
Copy
GET wss://api.semilayer.com/v1/stream/:lens
?key={apiKey}
&userToken={userJwt} ← optional
The lens name is in the path. The API key is in the query string.
ℹ️ TLS is required (wss://). Plaintext WebSocket (ws://) is only available
in self-hosted deployments on localhost.
Client opens WebSocket connection
Server authenticates the API key during the HTTP upgrade
On auth failure: server closes with code 4401 before the connection upgrades
On success: connection is open, client can send op frames
All frames are JSON-encoded text messages.
Op frame:
Copy
{
"id" : "op_1" ,
"op" : "search.stream" ,
"params" : { "query" : "running shoes" , "limit" : 50 }
}
Field Type Description idstringUnique ID for this op — correlates with response frames opstringOne of the op types below paramsobjectOp-specific parameters
Op types:
opDescription Relevant params search.streamChunked semantic search SearchParamsquery.streamChunked direct query QueryParamssubscribeLive tail — all changes { filter?: object }observeWatch one record { recordId: string }unsubscribeCancel a running op (op frame with no params)
Heartbeat pong:
Must be sent in response to every ping frame within 30 seconds, or the server
closes the connection.
Row frame — one result from search.stream or query.stream:
Copy
{
"id" : "op_1" ,
"type" : "row" ,
"data" : {
"id" : "emb_abc" ,
"sourceRowId" : "42" ,
"content" : "..." ,
"metadata" : { "name" : "Air Glide 3" , "price" : 89.99 } ,
"score" : 0.94
}
}
Done frame — signals the end of a chunked op:
Copy
{
"id" : "op_1" ,
"type" : "done" ,
"meta" : { "count" : 50 , "durationMs" : 42 }
}
Event frame — one change from subscribe or observe:
Copy
{
"id" : "op_2" ,
"type" : "event" ,
"kind" : "insert" ,
"record" : { "id" : 99 , "name" : "New Product" , "price" : 29.99 }
}
kind is "insert" | "update" | "delete".
Error frame — operation-level or connection-level error:
Copy
{
"id" : "op_1" ,
"type" : "error" ,
"code" : "rate_limited" ,
"message" : "WebSocket row budget exceeded"
}
If id is absent, the error applies to the whole connection.
codeMeaning rate_limitedRow/connection budget exceeded forbiddenLens rules denied the op bad_requestInvalid op params not_foundLens not found internalServer-side error
Ping frame — heartbeat from server:
Respond with { "type": "pong" }.
Code Meaning 1000Normal close 1011Server-side internal error 4401Auth failed during upgrade 4403Lens access forbidden 4290Over concurrent-connection quota 4291Row budget exceeded mid-stream
A complete, working client you can adapt or use as a reference:
Copy
const BASE_URL = 'https://api.semilayer.com'
const WS_URL = 'wss://api.semilayer.com'
async function apiPost<T>(
apiKey : string ,
path : string ,
body : unknown ,
userToken ?: string ,
): Promise <T> {
const headers : Record <string , string > = {
'Content-Type' : 'application/json' ,
'Authorization' : `Bearer ${apiKey} ` ,
}
if (userToken) headers['X-User-Token' ] = userToken
const res = await fetch (`${BASE_URL} ${path} ` , {
method : 'POST' ,
headers,
body : JSON .stringify (body),
})
if (!res.ok ) {
const err = await res.json ().catch (() => ({ error : res.statusText }))
throw new Error (`${res.status} : ${err.error} ` )
}
return res.json () as Promise <T>
}
export const search = (apiKey : string , lens : string , params : object ) =>
apiPost (apiKey, `/v1/search/${lens} ` , params)
export const similar = (apiKey : string , lens : string , params : object ) =>
apiPost (apiKey, `/v1/similar/${lens} ` , params)
export const query = (apiKey : string , lens : string , params : object = {} ) =>
apiPost (apiKey, `/v1/query/${lens} ` , params)
export function streamSearch (
apiKey : string ,
lens : string ,
params : object ,
onResult : (data: unknown ) => void ,
onDone : (meta: unknown ) => void ,
onError ?: (code: string , message: string ) => void ,
): () => void {
const url = `${WS_URL} /v1/stream/${lens} ?key=${encodeURIComponent (apiKey)} `
const ws = new WebSocket (url)
const opId = `op_${Date .now()} `
ws.addEventListener ('open' , () => {
ws.send (JSON .stringify ({ id : opId, op : 'search.stream' , params }))
})
ws.addEventListener ('message' , (e ) => {
const frame = JSON .parse (e.data as string )
if (frame.type === 'ping' ) {
ws.send (JSON .stringify ({ type : 'pong' }))
return
}
if (frame.type === 'row' && frame.id === opId) onResult (frame.data )
if (frame.type === 'done' && frame.id === opId) { onDone (frame.meta ); ws.close () }
if (frame.type === 'error' ) {
onError?.(frame.code , frame.message )
ws.close ()
}
})
return () => ws.close ()
}
export function subscribe (
apiKey : string ,
lens : string ,
onEvent : (kind: string , record: unknown ) => void ,
): () => void {
const url = `${WS_URL} /v1/stream/${lens} ?key=${encodeURIComponent (apiKey)} `
const ws = new WebSocket (url)
const opId = `op_${Date .now()} `
ws.addEventListener ('open' , () => {
ws.send (JSON .stringify ({ id : opId, op : 'subscribe' , params : {} }))
})
ws.addEventListener ('message' , (e ) => {
const frame = JSON .parse (e.data as string )
if (frame.type === 'ping' ) { ws.send (JSON .stringify ({ type : 'pong' })); return }
if (frame.type === 'event' ) onEvent (frame.kind , frame.record )
})
return () => ws.close ()
}
Copy
import httpx
import json
BASE_URL = "https://api.semilayer.com"
def search (api_key: str , lens: str , query: str , limit: int = 10 ):
response = httpx.post(
f"{BASE_URL} /v1/search/{lens} " ,
headers={
"Authorization" : f"Bearer {api_key} " ,
"Content-Type" : "application/json" ,
},
json={"query" : query, "limit" : limit},
)
response.raise_for_status()
return response.json()
results = search("sk_live_..." , "products" , "eco-friendly water bottle" )
for r in results["results" ]:
print (r["metadata" ]["name" ], r["score" ])
Copy
package semilayer
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const BaseURL = "https://api.semilayer.com"
type SearchResult struct {
ID string `json:"id"`
SourceRowID string `json:"sourceRowId"`
Metadata map [string ]interface {} `json:"metadata"`
Score float64 `json:"score"`
}
type SearchResponse struct {
Results []SearchResult `json:"results"`
}
func Search (apiKey, lens, query string , limit int ) (*SearchResponse, error ) {
body, _ := json.Marshal(map [string ]interface {}{
"query" : query,
"limit" : limit,
})
req, _ := http.NewRequest("POST" ,
fmt.Sprintf("%s/v1/search/%s" , BaseURL, lens),
bytes.NewReader(body),
)
req.Header.Set("Authorization" , "Bearer " +apiKey)
req.Header.Set("Content-Type" , "application/json" )
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil , err
}
defer resp.Body.Close()
var result SearchResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil , err
}
return &result, nil
}