Skip to main content

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:

  1. Unauthenticated public routes — health, login, signup, public marketplace, public lost-pet, share-redeem, password reset, Razorpay webhook.
  2. Tenant-scoped clinic group — under tenant/{tenant}/ with tenant.resolve middleware.
  3. Owner portal group — under owner/me/… with auth:sanctum.
  4. Tenant-scoped seller group — also under tenant/{tenant}/, separate group so the OrderFulfillmentController can register against the same prefix without crowding the clinic group.
  5. Signed routes — registered at the top level so a signed URL works for any client (e.g. pet document download).

Public surface

MethodPathControllerNotes
GET/system/healthSystemController@healthuptime
GET/dev/mail-testDevMailTestControllerlocal-only
POST/auth/loginAuthController@loginrate-limit recommended (audit)
POST/auth/owner/registerAuthController@registerOwner
GET/auth/meAuthController@meneeds Bearer
POST/auth/logoutAuthController@logoutsanctum
POST/auth/forgot-passwordPasswordResetController@requestResetthrottle:6,1
POST/auth/reset-passwordPasswordResetController@resetthrottle:10,1
GET/auth/verify-reset-tokenPasswordResetController@verify
POST/tenantsTenantController@storeself-serve onboarding
GET/providersProviderDirectoryController@indexfilters: business_type, q, near
GET/providers/{tenant}ProviderDirectoryController@show
POST/shares/redeemPetShareController@redeemdoctor-app QR redeem
GET/shares/{code}PetShareController@showlanding-page lookup
POST/webhooks/razorpayRazorpayWebhookControllersigned
GET/p/{slug}PublicPetController@showJsonlost-pet collar tag
POST/caregivers/acceptPetCaregiverController@acceptsanctum
GET/owner/me/pets/{pet}/documents/{document}/downloadPetDocumentController@downloadsigned

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 /dashboard
  • GET /clients
  • GET /pets, POST /pets
  • GET /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 kanban
  • GET /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 /invoices
  • POST /invoices/{invoice}/payments — record a manual payment
  • POST /invoices/{invoice}/refunds (payment.refund)
  • POST /invoices/{invoice}/convert (invoice.convert) — estimate → invoice
  • GET /invoices/{invoice}/receipt (invoice.read) — PDF
  • POST /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 on supplier.*)
  • Full CRUD + cancel + receive on /purchase-orders[/{purchaseOrder}] (gated on purchase_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).

MethodPathNotes
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/tenantstenants 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}/timelineunified
GET/me/pets/{pet}/vaccination-certificate.pdfaccepts ?vaccination_id=
GET/me/statement.pdfoptional ?from=&to=
*/me/shares (list / store / revoke)
POST/me/invoices/{invoice}/razorpay/order, /verifyRazorpay 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-meinverse view
GET/me/orders, /me/orders/{order}
POST/me/orders/{order}/cancel
GET/me/pets/{pet}/prescriptionsfor refill draft
POST/me/pets/{pet}/ordersplace order
GET/me/pets/{pet}/prescriptions/{prescription}/refill-draftbuilds 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 the tenant.resolve middleware, not because of the binding itself.
  • Signed URLs (signed middleware) are used for the pet document download — the URL itself is the authorization, short-lived.
  • require.permission middleware is the fast 4xx path; controllers double-check via enforcePermission for row-level decisions the URL alone can't model.