Edit this page

Integrations and API surface

Plans never reach the network directly. They use integrations: declared, typed operations the runtime executes.

Declaring an integration

const payments = a.integration("payments", {
  provider: "stripe",
  config: { secret: "STRIPE_SECRET_KEY" },          // names of secrets, never values
  operations: {
    charge: {
      mode: "call",                                 // returns data during the plan
      input: { orderId: t.id(), amountCents: t.int().min(1) },
      output: { chargeId: t.string().max(100) },
    },
    refund: { mode: "effect", input: { chargeId: t.string().max(100) } },  // sent after commit
  },
});

const charge = payments.call("charge", { orderId, amountCents: 2500 });   // charge.chargeId
payments.send("refund", { chargeId });
  • call runs during the plan, and its output is validated against output. Calls are journaled: a re-run after a conflict reuses the result. A route can make at most 3 calls.
  • effect is fire-and-forget, delivered through the outbox after commit, with retries.

Receiving webhooks

import { verify } from "@aleph-lang/sdk";

a.webhook("POST /webhooks/stripe", {
  verify: verify.stripe("STRIPE_WEBHOOK_SECRET"),
  dedupe: { bodyField: "id", ttl: "3d" },
})
  .body({ id: t.string().max(64), type: t.string().max(64) })
  .handle(($, { body }) => {
    $.log("stripe event", body.type);
    return { received: true };
  });
  • Verification first. The signature is checked on the exact raw body, in constant time, before the plan runs. A bad, missing, or stale signature returns 401 invalid_signature.
  • Lenient bodies. Declare only the fields you use; senders add fields over time, and undeclared ones are ignored.
  • Dedupe. With dedupe, a repeated delivery of the same event id gets the first response replayed, so retries from the sender are harmless.
SchemeUse
verify.stripe(secret)Stripe-Signature, 5-minute tolerance
verify.github(secret)X-Hub-Signature-256
verify.hmacSha256({ secret, header, prefix?, encoding?, timestampHeader?, tolerance? })Any HMAC-SHA256 sender

In development, pass secrets with aleph dev --secret STRIPE_WEBHOOK_SECRET=whsec_....

OpenAPI, MCP, and typed clients

Every route's inputs are declared, and its response type is inferred (or declared with .returns), so Aleph can describe your API precisely.

A typed fetch client

npx aleph client app.ts -o src/client.ts
import { AlephError, createClient, type Routes } from "./client";

const api = createClient({
  baseUrl: "https://api.example.com",
  headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
});

const order = await api.postOrders({ items: [{ sku: "BK-1", qty: 2 }], shipTo: { city: "Paris", zip: "75001" } });
order.total; // Decimal: `${number}`

try {
  await api.postOrdersOrderIdPay({ orderId: order.orderId }, { idempotencyKey: crypto.randomUUID() });
} catch (e) {
  if (e instanceof AlephError && e.body.error === "missing") { /* typed declared errors */ }
}

type OrderList = Routes["myOrders"]["output"];
  • Functions: one per route, named after the route id. Params, query, and body are merged into one input object.
  • Results: precise types, including nullability, decimals as strings, enums as unions, and union types for routes with several $.returns.
  • Errors: an AlephError whose body is typed by the route's declared errors, including their details.

Commit the generated file and regenerate it in CI. Changing a route then breaks the code that uses it at compile time. The example apps include a test that fails if src/client.ts is stale.

OpenAPI

npx aleph openapi app.ts > openapi.json

An OpenAPI 3.1 document with:

  • Inputs: parameter and body schemas.
  • Responses: response schemas, success statuses, and typed error bodies (with details).
  • Metadata: deprecation, and x-aleph-* extensions for idempotency, webhooks, and examples.

MCP tools for agents

a.route("GET /orgs/:orgId/members/by-email/:email")
  .params({ orgId: t.string().max(64), email: t.string().min(3).max(200) })
  .member("orgId", { apiKeyScopes: ["members:read"] })
  .describe("Find a member by email")
  .mcp("find_member")
  .errors({ notFound: err.notFound() })
  .handle(($, { params, errors }) => {
    const member = Member.findBy("byEmail", [params.orgId, params.email.lower()]);
    $.ensure(member.exists(), errors.notFound);
    return member;
  });
npx aleph mcp app.ts

This lists the routes exposed as MCP tools, with JSON Schema inputs. A tool call goes through the route, with the same auth, validation, and limits. That's what makes it safe to hand an agent: whatever it asks for, its usage is bounded.