Reminder Service – API Reference
REST API for scheduling and sending automated Email / WhatsApp reminders. All endpoints return JSON.
Overview
The Reminder Service stores everything as flat JSON files under data/, namespaced per domain (e.g. reminders_reminder.darmist.com.json, users_local.json). Read responses are cached in Redis (TTL 3600s) for speed, and a per-minute cron dispatches due reminders.
- Every reminder is private – only its owner can list, view, update, delete, or send it.
- At least one contact (email and/or WhatsApp) is required when creating or updating a reminder.
- WhatsApp numbers are normalized to Bangladesh format +8801XXXXXXXXX.
- Cache is auto-invalidated after create / update / delete / send – no manual cache reset exists.
- See Storage & Data Strategy below for the full file layout, domain resolution, and legacy migration.
Base URL
# Root installation (e.g. http://localhost/) https://your-host/api # Sub-directory deployment (e.g. behind /reminder/) https://your-host/reminder/api
Depending on whether the application is hosted at the web root or inside a sub-directory, endpoints are available at /api or /reminder/api. All endpoints below are relative to your base path.
| Deployment | Base URL |
|---|---|
| Web Root | /api |
| Subfolder | /reminder/api |
| Content Type | application/json |
| Authentication | Bearer Token (JWT) in Authorization header |
Authentication
Almost every endpoint requires a JWT obtained from POST /api/register or POST /api/login. Send it as:
Authorization: Bearer <token>
Tokens expire after 24 hours. When expired, re-login to obtain a fresh token.
| Endpoint | Auth Required |
|---|---|
| POST /api/register | No |
| POST /api/login | No |
| GET /api/me | Yes |
| All /api/reminders* | Yes |
| All /api/users* | Yes Admin only |
Response Envelope
Every response is wrapped in a consistent envelope.
// Success { "status": "success", "message": "Reminders retrieved successfully", "cached": true, // true when served from Redis "data": { ... } // omitted on delete success } // Error { "status": "error", "message": "Reminder not found.", "errors": { ... } // optional validation details }
Errors & Status Codes
| Code | Meaning | Typical Cases |
|---|---|---|
| 200 | Success | Data returned / operation completed |
| 201 | Created | User or reminder created |
| 400 | Bad Request | Missing fields, invalid email / WhatsApp number |
| 401 | Unauthorized | Missing / invalid Bearer token, bad credentials |
| 403 | Forbidden | Accessing another user's private reminder |
| 404 | Not Found | Reminder does not exist, unknown route |
| 409 | Conflict | Registering with an email or username that already exists |
| 422 | Unprocessable | Invalid username (bad characters / too short / reserved name) |
| 500 | Server Error | Storage write failure, downstream provider error |
Health Check & System Status
Returns current service health, PHP version, and Redis connection status. Accepts either ?format=json or Accept: application/json.
Headers / Query
| Parameter | Value | Description |
|---|---|---|
| Query | format=json | Requests JSON health status |
| Header | Accept: application/json | Alternative to format query parameter |
curl -H "Accept: application/json" https://your-host/api/
# Or via query parameter:
curl https://your-host/?format=json
{
"status": "success",
"message": "Reminder Service API is running",
"cached": false,
"data": {
"php_version": "8.2.12",
"redis_status": "connected"
}
}
Register a User
Creates a new standard user account and returns a JWT token. Self-registration always creates user role - client-supplied roles are ignored. Admin accounts are provisioned by an administrator.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | User display name |
username | string | Yes | Unique login handle & profile file key; 3-30 chars of [a-z0-9._-], lowercase. Reserved: users, reminders |
email | string | Yes | Unique email address |
password | string | Yes | Plain password (hashed server-side) |
curl -X POST https://your-host/reminder/api/register \
-H "Content-Type: application/json" \
-d '{"name":"John Client","username":"john","email":"user@example.com","password":"secret123"}'
{
"status": "success",
"message": "User registered successfully (JSON storage)",
"cached": false,
"data": {
"user": { "id": 4, "username": "john", "name": "John Client", "email": "user@example.com", "role": "user", "status": "active" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Login
Authenticates an existing user and returns a JWT token. The identifier may be the user's email or username (case-insensitive).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | Yes | Registered email address or username. Legacy clients may send email / username keys instead |
password | string | Yes | Account password |
curl -X POST https://your-host/reminder/api/login \
-H "Content-Type: application/json" \
-d '{"identifier":"john","password":"secret123"}'
{
"status": "success",
"message": "Login successful",
"cached": false,
"data": {
"user": { "id": 1, "username": "system", "name": "System Admin", "email": "admin@example.com", "role": "admin" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Get Current User
Returns the profile of the authenticated user.
Headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> |
curl https://your-host/reminder/api/me \ -H "Authorization: Bearer <token>"
{
"status": "success",
"message": "Current user profile",
"cached": false,
"data": {
"user": { "id": 1, "username": "system", "name": "System Admin", "email": "admin@example.com", "role": "admin" }
}
}
List Reminders
Lists the authenticated user's own reminders (all private), sorted by scheduled datetime descending (latest first), tie-broken by newest reminder ID. Served from Redis cache when available.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
nocache | string | No | 1 bypasses Redis and reads fresh from storage |
curl https://your-host/reminder/api/reminders \ -H "Authorization: Bearer <token>"
{
"status": "success",
"cached": true,
"data": [
{
"id": 11,
"user_id": 4,
"title": "Software License Expiry",
"description": "Renew before the deadline",
"datetime": "2026-10-01 10:00:00",
"email": "client@company.com",
"whatsapp_number": "+8801712345678",
"is_private": true,
"status": "pending",
"sent_at": null,
"last_error": null,
"owner_name": "John Client",
"owner_email": "user@example.com"
}
]
}
Create Reminder
Creates a new private reminder owned by the caller. Reminder stays pending until the cron dispatches it at the scheduled time, or you use Send Now.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Reminder title |
description | string | No | Extra context. Supports lightweight markup: *bold*, _italic_, ~strikethrough~, __underline__ and lines beginning with - or 1. for lists. |
datetime | string | Yes | Y-m-d H:i (e.g. 2026-10-01 10:00). Seconds appended automatically. |
email | string | Yes* | Recipient email(s). Accepts a comma-separated list of up to 10 addresses. At least one contact required. |
whatsapp_number | string | Yes* | Recipient WhatsApp number(s). Accepts a comma-separated list of up to 10 numbers, each as 10 digits, 0-prefixed local, or +88017XXXXXXXX. Each entry is normalized to +8801XXXXXXXXX. |
repeat_mode | string | No | none | hourly | daily | weekly | monthly | every_6_months | custom. Default none (one-shot). |
repeat_interval | int | No | Step count (≥ 1). Only meaningful with repeat_mode=custom. |
repeat_unit | string | No | hour | day | week | month | year (custom mode only). |
repeat_end_type | string | No | date | count or omit/null for "never". |
repeat_end_value | string/int | No | Y-m-d H:i when repeat_end_type=date (must be in the future), or an int ≥ 1 when repeat_end_type=count. |
* At least one of email / whatsapp_number is required.
last_error lists every entry that failed. Exceeding 10, or supplying one malformed entry, returns 400 with the offending values named in errors[].
curl -X POST https://your-host/reminder/api/reminders \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"title": "Domain Renewal",
"description": "Renew domain hosting",
"datetime": "2026-10-01 10:00",
"email": "client@company.com",
"whatsapp_number": "1712345678"
}'
{
"status": "success",
"message": "Reminder created successfully (JSON storage)",
"cached": false,
"data": {
"id": 12,
"user_id": 4,
"title": "Domain Renewal",
"description": "Renew domain hosting",
"datetime": "2026-10-01 10:00:00",
"email": "client@company.com",
"whatsapp_number": "+8801712345678",
"whatsapp_message": "",
"is_private": true,
"status": "pending",
"sent_at": null,
"created_at": "2026-09-20 10:00:00",
"updated_at": "2026-09-20 10:00:00"
}
}
Get Reminder Detail
Returns one reminder if the authenticated user is its owner.
curl https://your-host/reminder/api/reminders/12 \ -H "Authorization: Bearer <token>"
{
"status": "success",
"message": "Reminder detail retrieved successfully",
"cached": false,
"data": {
"id": 12,
"user_id": 4,
"title": "Domain Renewal",
"description": "Renew domain hosting",
"datetime": "2026-10-01 10:00:00",
"email": "client@company.com",
"whatsapp_number": "+8801712345678",
"whatsapp_message": "",
"is_private": true,
"status": "pending",
"sent_at": null,
"created_at": "2026-09-20 10:00:00",
"updated_at": "2026-09-20 10:00:00"
}
}
Update Reminder
Partially updates any supplied field. Only the owner may update. Changing the datetime of an already sent or failed reminder re-queues it back to pending.
Request Body (all optional)
| Field | Type | Description |
|---|---|---|
title | string | New title |
description | string | New description. Supports the same lightweight markup as on create (*bold*, _italic_, ~strikethrough~, __underline__, lists). |
datetime | string | New schedule (Y-m-d H:i) |
email | string | New recipient email(s) — comma-separated, up to 10 |
whatsapp_number | string | New WhatsApp number(s) — comma-separated, up to 10 |
status | string | pending | sent | failed | completed | cancelled |
repeat_mode | string | Same values as create; switching to a preset normalizes repeat_interval/repeat_unit for it. |
repeat_interval | int | Step count (custom mode). |
repeat_unit | string | hour | day | week | month | year (custom mode). |
repeat_end_type | string | date | count or null/omitted for "never". |
repeat_end_value | string/int | End date (Y-m-d H:i) or count ≥ 1. |
Changing datetime or the schedule definition (repeat_mode/repeat_interval/repeat_unit) resets occurrences_sent to 0 and re-anchors the occurrence chain on the new datetime.
End date rules: the past-end-date error ("must be in the future") applies on create and whenever the supplied end value actually changes; an existing stale end date no longer blocks unrelated edits. Moving a recurring reminder's datetime to at/past its old end date drops that dead end cap so the rescheduled chain isn't instantly re-completed.
curl -X PUT https://your-host/reminder/api/reminders/12 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title":"Domain Renewal (Paid)","datetime":"2026-10-05 09:30"}'
{
"status": "success",
"message": "Reminder updated successfully",
"cached": false,
"data": {
"id": 12,
"user_id": 4,
"title": "Domain Renewal (Paid)",
"description": "Renew domain hosting",
"datetime": "2026-10-05 09:30:00",
"email": "client@company.com",
"whatsapp_number": "+8801712345678",
"whatsapp_message": "",
"is_private": true,
"status": "pending",
"sent_at": null,
"created_at": "2026-09-20 10:00:00",
"updated_at": "2026-09-20 10:15:00"
}
}
Delete Reminder
Permanently deletes a reminder. Only the owner may delete.
curl -X DELETE https://your-host/reminder/api/reminders/12 \ -H "Authorization: Bearer <token>"
{
"status": "success",
"message": "Reminder deleted successfully.",
"cached": false
}
Delete Reminders (Bulk)
Deletes several reminders in one request, in a single atomic operation. Only the owner may delete, and ids that do not exist are reported back in not_found rather than failing the whole call. Send the selection as one request — firing a separate DELETE /api/reminders/{id} per id in parallel is not safe, because each request rewrites the whole store and they overwrite one another.
curl -X DELETE https://your-host/reminder/api/reminders \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "ids": [12, 13, 14] }'
{
"status": "success",
"message": "3 reminder(s) deleted successfully.",
"data": {
"deleted": 3,
"requested": 3,
"not_found": [],
"denied": false
}
}
Send Notification Now
Immediately dispatches the WhatsApp and/or Email notification for a reminder, bypassing the scheduled cron. One-shot reminders become sent (or failed if both channels fail). Recurring reminders consume the current occurrence, advance datetime to the next slot and stay pending — exactly as the cron would at the scheduled time.
curl -X POST https://your-host/reminder/api/reminders/11/send \ -H "Authorization: Bearer <token>"
{
"status": "success",
"message": "Notification dispatch completed",
"cached": false,
"data": {
"reminder": { "id": 11, "status": "sent", "sent_at": "2026-09-20 10:05:12", /* ... */ },
"whatsapp": {
"success": true, "skipped": false,
"attempted": 2, "sent": 2, "failed": 0,
"error": null,
"recipients": [
{ "to": "+8801712345678", "success": true, "error": null },
{ "to": "+8801812345678", "success": true, "error": null }
]
},
"email": {
"success": true, "skipped": false,
"attempted": 1, "sent": 1, "failed": 0,
"error": null,
"recipients": [
{ "to": "client@company.com", "success": true, "error": null }
]
}
}
}
List Users
Returns every registered user account (admin only). Password hashes are never included.
Headers
| Header | Value |
|---|---|
| Authorization | Bearer <token> (admin) |
curl https://your-host/reminder/api/users \ -H "Authorization: Bearer <admin_token>"
{
"status": "success",
"message": "Users retrieved successfully",
"cached": false,
"data": [
{ "id": 1, "username": "system", "name": "System Admin", "email": "admin@example.com", "role": "admin", "status": "active", "created_at": "2026-09-04 11:53:49" },
{ "id": 2, "username": "john", "name": "John Client", "email": "user@example.com", "role": "user", "status": "active", "created_at": "2026-09-04 11:53:49" }
]
}
Update User
Changes a user's role (admin/user) and/or status (active/blocked). Admins cannot change their own role or status, and the last admin account can never be demoted or blocked.
Path Parameters
| Field | Type | Description |
|---|---|---|
id | integer | Target user ID |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
role | string | No | admin or user |
status | string | No | active or blocked (blocked users cannot log in or access protected endpoints) |
curl -X PUT https://your-host/reminder/api/users/2 \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{"role": "user", "status": "blocked"}'
{
"status": "success",
"message": "User updated successfully",
"cached": false,
"data": { "id": 2, "username": "john", "name": "John Client", "email": "user@example.com", "role": "user", "status": "blocked" }
}
Delete User
Permanently removes a user account and its data/{username}_{domain}.json profile file (admin only). An admin cannot delete their own account, and the last admin account cannot be deleted. The user's reminders are kept but become unowned and hidden from everyone.
Path Parameters
| Field | Type | Description |
|---|---|---|
id | integer | Target user ID |
curl -X DELETE https://your-host/reminder/api/users/5 \ -H "Authorization: Bearer <admin_token>"
{
"status": "success",
"message": "User deleted successfully.",
"cached": false
}
Storage & Data Strategy
No database is used. All data lives as flat JSON files in data/, namespaced so one codebase can serve multiple installations (local dev vs. the live site) without data leaking between them.
| File | Contents |
|---|---|
| data/{username}_{domain}.json | One profile per user: id, username, name, email, bcrypt password hash, role, status, created_at |
| data/users_{domain}.json | Index of users for that domain — id, username, name, email, role, status — never password hashes |
| data/reminders_{domain}.json | All reminders for that domain; each row carries a user_id locating its owner and, when recurring, the flat repeat_* fields + occurrences_sent/repeat_anchor |
| data/usernames.json | Optional, domain-agnostic: desired handles for pre-username accounts, keyed by user id or email (e.g. {"1":"system"}) |
Domain resolution. The web server uses the request's Host header; CLI scripts and cron use APP_URL from .env. Localhost variants (localhost, 127.0.0.1, ::1) resolve to the domain key local.
- Login is index-driven: users_{domain}.json is searched by email/username, then the bcrypt hash is loaded from the matching profile file. Never hand-edit the index.
- Usernames are the file key — 3-30 chars of [a-z0-9._-], unique per domain, filesystem-safe. Reserved: users, reminders.
- Legacy auto-migration: a legacy single-file layout (data/users.json + data/reminders.json, or the interim habib_{domain}.json) is detected and migrated on first read; originals are preserved as .bak files.
- CLI tools: cron/migrate_data.php (one-time migration, --dry-run / --domain=) and cron/rename_users.php (rename handles + index, safe to re-run).
- Example (production): data/system_reminder.darmist.com.json, data/users_reminder.darmist.com.json, data/reminders_reminder.darmist.com.json.
Caching Notes
- List and detail endpoints read from Redis first (TTL 3600s, 1 hour).
- A "cached": true flag in the response indicates the payload came from Redis.
- Cache is auto-invalidated on create, update, delete, send-now, and after cron dispatch. No manual reset endpoint exists.
- Use ?nocache=1 on the list endpoint to force a fresh read.
Notification Delivery
- WhatsApp is sent via the WhatsJoin provider API using the normalized +8801XXXXXXXXX number. Endpoint and key come from WHATSAPP_API_URL, WHATSAPP_SEND_KEY, WHATSAPP_ACCOUNT_ID in .env.
- Email is sent via SMTP configured in .env (MAIL_HOST / MAIL_PORT, e.g. mail.darmist.com:587).
- Message format: *Title* on the first line (WhatsApp bold) followed by the description.
- Description formatting: the description supports WhatsApp-style markup — *bold*, _italic_, ~strikethrough~, __underline__, and lines beginning with - or 1. for bullet/numbered lists. Bold, italic, strikethrough and lists are preserved on WhatsApp; the markup is converted to HTML for email. Underline is rendered in email only, since WhatsApp has no underline syntax (the __ markers are stripped from the WhatsApp text).
- Scheduling: a cron job runs cron/send_reminders.php every minute in the Asia/Dhaka timezone and dispatches every due pending reminder.
- If a channel has no recipient configured, it is skipped rather than failed.