10-minute quickstart
By the end of this walkthrough you will have: a user account,
a connected Google or Outlook calendar, an API key, a routine
task created via curl, a public HTTPS tunnel,
a signed webhook endpoint registered, and one
reminder.fired webhook posted to your laptop.
1. Register an account
Create a user via the normal sign-up flow at
/auth/register (or the existing TIMEMANAGER UI).
Sign in. For the rest of this guide we assume the session
cookie (or bearer token) is stored in the environment variable
MYKAL_TOKEN.
2. Connect a calendar
Start the OAuth flow for your provider. This returns an
auth_url you open in a browser; when consent
completes the callback redirects back to
/mykal/pages/integrations.html.
# Replace with "microsoft" for Outlook/Graph
curl -X POST "$MYKAL_BASE/api/v1/mykal/integrations/google/oauth/start" \
-H "Authorization: Bearer $MYKAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"return_url":"pages/integrations.html"}'
3. Create an API key
4. Create your first task
One HTTP call creates a RoutineTask:
curl -X POST "$MYKAL_BASE/api/v1/mykal/tasks" \
-H "Authorization: Bearer $MYKAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Write quickstart blog post",
"raw_input": "Write quickstart blog post ~2h, prio 2",
"priority": 2,
"duration_min": 120,
"reminder_channels": ["email","inapp","webhook"]
}'
import os, httpx
base = os.environ["MYKAL_BASE"]
token = os.environ["MYKAL_TOKEN"]
r = httpx.post(
f"{base}/api/v1/mykal/tasks",
headers={"Authorization": f"Bearer {token}"},
json={
"title": "Write quickstart blog post",
"raw_input": "Write quickstart blog post ~2h, prio 2",
"priority": 2,
"duration_min": 120,
"reminder_channels": ["email", "inapp", "webhook"],
},
timeout=15.0,
)
r.raise_for_status()
print(r.json()["id"])
// Node 18+: native fetch
const base = process.env.MYKAL_BASE;
const token = process.env.MYKAL_TOKEN;
const res = await fetch(`${base}/api/v1/mykal/tasks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Write quickstart blog post",
raw_input: "Write quickstart blog post ~2h, prio 2",
priority: 2,
duration_min: 120,
reminder_channels: ["email", "inapp", "webhook"],
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const task = await res.json();
console.log(task.id);
5. Open a public tunnel with ngrok
mykal only posts webhooks to public HTTPS URLs. Spin up a local listener (Python / Node) on port 4000 and tunnel it.
ngrok http 4000
# Copy the https://<hash>.ngrok-free.app URL
# minimal receiver on port 4000 (no signature check yet -- see webhooks.html)
from fastapi import FastAPI, Request
import uvicorn
app = FastAPI()
@app.post("/hook")
async def hook(req: Request):
print("headers:", dict(req.headers))
print("body:", (await req.body()).decode("utf-8"))
return {"ok": True}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=4000)
6. Register the webhook
POST your tunnel URL to create an endpoint. The response body
contains the plaintext secret exactly
once. Store it — you cannot fetch it again, only rotate.
curl -X POST "$MYKAL_BASE/api/v1/mykal/webhooks" \
-H "Authorization: Bearer $MYKAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "laptop-dev",
"url": "https://<hash>.ngrok-free.app/hook",
"enabled": true,
"max_attempts": 4,
"fallback_to_email": true
}'
# response includes "id": "...", "secret": "...", "secret_preview": "abcd...wxyz"
7. Fire a test + wait for a real reminder
First verify the plumbing with a synthetic ping (does NOT consume the retry budget):
curl -X POST "$MYKAL_BASE/api/v1/mykal/webhooks/$ENDPOINT_ID/test" \
-H "Authorization: Bearer $MYKAL_TOKEN"
# returns {"ok":true,"status_code":200,"latency_ms":...,"signature":"t=...,v1=..."}
Now create a reminder (or let a scheduled task's reminder fire
naturally) and watch your receiver log the signed POST with
headers X-Mykal-Signature,
X-Mykal-Timestamp,
X-Mykal-Delivery-Id,
X-Mykal-Event: reminder.fired. Next step:
verify the signature.