Hamid Studio SMS

AI Agent Integration Manual

Implementation instructions for independent restaurant accounts and central admin registration.

Source reviewed 2026-09-23 ยท Android APK 1.1.0

Audience: AI coding agents. Source reviewed: 2026-09-23. Client contract: Android APK 1.1.0.

Canonical documentation: https://sms.hamid.studio/integration/

Machine-readable source: https://sms.hamid.studio/integration-manual.md

1. Objective and architecture

Complete BOTH deliverables for a new restaurant:

1. Independent restaurant access: its own HTTPS website origin plus a private SMS password in the Android app.

2. Central admin registration: the restaurant appears in the Hamid Studio admin catalog, initially disabled on every admin device.

Standalone flow: restaurant website/API -> restaurant database and queue -> restaurant Android device -> its SIM.

Admin flow: Android admin device -> https://sms.hamid.studio -> direct D1 binding to the restaurant database -> admin device SIM.

Both flows share one queue. Claims must be atomic across both consumers.

There is NO public restaurant signup/provisioning API on the hub. POST /api/device/restaurants does not create a restaurant. Creating a restaurant website or standalone login does not automatically register it in the hub. Central onboarding requires a Worker binding, an approved schema adapter, and a HUB restaurants row. Current central adapters use Cloudflare D1 bindings accessible to the hub's Cloudflare account. An arbitrary external REST API or cross-account database is not sufficient; supporting it requires a new connector.

Adding a compatible restaurant is a server-side change. The owner refreshes the restaurant list; no APK update is needed. A change to the native client protocol/features may require a new APK.

2. Inputs and authoritative sources

Collect: restaurant name, final HTTPS origin, country, IANA timezone, approved welcome message, repository path, database/schema access, and authorization to edit/deploy the restaurant and hub. Confirm whether the request includes a web admin dashboard; the SMS app does not automatically create one.

Choose a unique stable restaurant ID matching [a-z0-9-]+, a unique D1 binding name, and the verified D1 database ID. All demo-bistro, DEMOBISTRO, .example, and angle-bracket values below are placeholders, not provisioned accounts.

Typical local checkout names are hamid-studio-sms-bridge (Android) and hamid-studio-sms-delivery (hub/site), under E:\test project. Locate and verify the actual repository, cwd, Cloudflare account, Worker name and hostname before changing anything. Do not assume these paths exist in another agent environment.

Read current source before implementation:

- worker.mjs: routing, assets and APK download.

- hub/api.mjs: admin authentication, catalog, selection and device protocol.

- hub/adapters.mjs: allowed bindings and schema-specific adapters.

- hub/schema.sql: HUB schema; do NOT apply it to a restaurant database.

- wrangler.jsonc: deployment target and D1 bindings.

- Android MainActivity.java, ApiClient.java, SmsBridgeService.java, SiteUrl.java and JobIdentity.java: actual client behavior.

Do not use a fixed restaurant count as a registration check. Preserve all existing bindings, adapters, registry rows and owner selections. Current source takes precedence over stale examples; investigate and test any mismatch before integration.

3. Create independent restaurant access

In APK 1.1.0 an independent account means restaurant origin + private password, not a username on sms.hamid.studio. Provision isolated restaurant data/settings/subscribers/jobs and implement its device API on its own origin. A restaurant credential must never authorize another restaurant or central admin access.

Use a strong random password. Store a securely derived verifier, e.g. salted PBKDF2 with documented parameters, or use the project's existing secure identity system. Existing projects may use different verifier formats, including legacy SHA-256; inspect the implementation before changing a secret. Do not assume a universal environment variable name. Do not expose the password/verifier in frontend code, public assets, APK, HUB registry or test output.

Example secret installation, from the verified RESTAURANT checkout. SMS_PASSWORD_VERIFIER is a proposed name; it must match your implementation. Supply the verifier privately at the prompt, not in shell arguments:


