mcpField
E
extension-http-endpoints

extension-http-endpoints

github.com/caffeinelabsDevelopment23.8k installsUpdated today
Repository

About extension-http-endpoints

A skill for developers adding inbound HTTP endpoints to Caffeine AI Motoko backends with `caffeineai-http-endpoints`. It focuses on routing query and update requests correctly, gateway URL requirements, and authentication considerations for external callers.

Key features

  1. 01Inbound HTTP handling for Motoko backends
  2. 02Separate query and update paths
  3. 03Prebuilt HTTP modules and mixin
  4. 04Webhook URL and GET host guidance
  5. 05Signature and secret verification guidance

Use cases

  1. 01Receive external webhooks in a canister
  2. 02Serve GET routes such as health checks
  3. 03Implement POST routes that change state

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/caffeinelabs/skills --skill extension-http-endpoints

After installing, type/extension-http-endpointsin Claude Code to confirm it loaded.

Details

From the project README

HTTP Endpoints (inbound)

Inbound HTTP for Caffeine AI Motoko backends via mo:caffeineai-http-endpoints.

When to use which skill

NeedSkill
External client (curl, webhook, bot) hits the canister over HTTPSthis skill
Canister calls an external HTTPS APIextension-http-outcalls
Typed OpenAI / Google / X / Stripe clientthe 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):

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
};
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.

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:

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.

# 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

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.

Comments

Markdown supported