Skip to main content

Methods

In the Centia.io API, instead of sending raw SQL every time you want to run a statement, you can wrap your SQL statements inside JSON-RPC methods. This means you define a named method which holds your SQL query along with optional instructions on how to interpret and format the data types.

Why do this?

  • Reusability: Once a method is created, clients just call the method by name.
  • Consistency: You define type hints to specify the data types of the query results and formats for how the data should appear (e.g., date formats).
  • Encapsulation: The SQL code is stored on the server side, reducing the chance of SQL injection and improving security.
  • Easier client code: Clients don’t need to embed or parse SQL, they just call methods.

JSON-RPC is a stateless, lightweight remote procedure call (RPC) protocol that uses JSON for data interchange. Defined by the JSON-RPC 2.0 specification, it enables clients to invoke methods on a server by sending JSON-formatted requests and receiving JSON-formatted responses, with support for batch calls, notifications, and standardized error handling.

How it works

  1. You create a JSON-RPC method by posting a method definition to the methods API.
  2. You can include:
    • type_hints: indicating expected types for query results.
    • type_formats: specifying formats for those types (like date formatting).
    • output_format: specifying output format for the result (like json, csv, ndjson). See SQL → Output formats.
    • srs: specifying output geospatial reference system for PostGIS geometry. Must be specified as an EPSG code. Defaults to 4326.
  3. When calling the method later, clients only send the method name and parameters (if any).

Example

Suppose you have a SQL query that fetches the current timestamp:

select now() as date

You can wrap it as a JSON-RPC method named with a date formatting hint: getDate

POST  https://api.centia.io/api/v4/methods
Content-Type: application/json
Accept: application/json; charset=utf-8
Authorization: Bearer abc123

{
"q": "select now() as date",
"method": "getDate",
"type_formats": {
"date": "D M d Y"
}
}
  • Here "method": "getDate" tells the server to save this SQL as a callable method.
  • "type_formats" specifies that the date column should be formatted as D M d Y (e.g., "Thu May 08 2025").

After this is done, a client can simply call:

{
"jsonrpc": "2.0",
"method": "getDate",
"id": 1
}

And receive properly typed and formatted results without sending the SQL each time..

{
"jsonrpc": "2.0",
"result": {
"schema": {
"date": {
"type": "timestamptz",
"array": false
}
},
"data": [
{
"date": "Thu May 08 2025"
}
]
},
"id": 1
}

The result property is similar to what the SQL API returns.

The REST API is used like this:

POST  https://api.centia.io/api/v4/call
Content-Type: application/json
Accept: application/json; charset=utf-8
Authorization: Bearer abc123

{
"jsonrpc": "2.0",
"method": "getDate",
"id": 1
}

And the websocket API is used like this:

const socket = new WebSocket("wss://event.centia.io?token=abc123");
socket.onopen = () => {
socket.send(JSON.stringify({
"jsonrpc": "2.0",
"method": "getDate",
"id": 1
}))
}

Sending parameters

When creating a method, the statement is not executed, and therefore you don't have to provide any parameters.

So if you want to create a method that gives you a formatted date plus a given number of days like this SQL request:

{
"q": "select date(:date) + :days::int as result",
"params": {
"date": "09-07-10",
"days": 0
},
"type_hints": {
"date": "timestamptz",
"days": "int"
},
"type_formats": {
"result": "l jS F Y",
"date": "d-m-y"
}
}

You create the method without params:

{
"method": "getDatePlusDays",
"q": "select date(:date) + :days::int as result",
"type_hints": {
"date": "timestamptz",
"days": "int"
},
"type_formats": {
"result": "l jS F Y",
"date": "d-m-y"
}
}

And when call the the method with params:

{
"jsonrpc": "2.0",
"method": "getDatePlusDays",
"params": {
"date": "09-07-10",
"days": 10
},
"id": 1
}

Notifications

JSON-RPC supports notifications, a special type of request sent without an id member. Notifications instruct the server to perform the method but indicate that no response is expected.

A notification payload might look like:

{
"jsonrpc": "2.0",
"method": "notifyEvent",
"params": { "event": "user_signup", "userId": 123 }
}

Since the id field is omitted, the server must not send any response. Clients should handle notifications as one-way messages.

In a REST context, notifications will result in a 204 No Content response. In a websocket context, notifications will not result in a response.

Execute multiple methods

You can run multiple methods like this:

[
{
"jsonrpc": "2.0",
"method": "withDrawFromAccount",
"params": { "name": "joe", "amount": 100 }
},
{
"jsonrpc": "2.0",
"method": "depositToAccount",
"params": { "name": "peter", "amount": 100 }
}
]

This will run both withDrawFromAccount and depositToAccount in same transaction block, meaning both transactions must succeed or nothing will happen.

If you are going to call the same transactional method for e.g., inserting multiple records, it's most efficient to call the method once with multiple parameters instead:

{
"jsonrpc": "2.0",
"method": "notifyEvent",
"params": [
{ "event": "user_signup", "userId": 123 },
{ "event": "user_signup", "userId": 124 },
{ "event": "user_signup", "userId": 125 },
{ "event": "user_signup", "userId": 126 },
{ "event": "user_signup", "userId": 127 },
{ "event": "user_signup", "userId": 128 }
]
}

Error Handling

In the JSON-RPC 2.0 specification, errors are communicated using a standard structure in the response. If an error occurs during the processing of a request, the error object will be present in the response instead of the result object.

Error Response Format

An error response has the following structure:

{ "jsonrpc": "2.0", "error": { "code":  , "message": "  ", "data": }, "id": <id from request | null> }

Error Object Fields

  • code: A numeric code that indicates the type of error. JSON-RPC defines these codes:

    • -32600: Invalid Request (The JSON sent is not a valid Request object.)
    • -32601: Method Not Found (The method does not exist or is unavailable.)
    • -32602: Invalid Params (Invalid or missing method parameters.)
    • -32603: Internal Error (Internal server error occurred while processing the request.)
  • message: A short description of the error. This is intended for developers and should not change based on runtime context.

  • data: Additional information about the error, which could include server-specific debug information or additional details to help the client understand what went wrong.

  • id: Mirrors the id from the request or null when the id is not present in the request (e.g., for notifications).

Example Error Scenarios and Responses

Invalid Request

This error occurs when the JSON payload does not conform to the JSON-RPC 2.0 specification.

Request:

{ "jsonrpc": "2.0", "invalid_property": "invalid value" }

Response:

{ "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Request" }, "id": null }

Method Not Found

This error occurs when an unknown or unsupported method is invoked.

Request:

{ "jsonrpc": "2.0", "method": "unknownMethod", "id": 10 }

Response:

{ "jsonrpc": "2.0", "error": { "code": -32601, "message": "Method not found" }, "id": 10 }

Invalid Parameters

This error occurs when the parameters provided in the request are invalid.

Request:

{ "jsonrpc": "2.0", "method": "getDate", "params": { "invalid": "param" }, "id": 2 }

Response:

{ "jsonrpc": "2.0", "error": { "code": -32602, "message": "Invalid params" }, "id": 2 }

Internal Server Error

This error occurs when an unexpected error happens on the server while processing the request.

Response:

{ "jsonrpc": "2.0", "error": { "code": -32603, "message": "Internal error" }, "id": 3 }

Client-Side Handling

Clients should handle error responses gracefully by checking for the presence of the error object and using the code and message fields to determine actions or inform the user.

Get methods

Get all methods
GET https://api.centia.io/api/v4/methods HTTP/1.1
Accept: application/json
Authorization: Bearer abc123
Get a specific method
GET https://api.centia.io/api/v4/methods/getDate HTTP/1.1
Accept: application/json
Authorization: Bearer abc123

Update methods

Update a method
PATCH https://api.centia.io/api/v4/methods/getDate HTTP/1.1
Content-Type: application/json
Authorization: Bearer abc123

{
"q": "select now()::date as date",
"type_formats": {
"date": "Y-m-d"
}
}

Delete methods