node node_modules/wrangler/bin/wrangler.js secret put SMS_PASSWORD_VERIFIER --config wrangler.jsonc

On valid login issue a cryptographically random bearer token. Store its hash, device name, expiry and revocation state in the restaurant database. Rate-limit login attempts, reject invalid/expired/revoked sessions with 401 and excessive login attempts with 429. Password rotation alone does not revoke existing tokens; implement explicit session revocation.

Deliver the restaurant origin and password through an owner-approved private channel. Never issue a central admin pairing code as a restaurant credential. If a web dashboard is requested, separately implement restaurant-scoped web authentication, permissions and CSRF protection.

4. Standalone Android API contract

Base URL: the restaurant's bare HTTPS origin, e.g. https://demo-bistro.example. Android SiteUrl removes URL paths and www; ensure the resulting canonical origin serves the API. Do not implement tenant identity solely in /tenant/name paths. Release Android requires HTTPS.

Use application/json and Cache-Control: no-store for private responses. Protected routes use Authorization: Bearer <PRIVATE_DEVICE_TOKEN>. Do not require browser cookies or redirect to HTML login. Native Android does not require permissive CORS; restrict browser origins if browser clients use the API.

POST /api/device/login

Request:


{"password":"<PRIVATE_PASSWORD>","device_name":"Manufacturer Model"}

Success:


{"token":"<PRIVATE_DEVICE_TOKEN>","business":"Demo Bistro","base_url":"https://demo-bistro.example"}

Do NOT return role=admin for a restaurant. That field switches Android to the hub protocol; it is not a secure shortcut to restaurant admin access. Return a trusted origin belonging to this restaurant.

GET /api/device/settings

The enabled flag is a STRING in read responses:


{"message_template":"Owner-approved welcome message","auto_message_enabled":"0"}

PUT /api/device/settings

The enabled flag is a BOOLEAN in writes:


{"message_template":"Owner-approved welcome message","auto_message_enabled":false}

Return {"ok":true}. Android allows up to 500 characters. Enforce a compatible limit or return a clear 400 for a narrower existing restaurant limit. Preserve required unsubscribe links in the final queued message.

GET /api/device/stats


{"today":{"messages":0,"people":0},"week":{"messages":0,"people":0},"month":{"messages":0,"people":0},"all":{"messages":0,"people":0}}

All counts are numbers. Count successful jobs and distinct recipients per period. Document the time units, timezone and period definitions: existing adapters differ, and week/month may be rolling 7/30-day windows. Hub totals sum restaurant counts; they do not deduplicate recipients across restaurants. Do not confuse message counts with multipart SMS segments.

GET /api/device/jobs

WARNING: this route CLAIMS work. It is not a read-only health check. Android polls approximately every 20 seconds while running. Return [] when no eligible work exists, otherwise an array:


[{"id":123,"phone":"+962790000000","message":"Local fixture message only"}]

The example phone is not an approved sending destination. Standalone IDs must be positive integers, not UUIDs. Return E.164 phone numbers with + and country code. Android's local-number shortcut is Jordan-specific; normalize other countries on the server. Preserve the full queued message.

POST /api/device/jobs/123/complete


{"success":true,"parts":1,"error":""}

Validate types and bounds, including boolean success and integer parts. The hub allows 0..50 parts and truncates error to 200 characters; use a compatible contract. Only the device owning the claimed job may complete it. Persist outcome/parts/error without double-counting repeated callbacks. Return {"ok":true} on success; use idempotent success or a consistent 409 for an already completed job. Android send-callback success does not prove recipient receipt or reading.

POST /api/device/stop

Request {}. Response {"ok":true}. Stop this device's consumption, not all restaurant devices or the entire queue. Do not delete subscribers or jobs. Standalone logout in APK 1.1.0 clears local credentials and does not call /logout; provide server-side session revocation for administrators.

