Reminder Service Automated Email & WhatsApp Notification Management
← Open Web App
API v1

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.
Privacy. All reminders are treated as private. Client-supplied is_private=false is ignored and forced to true.

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.

DeploymentBase URL
Web Root/api
Subfolder/reminder/api
Content Typeapplication/json
AuthenticationBearer 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.

EndpointAuth Required
POST /api/registerNo
POST /api/loginNo
GET /api/meYes
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

CodeMeaningTypical Cases
200SuccessData returned / operation completed
201CreatedUser or reminder created
400Bad RequestMissing fields, invalid email / WhatsApp number
401UnauthorizedMissing / invalid Bearer token, bad credentials
403ForbiddenAccessing another user's private reminder
404Not FoundReminder does not exist, unknown route
409ConflictRegistering with an email or username that already exists
422UnprocessableInvalid username (bad characters / too short / reserved name)
500Server ErrorStorage write failure, downstream provider error

Health Check & System Status

GET/?format=json

Returns current service health, PHP version, and Redis connection status. Accepts either ?format=json or Accept: application/json.

Headers / Query

ParameterValueDescription
Queryformat=jsonRequests JSON health status
HeaderAccept: application/jsonAlternative 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

POST/api/register

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

FieldTypeRequiredDescription
namestringYesUser display name
usernamestringYesUnique login handle & profile file key; 3-30 chars of [a-z0-9._-], lowercase. Reserved: users, reminders
emailstringYesUnique email address
passwordstringYesPlain password (hashed server-side)
Every new account is created with role: user and status: active. A role field sent in the request is ignored.
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

POST/api/login

Authenticates an existing user and returns a JWT token. The identifier may be the user's email or username (case-insensitive).

Request Body

FieldTypeRequiredDescription
identifierstringYesRegistered email address or username. Legacy clients may send email / username keys instead
passwordstringYesAccount 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..."
  }
}
Invalid credentials return 401 {"status":"error","message":"Invalid email/username or password."}

Get Current User

GET/api/me

Returns the profile of the authenticated user.

Headers

HeaderValue
AuthorizationBearer <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

GET/api/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

ParamTypeRequiredDescription
nocachestringNo1 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

POST/api/reminders

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

FieldTypeRequiredDescription
titlestringYesReminder title
descriptionstringNoExtra context. Supports lightweight markup: *bold*, _italic_, ~strikethrough~, __underline__ and lines beginning with - or 1. for lists.
datetimestringYesY-m-d H:i (e.g. 2026-10-01 10:00). Seconds appended automatically.
emailstringYes*Recipient email(s). Accepts a comma-separated list of up to 10 addresses. At least one contact required.
whatsapp_numberstringYes*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_modestringNonone | hourly | daily | weekly | monthly | every_6_months | custom. Default none (one-shot).
repeat_intervalintNoStep count (≥ 1). Only meaningful with repeat_mode=custom.
repeat_unitstringNohour | day | week | month | year (custom mode only).
repeat_end_typestringNodate | count or omit/null for "never".
repeat_end_valuestring/intNoY-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.

Multiple recipients. Both contact fields take a comma-separated list, up to 10 entries each (20 sends per occurrence at most). Entries are de-duplicated — emails case-insensitively, numbers after normalization — and stored back as a canonical comma-separated string, so a single value is simply a one-element list and previously stored reminders need no migration. Every entry is delivered as its own message, so one unreachable recipient never blocks the others. A channel counts as delivered if at least one recipient was reached; last_error lists every entry that failed. Exceeding 10, or supplying one malformed entry, returns 400 with the offending values named in errors[].
Recurring reminders. The stored datetime always holds the next occurrence. When the cron fires one it advances datetime to the following slot and keeps status pending until the schedule ends (then completed); occurrences_sent counts firings (the anchor itself counts as the first run) and is server-managed. If the cron was down, advancing jumps straight to the next future slot — missed occurrences are skipped, never replayed. Month math clamps to the short month's last day anchored on the original date — e.g. monthly on Jan 31 fires Feb 28, then Mar 31, Apr 30 … so it never drifts. Day/week steps keep the wall-clock time across DST transitions. Invalid scheduling fields (bad mode, bad unit, past end date, count < 1) return 400.
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"
  }
}
WhatsApp validation: a number must normalize to +8801[0-9]{9}. Invalid numbers return 400.

Get Reminder Detail

GET/api/reminders/{id}

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"
  }
}
Non-existent → 404. Another user's reminder → 403 "Access denied. Private reminder."

Update Reminder

PUT/api/reminders/{id}

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)

FieldTypeDescription
titlestringNew title
descriptionstringNew description. Supports the same lightweight markup as on create (*bold*, _italic_, ~strikethrough~, __underline__, lists).
datetimestringNew schedule (Y-m-d H:i)
emailstringNew recipient email(s) — comma-separated, up to 10
whatsapp_numberstringNew WhatsApp number(s) — comma-separated, up to 10
statusstringpending | sent | failed | completed | cancelled
repeat_modestringSame values as create; switching to a preset normalizes repeat_interval/repeat_unit for it.
repeat_intervalintStep count (custom mode).
repeat_unitstringhour | day | week | month | year (custom mode).
repeat_end_typestringdate | count or null/omitted for "never".
repeat_end_valuestring/intEnd 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.

Rescheduling: setting a new datetime (different from the stored value) on a reminder that is already sent, failed or completed resets its status to pending and clears sent_at, so the cron will dispatch it again at the new time (this is also how a finished recurring reminder is revived). Editing fields without changing datetime — or without actually changing the schedule — leaves status, occurrences_sent and repeat_anchor untouched.
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

DELETE/api/reminders/{id}

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)

DELETE/api/reminders

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

POST/api/reminders/{id}/send

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 }
      ]
    }
  }
}
Admin only. All /api/users* endpoints require the caller to hold the admin role. Non-admins receive 403 {"status":"error","message":"Admin access required."}

List Users

GET/api/users

Returns every registered user account (admin only). Password hashes are never included.

Headers

HeaderValue
AuthorizationBearer <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

PUT/api/users/{id}

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

FieldTypeDescription
idintegerTarget user ID

Request Body

FieldTypeRequiredDescription
rolestringNoadmin or user
statusstringNoactive 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

DELETE/api/users/{id}

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

FieldTypeDescription
idintegerTarget 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.

FileContents
data/{username}_{domain}.jsonOne profile per user: id, username, name, email, bcrypt password hash, role, status, created_at
data/users_{domain}.jsonIndex of users for that domain — id, username, name, email, role, status — never password hashes
data/reminders_{domain}.jsonAll 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.jsonOptional, 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.
Why profiles are separate files. Login needs the bcrypt hash, but list/detail responses and the admin user list never do. Keeping hashes out of the shared index avoids accidentally exposing them and lets each account live (and be deleted) as one file.

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.