Apache 2.0 · Open Source
BETA

APIs that work everywhere.

Telepact bridges programs across any transport boundary. JSON in, JSON out — with optional binary efficiency, dynamic response shaping, and type‑safe code generation.

Capability, without Compromise

Telepact combines the best ideas from REST, gRPC, and GraphQL into one cohesive ecosystem — without their trade-offs.

{ }

JSON as a Query Language

API calls and SELECT-style field selection achieved entirely through JSON abstractions. No special parsers, no DSLs. Clients using only native JSON libraries are first-class citizens.

Binary Without Code Gen

Binary protocols established through runtime handshakes — not build-time code generation. Opt into MessagePack efficiency without any mandatory toolchain setup.

🔗

Hypermedia Without HTTP

API responses include "links" — functions with pre-filled arguments. HATEOAS-style navigation using pure JSON, over any transport. Discoverability built in.

Simple request, powerful response

No special client library needed. Just send JSON, get JSON

1. Plain JSON calls

Start with the smallest possible Telepact API: one function, one request, one response, all expressed as plain JSON.

Schema + Request + Response
Schema
- fn.helloWorld: {}
  ->:
    - Ok_:
        msg: string
Request
[
  {},
  {
    "fn.helloWorld": {}
  }
]
Response
[
  {},
  {
    "Ok_": {
      "msg": "Hello world!"
    }
  }
]
No required client library. If you can serialize JSON and send bytes over HTTP, WebSockets, NATS, stdio, or another transport, you can call a Telepact API.

2. Dynamic response shaping

Clients can ask for fewer fields with @select_, which keeps responses lean without introducing a separate query language.

Only return what you ask for
Schema
- struct.User:
    id: integer
    name: string
    email: string
    bio: string

- fn.getUser:
    id: integer
  ->:
    - Ok_:
        user: struct.User
Request
[
  {
    "@select_": {
      "->": {
        "Ok_": [
          "user"
        ]
      },
      "struct.User": [
        "name",
        "email"
      ]
    }
  },
  {
    "fn.getUser": {
      "id": 42
    }
  }
]
Response
[
  {},
  {
    "Ok_": {
      "user": {
        "name": "Ada",
        "email": "ada@example.com"
      }
    }
  }
]
@select_ trims payloads at the protocol level. You get GraphQL-style field selection while staying inside JSON documents.

3. Hypermedia without HTTP

Responses can include pre-filled function calls, which act like links the client can follow next without building URLs or stitching arguments together.

Link the next call
Schema
- fn.getTicket:
    id: string
  ->:
    - Ok_:
        title: string

- fn.createTicket:
    title: string
  ->:
    - Ok_:
        id: string
        view: fn.getTicket
Request
[
  {},
  {
    "fn.createTicket": {
      "title": "Fix mobile layout"
    }
  }
]
Response
[
  {},
  {
    "Ok_": {
      "id": "ticket-7",
      "view": {
        "fn.getTicket": {
          "id": "ticket-7"
        }
      }
    }
  }
]
Hypermedia stays transport-agnostic. The next action is encoded as data, so clients can navigate workflows without assuming HTTP routes or query strings.

4. Opt-in binary protocol

Clients can opt into efficient binary exchanges with any Telepact server, using a just-in-time negotiation handled cleanly by the Telepact runtime.

Same data, smaller wire format
Schema
- struct.Event:
    id: integer
    title: string
    status: string

- fn.listEvents:
    limit: integer
  ->:
    - Ok_:
        events: [struct.Event]