Critical APK 1.1.0 start/resume behavior

Standalone Start does NOT call POST /api/device/start: it starts polling directly. Only central admin mode calls /start. Do not make standalone jobs wait for a /start request that never arrives.

A compatible implementation may treat an authenticated /jobs request as resume intent from an authorized, non-revoked device, while checking subscriber consent and restaurant settings. /stop marks the device stopped; later authenticated polling can resume. Revocation is a separate state and polling must never override it. Verify Start -> Stop -> Start with the actual APK. Do not remove the app's manual start action.

5. Queue, consent and concurrency

Collect explicit subscriber consent with timestamp/version. Normalize phone numbers server-side. Unsubscribe must revoke consent and cancel pending jobs. Deduplicate welcome enqueue operations so HTTP retries do not create duplicate welcomes.

New restaurants start with auto_message_enabled=0. Explain the independent controls: restaurant automatic welcomes, restaurant selection on the admin device, and the device's global sending service. Existing adapters may separately allow explicitly requested manual jobs; inspect their source policy.

Atomic claim example for ONE possible schema, not a universal migration. Check restaurant settings before claiming, and adapt columns/status/time units to the real schema:


UPDATE sms_jobs

SET status='claimed', device_id=?, claimed_at=CURRENT_TIMESTAMP

WHERE id=(

 SELECT j.id FROM sms_jobs j

 JOIN subscribers s ON s.phone=j.phone

 WHERE j.status='pending' AND s.status='active'

 ORDER BY j.id LIMIT 1

) AND status='pending'

RETURNING id,phone,message;

Never use an unguarded SELECT followed by UPDATE. Standalone and hub must agree on state transitions and claim ownership. If device_id has a foreign key, the hub adapter must create its device record before claiming.

Do not automatically requeue a timed-out claim whose SMS may already have reached the modem. Investigate uncertain delivery before retrying. APK 1.1.0 has no durable acknowledgement retry mechanism; do not claim exactly-once delivery. A message already handed to the modem cannot be recalled by Stop.

6. Register the restaurant in central admin

Run this procedure from the CURRENT HUB checkout with authorized operator access. Standalone credentials are not Cloudflare credentials. Public access to this manual grants no hub access.

1. Implement, test and deploy the restaurant schema and standalone API. Verify the actual D1 ID.

2. Add its D1 binding to the hub wrangler.jsonc d1_databases array, preserving every existing entry.

3. Add its binding/profile pair to the fixed allowed map in hub/adapters.mjs. Reuse a profile only after proving full schema compatibility. For a new profile, add it to types and implement any required method branches.

4. Add an isolated real-schema fixture and tests for claims, consent, settings, completion, stats and cross-device isolation. Run the entire hub test suite.

5. Deploy the hub Worker with the new binding and adapter FIRST. Only after successful deployment apply a separate SQL migration inserting the HUB restaurants row.

6. Verify catalog/settings/stats using a private test admin session with sending inactive. Revoke that test session afterward. Do not consume the owner's one-time pairing code for QA.

7. The owner refreshes the restaurant list. The restaurant appears disabled until they select it. Do not create enabled=1 device_restaurants rows on their behalf.

Example new binding; replace all demo values:


{"binding":"DEMOBISTRO","database_name":"demo-bistro-db","database_id":"<REAL_D1_UUID>"}

Example addition INSIDE the existing allowed map, only after matching the ulo profile:


DEMOBISTRO: 'ulo'

Matching table names alone is insufficient. Check columns, status values, consent joins, foreign keys, timestamp units, settings limits and ready behavior. If incompatible, implement a dedicated adapter; do not reshape an existing restaurant merely to force compatibility.

Example new file hub/add-demo-bistro.sql:


INSERT INTO restaurants(id,name,site,binding,adapter,available)

VALUES('demo-bistro','Demo Bistro','https://demo-bistro.example',

       'DEMOBISTRO','ulo',1);

