hooks

three extension points on the Api — before, after, onError — wrap every request, and the error hook drives the retry loop.

the request lifecycle

every call goes through before hooks, the request itself, then after hooks. if the request throws, onError hooks decide whether to retry. plugins hang off these same three points.

api.ts
import { createApi } from "@yaebal/core";

const api = createApi(process.env.BOT_TOKEN!)
  .before((m, p) => p)
  .after((m, r) => r)
  .onError((m, e) => undefined);
// each registrar returns the Api, so registration chains

before

a before hook receives the method name and params and may return replacement params. returning undefined leaves them as-is. hooks run in registration order, each seeing the previous one's output.

before.ts
// before — inspect or rewrite params; return new params to replace them
bot.api.before((method, params) => {
  if (method === "sendMessage") {
    return { parse_mode: "HTML", ...params };
  }
  // return undefined → params unchanged
});

after

an after hook receives the method name and the successful result, and may return a replacement value. returning undefined leaves the result unchanged.

after.ts
// after — inspect or rewrite the result; return a value to replace it
bot.api.after((method, result) => {
  console.log(method, "ok");
  // return undefined → result unchanged
});

onError and the retry loop

when a request throws, each onError hook is called with the method, the error, and the 1-based attempt that just failed. the first hook to return { retry: true } triggers a re-run; an optional delayMs waits before retrying. if no hook requests a retry, the error is rethrown.

onError.ts
// onError — runs when a request throws; ask for a retry by returning an action
bot.api.onError((method, error, attempt) => {
  if (error instanceof TelegramError && error.code === 429 && attempt < 5) {
    return { retry: true, delayMs: 1000 };
  }
  // return undefined (or { retry: false }) → the error is rethrown
});
bounded by the hooks themselves. the retry loop has no built-in cap — it loops only while a hook keeps asking for a retry. gate on attempt (or use a plugin like auto-retry) so it terminates.

errors

when Telegram replies with ok: false, the call throws a TelegramError carrying the method and numeric code.

error.ts
import { TelegramError } from "@yaebal/core";

try {
  await bot.api.sendMessage({ chat_id: 1, text: "" });
} catch (e) {
  if (e instanceof TelegramError) {
    e.method;      // "sendMessage"
    e.code;        // error_code from Telegram
    e.message;     // "[sendMessage] 400: message text is empty"
  }
}

encodeRequest: JSON vs multipart

the body encoding is chosen per request. plain params (and url/fileId media) serialize to JSON; the moment a path/buffer upload is present the request becomes multipart with attach:// references. see media for the full encoding rules.

fileUrl

fileUrl builds the download URL for a file_path returned by getFile.

fileUrl.ts
const file = await bot.api.call("getFile", { file_id: id });
const url = bot.api.fileUrl(file.file_path);
// https://api.telegram.org/file/bot<token>/<file_path>
//                                ^^^^^^^ contains the bot token — never log it
contains the token. the file download URL embeds the bot token. never log it, and never hand it to an untrusted client — anyone with it controls the bot.