API Routes
The full route catalog lives in backend/routes/api.php. Everything sits under the /api/v1 prefix. Web HTML routes (lost-pet landing page, password-reset stub) are in backend/routes/web.php.
The route file is organised in five tiers:
- Unauthenticated public routes — health, login, signup, public marketplace, public lost-pet, share-redeem, password reset, Razorpay webhook.
- Tenant-scoped clinic group — under
tenant/{tenant}/withtenant.resolvemiddleware. - Owner portal group — under
owner/me/…withauth:sanctum. - Tenant-scoped seller group — also under
tenant/{tenant}/, separate group so theOrderFulfillmentControllercan register against the same prefix without crowding the clinic group. - Signed routes — registered at the top level so a signed URL works for any client (e.g. pet document download).
Public surface
| Method | Path | Controller | Notes |
|---|---|---|---|
| GET | /system/health | SystemController@health | uptime |
| GET | /dev/mail-test | DevMailTestController | local-only |
| POST | /auth/login | AuthController@login | rate-limit recommended (audit) |
| POST | /auth/owner/register | AuthController@registerOwner | |
| GET | /auth/me | AuthController@me | needs Bearer |
| POST | /auth/logout | AuthController@logout | sanctum |
| POST | /auth/forgot-password | PasswordResetController@requestReset | throttle:6,1 |
| POST | /auth/reset-password | PasswordResetController@reset | throttle:10,1 |
| GET | /auth/verify-reset-token | PasswordResetController@verify | |
| POST | /tenants | TenantController@store | self-serve onboarding |
| GET | /providers | ProviderDirectoryController@index | filters: business_type, q, near |
| GET | /providers/{tenant} | ProviderDirectoryController@show | |
| POST | /shares/redeem | PetShareController@redeem | doctor-app QR redeem |
| GET | /shares/{code} | PetShareController@show | landing-page lookup |
| POST | /webhooks/razorpay | RazorpayWebhookController | signed |
| GET | /p/{slug} | PublicPetController@showJson | lost-pet collar tag |
| POST | /caregivers/accept | PetCaregiverController@accept | sanctum |
| GET | /owner/me/pets/{pet}/documents/{document}/download | PetDocumentController@download | signed |
Tenant-scoped clinic group
Prefix tenant/{tenant}/, middleware tenant.resolve. Permissions on every endpoint via require.permission:<key>.
Settings, dashboard, clients, pets
GET /settings(tenant.settings.read)PATCH /settings(tenant.settings.update)GET /dashboardGET /clientsGET /pets,POST /petsGET /patients/{pet}(patient.read)GET /patients/{pet}/timeline(medical_record.read)POST /patients/onboard(single-call create-client + pet)PATCH /patients/{pet}
Visits, prescriptions, lab results, attachments
CRUD endpoints under /patients/{pet}/visits|prescriptions|lab-results, all gated on medical_record.* or prescription.* or lab_result.* permissions. Visits also expose /visits/{visit}/discharge-summary.pdf.
The polymorphic attachment endpoints sit at /medical-attachments (index / store / show / /download / destroy).
Vaccinations + health logs (clinic side)
- CRUD:
/patients/{pet}/vaccinations[/{vaccination}](medical_record.*) - Read:
/patients/{pet}/health-logs(medical_record.read) - Write:
POST /patients/{pet}/health-logs(medical_record.create)
Appointments
GET /appointments(appointment.read)GET /appointments/queue— receptionist's kanbanGET /appointments/{appointment},POST /appointments,PUT /appointments/{appointment}(appointment.update)PATCH /appointments/{appointment}— legacy status-only patch (kept for back-compat)POST /appointments/{appointment}/cancel(appointment.cancel)
Billing
GET /invoices,GET /invoices/{invoice}(invoice.read),POST /invoicesPOST /invoices/{invoice}/payments— record a manual paymentPOST /invoices/{invoice}/refunds(payment.refund)POST /invoices/{invoice}/convert(invoice.convert) — estimate → invoiceGET /invoices/{invoice}/receipt(invoice.read) — PDFPOST /invoices/{invoice}/receipt/email(reminder.send)
Inventory
GET /medicines/summary | /alerts/low-stock | /alerts/expiry- Full CRUD on
/medicines[/{medicine}] GET /medicines/{medicine}/stock-movements,GET|POST /stock-movements- Full CRUD on
/suppliers[/{supplier}](gated onsupplier.*) - Full CRUD +
cancel+receiveon/purchase-orders[/{purchaseOrder}](gated onpurchase_order.*)
Admin (RBAC + audit)
/admin/permission-catalog(role.read)/admin/roles[/{role}](role.read/role.manage)/admin/users/{user}/roles(role.read)POST /admin/user-roles,DELETE /admin/users/{user}/roles/{role}(role.assign)GET /admin/audit-log(audit.read)
Reminders & opt-outs
GET /reminders/health(driver health-check)- Full CRUD-ish on
/reminders+retry/dismiss - Opt-out CRUD on
/reminder-opt-outs[/{optOut}]
Owner portal group
Prefix owner/me/, middleware auth:sanctum. The authenticated user's id is the scoping key (no client-supplied owner_user_id).
| Method | Path | Notes |
|---|---|---|
| GET | /me/pets, POST /me/pets, PATCH /me/pets/{pet} | |
| GET | /me/appointments | |
| GET | /me/invoices, /me/invoices/{invoice}, /me/invoices/{invoice}/receipt | |
| GET | /me/tenants | tenants this user is associated with |
| GET | /me/pets/{pet}/vaccinations, full CRUD | |
| GET | /me/vaccinations/due | "Due Soon" cross-pet feed |
| GET | /me/pets/{pet}/timeline | unified |
| GET | /me/pets/{pet}/vaccination-certificate.pdf | accepts ?vaccination_id= |
| GET | /me/statement.pdf | optional ?from=&to= |
* | /me/shares (list / store / revoke) | |
| POST | /me/invoices/{invoice}/razorpay/order, /verify | Razorpay integration |
* | /me/pets/{pet}/health-logs (index / store / destroy) | |
* | /me/pets/{pet}/documents (index / store / show / destroy) | download is signed (above) |
| GET | /me/pets/{pet}/passport.pdf | |
* | /me/pets/{pet}/public-profile (update / disable / /lost / DELETE /lost) | |
* | /me/pets/{pet}/caregivers (index / store / resend / revoke) | |
| GET | /me/caregivers/shared-with-me | inverse view |
| GET | /me/orders, /me/orders/{order} | |
| POST | /me/orders/{order}/cancel | |
| GET | /me/pets/{pet}/prescriptions | for refill draft |
| POST | /me/pets/{pet}/orders | place order |
| GET | /me/pets/{pet}/prescriptions/{prescription}/refill-draft | builds the refill payload |
Owner pet vaccination + clinic vaccination split
Owner-side: /owner/me/pets/{pet}/vaccinations (no tenant in URL).
Clinic-side: /tenant/{tenant}/patients/{pet}/vaccinations.
Both routes hit App\Http\Controllers\Api\V1\PetVaccinationController. The controller signatures accept optional ?Tenant $tenant = null so Laravel's ControllerDispatcher wires the URL {tenant} parameter via SubstituteBindings rather than positionally injecting the raw scalar id into Pet $pet (which would TypeError).
Tenant-scoped seller group
Same tenant/{tenant}/ prefix, but distinct routes for the seller's order inbox:
GET /orders(order.read)GET /orders/{order}(order.read)POST /orders/{order}/accept | /reject | /fulfill(order.manage)
Permissions are seeded onto the pharmacy and retail_counter roles.
Web routes
backend/routes/web.php carries:
GET /p/{slug}— HTML lost-pet landing page (separate template from the JSON endpoint).GET /password-reset/{token}— minimal landing for the email link to deep-link into the SPA / app.
Routing conventions worth knowing
- Implicit binding is the default everywhere a placeholder appears.
scopeBindings()is recommended (audit) but not yet uniformly applied — without it,/tenant/{tenant}/patients/{pet}with a pet from a different tenant currently 404s only because of thetenant.resolvemiddleware, not because of the binding itself.- Signed URLs (
signedmiddleware) are used for the pet document download — the URL itself is the authorization, short-lived. require.permissionmiddleware is the fast 4xx path; controllers double-check viaenforcePermissionfor row-level decisions the URL alone can't model.