Delete a method
DELETE https://api.centia.io/api/v4/methods/getDate HTTP/1.1
Authorization: Bearer abc123
Delete multiple methods
DELETE https://api.centia.io/api/v4/methods/getDate,getDatePlusDays HTTP/1.1
Authorization: Bearer abc123

Using the SDK

The SDK calls JSON-RPC methods through the Rpc class:

  • Class: new Rpc()
  • Method: call(request: RpcRequest): Promise<RpcResponse>
  • Endpoint: POST https://api.centia.io/api/v4/call

Types (simplified):

  • RpcRequest has jsonrpc: "2.0", method, optional params, optional id
  • RpcResponse has jsonrpc: "2.0", id, and result with { schema, data }
import { Rpc } from "@centia-io/sdk";

const rpc = new Rpc();

const payload = { a: 1, b: "hello" };

const res = await rpc.call({
jsonrpc: "2.0",
method: "typeTest",
params: payload,
id: 1
});

console.log(res.result.schema);
console.log(res.result.data); // array of rows

Typing the rows:

import type { PgTypes } from "@centia-io/sdk";

interface Row extends PgTypes.DataRow {
a: number;
b: string;
}

const res = await rpc.call({ jsonrpc: "2.0", method: "typeTest", params: payload }) as PgTypes.RpcResponse<Row>;

createApi

A tiny helper that builds a Proxy around Rpc so you can call api.someMethod(params) directly, with TypeScript autocompletion and type-checking based on your own interface.

Under the hood, each property access becomes a JSON-RPC call with the property name as the method. The helper returns result.data (array of rows) from the RPC response.

import { createApi } from "@centia-io/sdk";
import type { PgTypes, RowOfApiMethod } from "@centia-io/sdk";

// Define the shape of your RPC methods and return types
// Don't write this by hand. Get the interfaces from /api/v4/interfaces
interface MyApi {
getBandById(params: {
band_id: number;
}): Promise<Array<{
name: PgTypes.Varchar;
subgenre: PgTypes.Varchar;
albums: PgTypes.PgArray<PgTypes.Varchar>;
}>>;
}

// Create the API proxy
const api = createApi<MyApi>();

// If you need the type of the result, use the helper RowOfApiMethod
type Band = RowOfApiMethod<MyApi, "getBandById">

// Call the method
const bands: Band[] = await api.getBandById({
band_id: 1
});

console.log(bands); // typed row array
note
  • createApi<T>() relies on naming conventions: the property name is the JSON-RPC method name.
  • Each call returns result.data from the RPC response (array of rows).

JSON-RPC TypeScript interfaces

Instead of defining the interfaces yourself as shown above, you can use the API /api/v4/interfaces helper to get the interfaces for all JSON-RPC methods.

  1. Define your JSON-RPC method. Input and output types are inferred from an actual request/response.

Consider this method definition:

{
"q": "select :x as unknown",
"method": "getX"
}

The type of x can not be inferred from the definition. So requesting /api/v4/interfaces will return:

export interface Api {
getX(params: Record<string, unknown>): Promise<Record<string, unknown>>;
}
  1. You will need to 'cast' the field x to the correct type. This is not necessary for real columns in tables, but for the example above, you can cast the field to int:
{
"q": "select :x::int as my_int",
"method": "getX"
}
  1. Dry run the request to set the types. Types are only inferred for dry run calls. This will cache the types on the server:
POST https://api.centia.io/api/v4/call/dry
Content-Type: application/json
Accept: application/json; charset=utf-8
Authorization: Bearer abc123

{
"jsonrpc": "2.0",
"method": "getX",
"params": { "x": 1 },
"id": 1
}

And /api/v4/interfaces will now return a typed interface:

export interface Api {
getX(params: { x: number; }): Promise<{ my_int: number; }[]>;
}

This is ready for import and use in your application:

api.ts
export interface Api {
getX(params: { x: number; }): Promise<{ my_int: number; }[]>
}
main.ts
import { createApi } from "@centia-io/sdk"
import type { Api } from "./api"

const api = createApi<Api>()
const res = await api.getX({ x: 1 })
console.log(res)