Check ID uniqueness first. If it already exists, compare identity/domain/database before editing; do not silently overwrite another account with an upsert. binding must exactly match Wrangler and the allowlist; adapter must match types and the allowlist. available=1 means visible, not enabled on a device. Do not store a restaurant password in this table.

Run only after replacing placeholders, creating the migration and confirming the correct target:


node node_modules/wrangler/bin/wrangler.js whoami

npm test

node node_modules/wrangler/bin/wrangler.js deploy --config wrangler.jsonc --dry-run

node node_modules/wrangler/bin/wrangler.js deploy --config wrangler.jsonc

node node_modules/wrangler/bin/wrangler.js d1 execute HUB --config wrangler.jsonc --remote --file hub/add-demo-bistro.sql

node node_modules/wrangler/bin/wrangler.js d1 execute HUB --config wrangler.jsonc --remote --command "SELECT id,name,site,binding,adapter,available FROM restaurants WHERE id='demo-bistro'"

Pass --config explicitly to avoid inheriting a parent project's deployment config. If deployment fails, do not apply the registry insertion. Visibility rollback: set available=0 for the specific new restaurant and coordinate any device deactivation with its owner; preserve all restaurant data. Do not rerun the root schema as an onboarding migration.

7. Adapter implementation contract

- settings(): return message_template and string auto_message_enabled. Some schemas use key/value settings, others a single settings.id=1 row.

- saveSettings(body): accept a boolean enabled value; persist according to the actual schema and restaurant message limit.

- claim(device): check consent/settings and atomically claim at most one job, returning id/phone/message. Hub ownership is 'hub-' + device.id.

- ready(device,id): check claim ownership, claimed status, consent and applicable settings immediately before sending. Android admin mode calls this through the hub. Some existing profiles currently return true without these database checks; do not assume all profiles implement revalidation. Implement and test this for a new profile.

- complete(device,id,result): scope updates to the correct restaurant, owner and claimed state. Job ID alone is insufficient.

- stats(): return the four numeric period objects. Respect SQL datetime vs milliseconds vs seconds.

Known schema differences: Dhoo uses queued/subscribed and millisecond timestamps; Chinampita uses dispatched and seconds; Bungly and Chinampita resolve phones through subscriber_id. Other profiles commonly use pending/claimed and app_settings. Read current source for the full mapping.

The hub wraps numeric IDs as demo-bistro~123 to prevent cross-restaurant collisions. Standalone APIs normally return 123; the hub adds the prefix. Preserve numeric IDs end to end.

8. Central admin identity and API

Central admin access covers ALL available restaurants, not a restricted restaurant account. Only an authorized hub operator may issue a new device code:


node hub/issue-admin-code.mjs unique-device-label

The current script writes private/unique-device-label-pairing.txt and stores the code hash in HUB. Codes are single-use and expire after seven days; device sessions last one year unless revoked. Use a unique label. Never publish that file, token, code or subscriber data. Never issue this code to satisfy independent restaurant access.

Hub routes (protected routes require the hub-issued bearer token):

- POST /api/device/login: password is the central pairing code; success includes role=admin.

- GET /api/device/restaurants: [{id,name,site,enabled}]. No anonymous catalog access.

- PUT /api/device/restaurants/demo-bistro: {"enabled":true} or false; selection is per admin device.

- GET/PUT /api/device/restaurants/demo-bistro/settings: restaurant settings contract above.

- GET /api/device/restaurants/demo-bistro/stats: restaurant period counts.

- POST /api/device/start and /stop: activate/deactivate the admin device.

- POST /api/device/logout: deactivate and revoke this admin session.

- GET /api/device/settings: {"role":"admin","active":false} when inactive.

- GET /api/device/jobs: claims from enabled restaurants while active; adds restaurant_id, restaurant name and composite id.

- GET /api/device/jobs/demo-bistro~123/ready: {"ready":true} or false; checks device/selection and adapter readiness.

