extension-http-endpoints — Claude Code skill: setup and features
Configures inbound HTTPS endpoints for Motoko canisters on the Internet Computer. Useful for Stripe webhooks, Telegram bot callbacks, and plain health checks.
npx skills add caffeinelabs/skills --skill extension-http-endpoints -a claude-codeWhat extension-http-endpoints does
This skill teaches the agent how to expose inbound HTTPS endpoints on Internet Computer Motoko canisters. It relies on mo:caffeineai-http-endpoints and its MixinHttpEndpoints mixin. The IC HTTP Gateway sends inbound traffic to http_request as a query call first. When a route modifies state, the handler returns Http.upgrade(), which prompts the gateway to reissue the request as an update call to http_request_update.
Setup requires the caffeineai-http-endpoints and caffeineai-authorization packages from MOPS. The mixin integrates directly into src/backend/main.mo. The onQuery function must stay query-safe: no await statements and no state mutations. Webhook callers never provide an IC caller principal. Because of that, you must verify HMAC signatures and secrets manually via Http.header and raw req.body.
Uncertified GET requests on regular gateway domains trigger an immediate 503 gateway error. Reading plain text or /health routes requires targeting the .raw.icp0.io subdomain. This skill does not handle outgoing requests from canisters to the internet.
What it helps with
- receive webhooks from Stripe, GitHub, or Slack in a Motoko canister
- handle incoming HTTPS events from Telegram bots
- serve plaintext health checks without response certification
- upgrade mutating query requests to update calls via
Http.upgrade()
Example prompts for the agent
- Add a
/webhookPOST route tosrc/backend/main.mousingmo:caffeineai-http-endpoints - Set up an uncertified GET
/healthendpoint for my Motoko backend - Implement signature verification for incoming webhooks in
onHttpUpdate
How to install extension-http-endpoints
1Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-http-endpoints -a claude-code2Manually: put the files in place
~/.claude/skills/extension-http-endpoints/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/extension-http-endpoints/
1Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-http-endpoints -a codex2Manually: put the files in place
.agents/skills/extension-http-endpoints/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-http-endpoints -a cursor2Manually: put the files in place
.cursor/skills/extension-http-endpoints/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install caffeinelabs/skills extension-http-endpoints2Manually: put the files in place
.github/skills/extension-http-endpoints/SKILL.md1Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-http-endpoints -a gemini-cli2Platform CLI
gemini skills install https://github.com/caffeinelabs/skills --path skills/extension-http-endpointsManually: put the files in place
.devin/skills/extension-http-endpoints/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add caffeinelabs/skills --skill extension-http-endpoints -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about extension-http-endpoints
How do I install extension-http-endpoints in Claude Code?
Run npx skills add caffeinelabs/skills --skill extension-http-endpoints -a claude-code in your terminal.
How does this skill differ from extension-http-outcalls?
This skill sets up server endpoints to receive incoming traffic from external clients. Outgoing HTTPS requests from a canister to external APIs require extension-http-outcalls instead.
Why do uncertified GET requests require the .raw.icp0.io subdomain?
The standard IC gateway requires certified responses for query requests and returns a 503 error when certification is missing. The .raw.icp0.io address bypasses gateway response verification for plain endpoints like /health.
Source text
SKILL.md · Apache-2.0
---
name: extension-http-endpoints
description: >-
Serve inbound HTTP from a Motoko canister (webhooks, curl POST/GET, bot
callbacks, ingest/health APIs) via the IC HTTP Gateway methods
`http_request` / `http_request_update`. Load whenever the user or spec needs
a canister URL that external HTTP clients can hit — Stripe/GitHub/Slack
webhooks, Telegram bots, health checks, or any plain REST-ish endpoint.
NOT for calling external APIs from the canister (use
`extension-http-outcalls`); NOT for certified static asset serving.
version: 0.2.1
compatibility:
mops:
caffeineai-http-endpoints: "~0.1.0"
caffeineai-authorization: "~1.0.1"
caffeineai-subscription: [none]
---
# HTTP Endpoints (inbound)
Inbound HTTP for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral) Motoko backends via `mo:caffeineai-http-endpoints`.
## When to use which skill
| Need | Skill |
| --- | --- |
| External client (`curl`, webhook, bot) hits the canister over HTTPS | **this skill** |
| Canister calls an external HTTPS API | [`extension-http-outcalls`](../extension-http-outcalls/SKILL.md) |
| Typed OpenAI / Google / X / Stripe client | the matching domain skill (those wrap **outbound** calls) |
Do not implement inbound handlers with the management-canister `http_request` / `Call.httpRequest` outcall API — that is the opposite direction.
## How inbound HTTP works
1. Client hits the **backend** Motoko canister over HTTPS (not `*.caffeine.xyz`, custom domain, or frontend/asset canister).
2. The HTTP Gateway calls `http_request` (**query**).
3. Mutating routes return `Http.upgrade()` so the gateway re-issues as `http_request_update` (**update**).
**URLs**
- **Webhooks / POST:** `https://<backend-canister-id>.icp0.io/...` (or `.ic0.app`)
- **Uncertified GET (health / plaintext):** `https://<backend-canister-id>.raw.icp0.io/...` — non-raw gateways expect certified query bodies (out of scope)
# Backend
Use the prefabricated modules (do not modify them):
```mo:caffeineai-http-endpoints/Http
module {
public type Request = { method : Text; url : Text; headers : [HeaderField]; body : Blob; certificate_version : ?Nat16 };
public type Response = { status_code : Nat16; headers : [HeaderField]; body : Blob; streaming_strategy : ?StreamingStrategy; upgrade : ?Bool };
public func path(url : Text) : Text;
public func header(req : Request, name : Text) : ?Text; // case-insensitive
public func text(status : Nat16, body : Text) : Response;
public func upgrade() : Response; // query path asks gateway to call http_request_update
public func notFound() : Response;
public func methodNotAllowed() : Response;
public func bodyText(req : Request) : ?Text; // null if body is not UTF-8; HMAC must use req.body
};
```
```mo:caffeineai-http-endpoints/MixinHttpEndpoints
mixin (
onQuery : Http.Request -> Http.Response,
onUpdate : Http.Request -> async Http.Response,
) {
public query func http_request(req : Http.Request) : async Http.Response;
public func http_request_update(req : Http.Request) : async Http.Response;
};
```
Add `mops add caffeineai-http-endpoints`. Wire handlers on the **existing** actor in `main.mo` — keep `MixinAuthorization`, do not replace the actor or redeclare `http_request*`.
`onQuery` must stay query-safe: no awaits, no state writes. Mutating work belongs in `onUpdate` after `Http.upgrade()` from the query path.
```motoko filepath=src/backend/main.mo
import Http "mo:caffeineai-http-endpoints/Http";
import MixinHttpEndpoints "mo:caffeineai-http-endpoints/MixinHttpEndpoints";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
actor {
let accessControlState : AccessControl.AccessControlState;
include MixinAuthorization(accessControlState, null);
func onHttpQuery(req : Http.Request) : Http.Response {
let path = Http.path(req.url);
if (req.method == "POST" and path == "/webhook") {
return Http.upgrade();
};
if (req.method == "GET" and (path == "/" or path == "/health")) {
return Http.text(200, "ok");
};
Http.notFound();
};
func onHttpUpdate(req : Http.Request) : async Http.Response {
let path = Http.path(req.url);
if (req.method == "POST" and path == "/webhook") {
// Signature checks: Http.header(req, "stripe-signature"), HMAC over req.body
ignore Http.bodyText(req);
return Http.text(200, "received");
};
Http.notFound();
};
include MixinHttpEndpoints(onHttpQuery, onHttpUpdate);
};
```
The migration chain head:
```motoko filepath=src/backend/migrations/00000000_000000.mo
import AccessControl "mo:caffeineai-authorization/access-control";
module {
type NewActor = {
accessControlState : AccessControl.AccessControlState;
};
public func migration(_old : {}) : NewActor {
{ accessControlState = AccessControl.initState() };
};
};
```
## Rules of thumb
- **Mutations only in `onUpdate`.** Query returns `Http.upgrade()` for POST/PUT/PATCH/DELETE routes that write.
- **Backend canister id only** for webhook URLs — project/frontend origins never reach Motoko `http_request`.
- **Uncertified GET → `.raw.icp0.io`.** Do not invent response certificates.
- **Prefer Candid + React** for app UI; use inbound HTTP only for external HTTP clients.
- **Auth:** no IC caller principal from webhook senders — verify signatures/secrets yourself (`Http.header`, raw `req.body`).
- **Large JSON:** keep payloads small/schema-simple, or store the blob and process elsewhere.
## How to `curl`
Uncertified GET on a non-raw host is a hard gateway failure (`503` / `backend_response_verification` / `Certification values not found`), not a 404 from your handler. POSTs that return `Http.upgrade()` skip that check.
```bash
# Reads — must be raw
curl https://<backend-canister-id>.raw.icp0.io/health
# Writes — non-raw is fine (update path)
curl -X POST https://<backend-canister-id>.icp0.io/webhook \
-H 'content-type: application/json' \
-H 'x-webhook-secret: …' \
-d '{"event":"…"}'
```
`?canisterId=<id>` on `https://icp0.io/...` is an alternate host form. Prefer the canister subdomain. An IP/`localhost` GET with only `?canisterId=` can return an empty `204` instead of your body — treat that as the wrong URL shape, not an empty handler.
## Related
- [`extension-http-outcalls`](../extension-http-outcalls/SKILL.md) — canister → internet (module helpers, no inbound mixin)
- [`extension-stripe`](../extension-stripe/SKILL.md) — checkout via outcalls; inbound Stripe webhooks still use this skill
- [icskills#337](https://github.com/dfinity/icskills/issues/337) — upstream skill-gap discussion
## Local PocketIC (limitations)
PocketIC is useful for proving the upgrade dance, not as a drop-in `curl` replica.
- The `pocket-ic` binary is a **control server**. It does not listen for `curl` until a client (`@dfinity/pic`) creates an instance (include an NNS subnet) and calls `makeLive()`, which returns a **port**.
- There is no `mops` one-liner that prints a curl URL. The gateway dies with that Node/pic process.
- Once live, the same rules apply:
- `http://<canister-id>.raw.localhost:<port>/health` — uncertified GET
- `http://127.0.0.1:<port>/notes?canisterId=<id>` — POST / upgrade (worked in a live gateway test)
- `http://<canister-id>.localhost:<port>/health` (no `.raw`) — `503` certification failure
- Do not document PocketIC as the way users hit production webhooks. Production URLs are the `*.icp0.io` / `*.raw.icp0.io` forms above.
© caffeinelabs · Apache-2.0 · caffeinelabs/skills