Welcome! This guide is a walk-through setting up a local environment and deploying the working example in this repository to the PolyAPI platform.
By the end of this guide you will have:
- A working Node.js/TypeScript development environment (macOS or Windows).
- This project connected to our PolyAPI tenant.
- A generated PolyAPI SDK.
- The example in
src/deployed end-to-end:
flowchart LR
A[HTTP request] --> B["motdWebhook
(webhook - public entrypoint)"]
B --> V["validateMotdPayload
(security function)"]
V -->|invalid| X[403 rejected]
V -->|valid| C["motdTrigger
(trigger - wires webhook to server function)"]
C --> D["motdServer
(server function)"]
D --> E["motdClient
(client function - decides by mood)"]
E -->|mood: funny| F["getRandomJoke
(trained API Function)"]
E -->|mood: wise| G["getRandomAdvice
(trained API Function)"]
classDef darkred fill:#b71c1c,stroke:#7f0000,color:#fff
classDef lightred fill:#ffcdd2,stroke:#e57373,color:#000
classDef blue fill:#0d47a1,stroke:#082a60,color:#fff
classDef yellow fill:#b8860b,stroke:#7c5b00,color:#000
class B darkred
class F,G lightred
class D,V blue
class E yellow
- Key concepts
- Prerequisites
- Install Node.js and npm
- Get the project and install dependencies
- Connect the CLI to our PolyAPI tenant
- Train API Functions
- Deploy to the PolyAPI platform
- Wire the webhook
- Test it end-to-end
PolyAPI implementations are built from a small set of constructs:
| Construct | What it is | In our example |
|---|---|---|
| Webhook | A public HTTP endpoint that receives requests | motdWebhook |
| Trigger | Wiring that connects a webhook to a server function | motdTrigger |
| Server function | Backend logic that runs on the PolyAPI platform | motdServer |
| Client function | Reusable logic callable from server (or client) functions | motdClient |
| API Function | Typed wrapper around a real HTTP endpoint, generated ("trained") from an OpenAPI spec | getRandomJoke, getRandomAdvice |
| Security function | A server function that validates/authorizes a webhook request before it's let through | validateMotdPayload |
All resources in this example live under the foo.bar context.
- Access to our Foo PolyAPI tenant: base URL (e.g.
https://eu1.polyapi.io) and an API key. Ask your PolyAPI admin if you don't have these yet. - A code editor (VS Code recommended).
- Git (to clone/manage the project repository).
You need Node.js 18 or later (20+ recommended). npm is installed
automatically with Node.js.
Install Node.js with Homebrew:
brew install nodenode -v # v18.x.x or higher
npm -vClone the repository and move into it:
git clone <this-repo-url> poly-getting-started
cd poly-getting-startedInstall dependencies:
npm installCreate your local environment file from the template:
cp .env.example .envFill in .env with the tenant base URL and your personal API key:
POLY_INSTANCE_URL=https://eu1.polyapi.io
POLY_API_KEY=your_poly_api_key_hereRun the interactive setup once per machine. It stores your base URL and API key locally so the CLI and generated SDK can talk to PolyAPI:
npx poly setupYou'll be prompted for the same base URL and API key you put in
.env. You can also pass them directly instead of the interactive prompts:
npx poly setup https://eu1.polyapi.io <your-api-key>You'll also be asked whether you want to use a project template:
? Do you want to use a project template?
1) No (empty project)
2) Default Project (instance)
Choose 1) No (empty project) — this repo already has its own webhook,
server function, and client function under src/, so there's
nothing for a template to scaffold.
Not every integration needs hand-written code. If a real HTTP API already exists, PolyAPI can generate a typed wrapper function for it directly from an OpenAPI spec — no request/response handling to write by hand. This is called training an API Function.
This project trains two, each from a small OpenAPI spec describing one free, no-auth demo API:
src/apiFunctions/jokeApi.openapi.yaml— trainsgetRandomJokesrc/apiFunctions/adviceApi.openapi.yaml— trainsgetRandomAdvice
motdClient calls whichever one matches the mood field in the
webhook payload (see step 9). Neither demo API
publishes its own OpenAPI spec, so these were hand-written to cover just the
one endpoint each function needs — you don't need an API's own documentation
to train against it, just an accurate spec for the calls you want to make.
Generate a Poly model file from each spec (this uses AI to fill in descriptions):
npm run models:generateThis writes jokeApi.model.json / adviceApi.model.json next to their
specs — open one to see the generated function name, description, and
inferred return type.
Validate, then train them. Training is the step that actually creates the API Functions on the platform:
npm run models:validate
npm run models:trainAfter this, getRandomJoke and getRandomAdvice exist under the
foo.bar context on the platform, ready to be pulled into your local
SDK the next time you run npm run generate (step 7).
Deploying pushes the local .ts deployables (client functions, server
functions, webhooks) up to PolyAPI:
npm run deployThis runs poly sync, which scans the whole project for files exporting a
polyConfig (typed PolyClientFunction, PolyServerFunction, or
PolyWebhook) and creates or updates the matching resource on the platform.
After it deploys a file, it stamps a comment at the top like:
// Poly deployed @ 2026-07-03T10:00:00.000Z - foo.bar.motdServer - https://eu1.polyapi.io/... - a1b2c3d4Leave that comment in place — it's how the tool tracks what's already deployed.
Tip: preview what would change without deploying anything with
npx poly sync --dry-run.
Once deployed, pull those changes into your local SDK:
npm run generateThis regenerates the typed bindings under node_modules/.poly, so
motdClient, motdServer, and the two API Functions trained in step 6
(getRandomJoke, getRandomAdvice) all become available as typed poly.*
calls locally, e.g. poly.foo.bar.motdServer({ name: "Ada", mood: "funny" }).
Re-run npm run generate any time the platform-side catalog changes —
whenever you or a teammate add or change functions, webhooks, or other
constructs.
poly sync doesn't reliably wire two things onto a webhook: the trigger that
connects it to a server function, and the securityFunctions that validate
its payload. Both need a one-time manual step.
Triggers aren't created by poly sync, so run the trigger script once. It
reads POLY_INSTANCE_URL / POLY_API_KEY from .env, finds the deployed
webhook and server function named in
motdTrigger.config.json,
and creates (or updates) the trigger connecting them:
npm run setup:triggerYou should see output like:
Trigger 'motdTrigger' created successfully (id: ...).
Re-running the same command later will update the existing trigger instead of creating a duplicate — safe to re-run any time the config changes.
src/serverFunctions/validateMotdPayload.ts
is a normal server function — npm run deploy (step 7) deploys it like any
other. The tricky part is wiring it into
motdWebhook.ts's
securityFunctions, which references it by platform-assigned id:
securityFunctions: [
{
id: "1ee8f8ed-480b-46bb-bb7b-a31868522018",
message: "Invalid or missing field in event payload.",
},
],That id only exists after validateMotdPayload has been deployed
once — grab it from the // Poly deployed @ ... comment poly sync stamps
at the top of the file after deploying it, then paste it into the webhook's
securityFunctions and run npm run deploy again.
Once deployed, get this webhook's actual invoke URL from its details page in
the PolyAPI dashboard (Canopy PolyUI) — it includes an environment-specific
subdomain, so it won't match the tenant base URL you used for poly setup.
For our tenant/environment it looks like this:
curl -X POST 'https://94021a50.eu1.polyapi.io/apis/foo-motd/motd' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your-poly-api-key>' \
--data '{ "name": "Ada", "mood": "funny" }'Expected response — motdClient called the trained getRandomJoke API
Function, since mood is "funny" (the joke itself is random, so the exact
text will differ each call):
{ "statusCode": 200, "message": "Hi Ada! What did the beaver say to the tree? ... It's been nice gnawing you." }Send "mood": "wise" instead to hit the trained getRandomAdvice API
Function:
{ "statusCode": 200, "message": "Hi Ada! Mercy is the better part of justice." }That's the motdServer server function's actual return value — not the
webhook's own responsePayload. The trigger's waitForResponse setting (in
motdTrigger.config.json)
controls whether the call is synchronous or asynchronous from the caller's
point of view:
waitForResponse: true(used here) — synchronous: the caller waits formotdServerserver function to finish and gets its return value back as the response.waitForResponse: false— asynchronous: the caller gets the webhook's ownresponsePayloadback immediately, whilemotdServerfunction keeps running in the background.
Now send an invalid payload (missing name, or a mood other than
"funny"/"wise") to see validateMotdPayload reject it before the
trigger — and motdServer — ever run:
curl -X POST 'https://94021a50.eu1.polyapi.io/apis/foo-motd/motd' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your-poly-api-key>' \
--data '{ "name": "Ada", "mood": "dummy" }'{ "statusCode": 403, "message": "Invalid or missing field in event payload." }You now have a deployed webhook → trigger → server function → client function chain, validated by a security function and choosing between two trained API Functions — the skeleton we'll build on. Next steps typically look like:
- Train API Functions against a real 3rd-party API instead of demo ones, using its actual OpenAPI spec where one exists.
- Replace the webhook's payload schema with the real event contract, and
extend
validateMotdPayloadto match. - Add
varientries for config/secrets the functions need (e.g. API base URLs, credentials), andtabientries if you need to persist structured data. - Introduce global error handler to catch errors scoped by the tenat, environment, some specific function(s) etc.