- POST /api/device/jobs/demo-bistro~123/complete: completion body above; wrong owner/state returns 409.

- GET /api/device/stats: totals for selected restaurants plus an unavailable array.

Hub JSON request bodies are limited to 4096 bytes and require application/json. An Origin header, if present, must match the hub origin. Do not log credentials in curl commands, screenshots or reports.

9. Acceptance checklist

- Valid/invalid login; expired/revoked token; restaurant A token rejected by B and hub.

- GET settings string flag vs PUT boolean; message validation; known fixture stats for all periods.

- Three concurrent polls against one fixture queue, including standalone and hub consumers: one job claimed only once.

- Wrong-device completion rejected; repeated completion does not inflate counts; the same numeric job ID in two restaurants remains distinct.

- Unconsented/unsubscribed customers are excluded; disabling settings before claim/ready applies the documented policy.

- Country-specific international phones, full message text and unsubscribe link preserved. App UI language does not rewrite customer messages.

- Actual APK standalone login/settings/Start/Stop/Start; admin catalog refresh reveals the new disabled restaurant without reinstalling.

- Safe production verification: HTTPS, unauthenticated 401, authenticated settings/stats, correct admin catalog ID and disabled selection. Do not poll an active production queue for testing.

- Real SMS only when explicitly authorized, using one approved test recipient, known message and correct SIM. Track callback and obtain recipient receipt confirmation. Local/API tests do not prove physical SMS delivery.

10. Troubleshooting

Missing from admin: inspect the production HUB row, available=1, correct hub deployment, admin session and list refresh. A working website API alone does not register it.

Visible but settings fail: compare binding/profile/allowlist/schema and database access, including the deployed Worker version.

Standalone login opens admin UI: remove role=admin from the restaurant response and verify base_url. sms.hamid.studio login accepts an admin pairing code, not a restaurant password.

Connected but no SMS: inspect the three activation controls, consent, pending jobs, Android SMS/notification/battery permissions, SIM readiness and standalone start behavior. Hub polling can skip failing adapters; connected status does not prove every restaurant database is healthy.

Claimed job with unknown outcome: investigate instead of resetting it to pending automatically.

Errors: 401 session/credentials; 400 invalid values; 409 ownership/state conflict; 413 oversized hub body; 415 wrong content type; 429 login limit; 503 inspect private operator logs and reproduce SQL locally without leaking tokens/phone data.

11. Required handoff

Report restaurant ID/origin, verified checkout, private credential delivery method, standalone deployment, central binding/profile/migration, catalog visibility, test evidence and remaining owner steps. Keep secrets separate.

Distinguish these states explicitly: independent login ready; central registration complete; owner enabled sending; physical SMS receipt verified. Do not present any one as proof of the others.

If you lack hub access, finish the authorized restaurant work and provide an operator handoff containing id/name/site, D1 metadata, schema, proposed adapter and tests. Do not claim central registration succeeded, and do not include customer records or restaurant passwords in that handoff.

12. Copyable agent task


Read https://sms.hamid.studio/integration-manual.md fully.

Integrate restaurant [NAME] at [HTTPS_ORIGIN], repository [PATH], country [COUNTRY], timezone [IANA_ZONE].

Implement independent restaurant SMS login, then register it in central Hamid Studio admin.

Inspect current Android/hub source and actual restaurant schema before editing.

Preserve other restaurants. Never grant central admin access to a restaurant account.

Keep new automatic sending and admin device selections disabled pending owner activation.

Test authentication isolation, atomic claims, consent, completion, stats and international phones.

Deploy only to verified authorized targets, then verify safely without claiming production jobs.

Do not send real SMS without explicit authorization and an approved test recipient.

Deliver deployment and catalog evidence, private account access and remaining owner steps.

If hub access is unavailable, provide the complete registration handoff; do not claim it is registered.