Plain JSON Request
[
  {},
  {
    "fn.listEvents": {
      "limit": 8
    }
  }
]
Plain JSON Response · 419 B
[{},{"Ok_":{"events":[{"id":964950937,"status":"eta","title":"ga
mma"},{"id":547909113,"status":"epsilon","title":"upsilon"},{"id
":1271036600,"status":"theta","title":"tau"},{"id":1233703683,"s
tatus":"sigma","title":"iota"},{"id":677484023,"status":"chi","t
itle":"xi"},{"id":1504773852,"status":"phi","title":"nu"},{"id":
801153710,"status":"tau","title":"omicron"},{"id":502003645,"sta
tus":"upsilon","title":"kappa"}]}}]
Binary Request (After Negotiation)
[
  {
    "@pac_": true,
    "@time_": 5000,
    "@bin_": [
      1466586054
    ]
  },
  {
    "fn.listEvents": {
      "limit": 8
    }
  }
]
Negotiated Binary Response · 169 B
���@pac_å@bin_��WjSƁ·�·��··��···��9����eta�gamma�� �m��epsilon�u
psilon��K�z��theta�tau��I��·�sigma�iota��(a���chi�xi��Y�·ܣphi�nu
��/����tau�omicron��·����upsilon�kappa
Cut your payload size in half, without the hassle. Convention says you can't have binary without rigid code generation toolchains. Now you can, with Telepact.

How it stacks up

Every API tool makes trade-offs. Telepact was designed so you don't have to choose.

Capability OpenAPI JSON-RPC gRPC GraphQL Telepact
No transport restrictions ~JSON-RPC is transport-agnostic in theory, but lack of support for metadata at the protocol level usually restrict JSON-RPC cases to transports that support headers, such as HTTP. ~The GraphQL protocol has limited ergonomics, so in practice most use cases are restricted to what the client and server can agree on with library support, usually HTTP.
No required client libraries
Type-safe generated code ~OpenAPI code generation exists, but quality and type coverage vary across generators and target languages.
Human-readable wire format ~GraphQL documents are text, but the query language is brittle to work with directly, so most use cases rely on dedicated graphql libraries.
Built-in binary serialization
Dynamic response shaping
Built-in API documentation ~OpenAPI tooling exists to generate documentation, but this documentation has to served out-of-band from the API itself.
Built-in mocking

Write once, call from anywhere

Define your schema once. Implement servers and clients in any supported language with full type safety.

# divide.telepact.yaml

- fn.divide:
    x: integer
    y: integer
  ->:
    - Ok_:
        result: number
    - ErrorCannotDivideByZero: {}
from telepact import FunctionRouter, Message, Server, TelepactSchema

schema = TelepactSchema.from_directory('./api')

async def divide(function_name: str, request_message: Message) -> Message:
    args = request_message.body[function_name]
    x, y = args['x'], args['y']
    if y == 0:
        return Message({}, {'ErrorCannotDivideByZero': {}})
    return Message({}, {'Ok_': {'result': x / y}})

options = Server.Options()
options.auth_required = False
function_router = FunctionRouter({'fn.divide': divide})
server = Server(schema, function_router, options)

# Plug into any framework — Starlette, Flask, FastAPI...
async def http_handler(request):
    response = await server.process(await request.body())
    return Response(content=response.bytes)
// No Telepact library required

const request = [
  {},
  {
    "fn.divide": {
      x: 10,
      y: 2,
    },
  },
];

const response = await fetch("/api/telepact", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(request),
});

const [headers, body] = await response.json();

if (body.Ok_) {
  console.log(body.Ok_.result);
} else {
  console.error(headers, body);
}
import io.github.telepact.Client;
import io.github.telepact.Message;
import io.github.telepact.Serializer;

BiFunction<Message, Serializer, Future<Message>> adapter = (message, serializer) ->
    CompletableFuture.supplyAsync(() -> {
        var requestBytes = serializer.serialize(message);
        var responseBytes = transport.send(requestBytes);
        return serializer.deserialize(responseBytes);
    });

var client = new Client(adapter, new Client.Options());
var request = new Message(
    Map.of(),
    Map.of("fn.divide", Map.of("x", 10, "y", 2))
);
var response = client.request(request);

if ("Ok_".equals(response.getBodyTarget())) {
    System.out.println(response.getBodyPayload().get("result"));
}
// First generate bindings from the schema:
// telepact codegen --schema-dir ./api --lang go --package calcclient --out ./gen

package main

import (
    "context"
    "log"

    calcclient "example.com/project/gen"
    telepact "github.com/telepact/telepact/lib/go"
)

func main() {
    adapter := func(ctx context.Context, request telepact.Message, serializer *telepact.Serializer) (telepact.Message, error) {
        requestBytes, err := serializer.Serialize(request)
        if err != nil {
            return telepact.Message{}, err
        }

        responseBytes, err := transport.Send(ctx, requestBytes)
        if err != nil {
            return telepact.Message{}, err
        }

        return serializer.Deserialize(responseBytes)
    }

    client, err := calcclient.NewClient(adapter, telepact.NewClientOptions())
    if err != nil {
        log.Fatal(err)
    }

    response, err := client.Divide(context.Background(), calcclient.FnDivideArgs{
        X: 10,
        Y: 2,
    })
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("result=%v", response.Ok.Result)
}

Official SDK libraries

First-class support for four languages. Same API, same schema, fully cross-compatible.

🟦

TypeScript

npm i telepact

🐍

Python

pip install --pre telepact

🔵

Go

go get github.com/
telepact/telepact/lib/go

Java 21+

io.github.telepact:telepact

Built-in tooling

CLI workflows, an interactive console, mock servers, and more — batteries included.

🛠️ CLI

Fetch schemas, generate type-safe bindings, compare compatibility, and run mock or demo servers from one command line tool.

uv tool install --prerelease=allow telepact-cli

💻 Console

Interactive browser-based API explorer. Build requests, browse documentation, and test against live servers.

npx telepact-console -p 8080

🧪 Mock Server

Spin up a fully-functional mock server from your schema. Perfect for frontend development and testing.

telepact mock --dir ./api

Ready to build universal APIs?

Read the documentation to get started!