tokolaku
Official Python SDK for the Tokolaku Engine API — AI bot replies, omnichannel messaging (WhatsApp/Instagram/Messenger), and webhook verification.
Install
pip install tokolaku
Requirements
- Python >= 3.10
Quickstart
Bahasa Indonesia ringkas: buat instance Tokolaku dengan API key, lalu panggil bot_reply untuk balasan AI atau messages.send untuk kirim pesan lewat channel resmi (WhatsApp/Instagram/Messenger) yang sudah terhubung.
import os
from tokolaku import Tokolaku
tokolaku = Tokolaku(os.environ["TOKOLAKU_API_KEY"])
# or with options: Tokolaku(api_key=..., base_url=..., timeout=30.0, max_retries=2)
# 1. AI bot reply for a single customer message
reply = tokolaku.bot_reply(
"Halo, apakah produk ini ready stock?",
session_id="wa:628123456789", # keeps multi-turn context
)
print(reply.reply, reply.parts)
# 2. Send a text message through a connected channel
sent = tokolaku.messages.send(
to="628123456789",
text="Terima kasih sudah menghubungi kami!",
channel_id="ch_abc123",
)
print(sent.id, sent.status)
messages.send also accepts a business-initiated template message — pass template instead of text (exactly one of the two, never both):
tokolaku.messages.send(
to="628123456789",
template={"name": "order_update", "language": "id", "category": "utility"},
channel_id="ch_abc123",
)
Error handling
Every failed request raises an instance of TokolakuAPIError (or one of its subclasses). .status is None when the request never got an HTTP response (network error, timeout); .code is the backend's machine-readable error code when available.
| Class | HTTP status | When it's raised |
|---|---|---|
TokolakuValidationError |
400, 422 | Invalid request params — also raised client-side before any network call (e.g. messages.send with both text and template, or neither) |
TokolakuAuthenticationError |
401 | Missing or invalid API key |
TokolakuInsufficientBalanceError |
402 | Tenant balance too low to cover the charge |
TokolakuPermissionError |
403 | API key lacks permission for this action |
TokolakuRateLimitError |
429 | Rate limit exceeded |
TokolakuAPIError |
any other status, or None |
Base class — also covers network errors, timeouts, and malformed responses not mapped above |
TokolakuWebhookSignatureError |
— | Webhook signature missing or invalid (does not extend TokolakuAPIError) |
import os
from tokolaku import (
Tokolaku,
TokolakuAPIError,
TokolakuInsufficientBalanceError,
TokolakuRateLimitError,
)
tokolaku = Tokolaku(os.environ["TOKOLAKU_API_KEY"])
try:
tokolaku.bot_reply("Halo")
except TokolakuInsufficientBalanceError:
... # top up balance, notify the tenant
except TokolakuRateLimitError:
... # back off and retry later
except TokolakuAPIError as err:
print(err.status, err.code, err)
Retry policy
The SDK retries automatically (max_retries, default 2) using exponential backoff with full jitter (base 250ms, capped at 1s; a Retry-After response header wins when present). The policy is money-aware: it only retries when a retry cannot cause a duplicate side effect.
| Condition | bot_reply |
messages.send |
|---|---|---|
429 Too Many Requests |
Retried | Retried |
| Network error (request never got a response) | Retried | Retried |
5xx server error |
Retried | Not retried |
Timeout (code: "timeout") |
Not retried | Not retried |
2xx with malformed JSON body (code: "invalid_response") |
Not retried | Not retried |
2xx where the body stream fails mid-read (code: "response_read_error") |
Not retried | Not retried |
bot_replyhas no side effect if it fails, so it retries on429, any5xx, and network errors.messages.sendis NOT retried on timeout/5xx because the message may already have been sent and charged even though the client never saw a successful response, and the API does not yet expose an idempotency key. It only retries on429and network errors — a network retry only applies when the request itself failed before any response headers arrived (no response headers were ever received, so the send most likely never reached the server). Once response headers have arrived, a failure reading the body is aresponse_read_error, not a network error, and is never retried.- A timeout (
code: "timeout") is never retried on either endpoint, since it's ambiguous whether the server received/processed the request. - A
2xxresponse with a body that fails to parse as JSON (code: "invalid_response") carries the actual 2xx status the server returned (usually200) and is never retried on either endpoint — the request already reached the server and had its side effect (reply generated / message sent and charged); retrying would risk a double-send or burning AI quota for nothing. - A
2xxresponse whose body stream errors mid-read (code: "response_read_error", e.g. the connection resets after headers arrive) is likewise never retried, for the same reason: response headers arriving means the request already reached the server and may have had its side effect, even though the body was never fully read.
Webhooks
Verify the x-tokolaku-signature header (sha256=<hex>, HMAC-SHA256 of the raw request body) before trusting a webhook payload. Always use the raw, unmodified request body — a re-serialized JSON string will not match the signature.
from tokolaku.webhooks import verify_webhook_signature, construct_event, TokolakuWebhookSignatureError
Flask
import os
from flask import Flask, request, jsonify
from tokolaku.webhooks import construct_event, TokolakuWebhookSignatureError
app = Flask(__name__)
@app.post("/webhooks/tokolaku")
def tokolaku_webhook():
raw_body = request.get_data() # raw bytes — do NOT use request.get_json() here
try:
result = construct_event(
raw_body,
request.headers.get("x-tokolaku-signature"),
os.environ["TOKOLAKU_WEBHOOK_SECRET"],
)
except TokolakuWebhookSignatureError:
return jsonify({"error": "invalid signature"}), 401
event = result["event"]
# ... handle event
return jsonify({"received": True})
FastAPI
import os
from fastapi import FastAPI, Request, HTTPException
from tokolaku.webhooks import construct_event, TokolakuWebhookSignatureError
app = FastAPI()
@app.post("/webhooks/tokolaku")
async def tokolaku_webhook(request: Request):
raw_body = await request.body() # raw bytes — do NOT depend on request.json() here
try:
result = construct_event(
raw_body,
request.headers.get("x-tokolaku-signature"),
os.environ["TOKOLAKU_WEBHOOK_SECRET"],
)
except TokolakuWebhookSignatureError:
raise HTTPException(status_code=401, detail="invalid signature")
event = result["event"]
# ... handle event
return {"received": True}
License
MIT
Docs
Full API reference: https://tokolaku.id/api-docs
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tokolaku-1.1.0.tar.gz.
File metadata
- Download URL: tokolaku-1.1.0.tar.gz
- Upload date:
- Size: 14.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
70c922edbe2add832092cbaef93a0de5dd223d90e95339c74a17cd24c7b2a32b
|
|
| MD5 |
f264db56c88df27fdd69964cda9833e6
|
|
| BLAKE2b-256 |
d059d945a175ccd20333f45860f4a6d82f8650c94e63fd966e6a70ab15685cfa
|
File details
Details for the file tokolaku-1.1.0-py3-none-any.whl.
File metadata
- Download URL: tokolaku-1.1.0-py3-none-any.whl
- Upload date:
- Size: 11.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb1a296fba581d0db4da6966420c2c8379ce185b8b143b9e6a5e527416f6df47
|
|
| MD5 |
4962173092e386e30a4b22f03aa9a054
|
|
| BLAKE2b-256 |
0b58421cf4768ac6c14bbb4818789809baba034a4184df4bef1b20048f96fc58
|